<?php
/**
 * Main plugin controller.
 *
 * @package MarkPacuraAlbumBuilder
 */

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

final class MPAB_Plugin {
	const OPTION_KEY = 'mpab_settings';
	const POST_TYPE  = 'mpab_submission';
	const NONCE_KEY  = 'mpab_submit';
	const SETUP_SEEN = 'mpab_setup_seen';
	const EMAIL_TEST_STATE = 'mpab_email_test_state';
	const ORDER_SEQUENCE = 'mpab_order_sequence';
	const ORDER_CAPABILITY = 'manage_mpab_orders';
	const RETENTION_CRON = 'mpab_cleanup_expired_orders';
	const SUBMISSION_RATE_LIMIT = 5;
	const SUBMISSION_IP_LIMIT = 20;
	const SUBMISSION_SITE_LIMIT = 100;
	const SUBMISSION_RATE_WINDOW = 900;

	/** @var MPAB_Plugin|null */
	private static $instance = null;

	/** @var string Most recent synchronous WordPress mail error. */
	private $last_mail_error = '';

	/** @var string Envelope sender used only while Album Studio sends a message. */
	private $mail_sender_email = '';

	/** @var string Plain-text alternative used only while Album Studio sends a message. */
	private $mail_alt_body = '';

	/** @var array<string,mixed>|null Temporary settings used by the private client preview only. */
	private $preview_settings_override = null;

	/**
	 * Return the shared plugin instance.
	 *
	 * @return MPAB_Plugin
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/** Render the shared Studio banner used throughout the plugin dashboard. */
	public static function render_studio_hero() {
		?>
		<header class="mpab-admin-hero mpab-studio-home__hero">
			<div class="mpab-studio-home__brandline"><img class="mpab-admin-hero__logo" src="<?php echo esc_url( MPAB_URL . 'assets/images/wooden-banana-logo.png' ); ?>" width="992" height="306" alt="<?php esc_attr_e( 'Wooden Banana', 'mark-pacura-album-builder' ); ?>"><div class="mpab-admin-hero__title" aria-hidden="true"><?php esc_html_e( 'Studio', 'mark-pacura-album-builder' ); ?></div></div>
			<p><?php esc_html_e( 'The simplest way to help your clients preview, select, and buy heirloom albums and wall displays straight from your site, delivering a complete print experience with zero friction.', 'mark-pacura-album-builder' ); ?></p>
		</header>
		<?php
	}

	/**
	 * Seed safe defaults without creating or modifying a public page.
	 *
	 * @return void
	 */
	public static function activate() {
		if ( false === get_option( self::OPTION_KEY, false ) ) {
			add_option( self::OPTION_KEY, self::defaults() );
		}
		if ( false === get_option( self::ORDER_SEQUENCE, false ) ) {
			add_option( self::ORDER_SEQUENCE, 10, '', false );
		}
		delete_option( self::SETUP_SEEN );
		self::grant_order_capability();
		self::ensure_retention_schedule();
	}

	/** Stop background cleanup while the plugin is inactive. */
	public static function deactivate() {
		wp_clear_scheduled_hook( self::RETENTION_CRON );
	}

	/** Ensure existing installations receive the daily retention task too. */
	public static function ensure_retention_schedule() {
		if ( ! wp_next_scheduled( self::RETENTION_CRON ) ) {
			wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::RETENTION_CRON );
		}
	}

	/**
	 * Keep private client orders limited to trusted Album Studio administrators.
	 * This also upgrades existing installations that predate the capability.
	 *
	 * @return void
	 */
	public static function grant_order_capability() {
		$administrator = get_role( 'administrator' );
		if ( $administrator && ! $administrator->has_cap( self::ORDER_CAPABILITY ) ) {
			$administrator->add_cap( self::ORDER_CAPABILITY );
		}
	}

	/**
	 * Wire WordPress hooks.
	 */
	private function __construct() {
		add_action( 'init', array( $this, 'load_textdomain' ), 0 );
		add_action( 'change_locale', array( $this, 'reload_textdomain' ) );
		add_action( 'init', array( $this, 'register_submission_type' ) );
		add_action( 'admin_menu', array( $this, 'register_admin_menu' ) );
		add_action( 'admin_init', array( $this, 'register_settings' ) );
		add_action( 'admin_init', array( __CLASS__, 'grant_order_capability' ) );
		add_action( 'admin_init', array( $this, 'add_privacy_policy_content' ) );
		add_action( 'admin_notices', array( $this, 'render_setup_notice' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
		add_action( 'admin_post_mpab_send_test_email', array( $this, 'send_test_email' ) );
		add_action( 'admin_post_mpab_confirm_test_email', array( $this, 'confirm_test_email' ) );
		add_action( 'admin_post_mpab_resend_client_email', array( $this, 'resend_client_email' ) );
		add_action( 'admin_post_mpab_remove_order', array( $this, 'remove_order' ) );
		add_action( 'admin_post_mpab_mark_order_read', array( $this, 'mark_submission_as_read' ) );
		add_action( 'admin_post_mpab_apply_starter_offer', array( $this, 'apply_starter_offer' ) );
		add_action( 'admin_post_mpab_save_studio_settings', array( $this, 'save_studio_settings' ) );
		add_action( 'admin_post_mpab_save_studio_promotions', array( $this, 'save_studio_promotions' ) );
		add_action( 'template_redirect', array( $this, 'render_private_client_preview' ) );
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
		add_action( 'mpab_retry_client_email', array( $this, 'retry_client_email' ), 10, 1 );
		add_action( self::RETENTION_CRON, array( $this, 'delete_expired_orders' ) );
		add_action( 'init', array( __CLASS__, 'ensure_retention_schedule' ) );
		add_action( 'add_meta_boxes', array( $this, 'register_submission_metabox' ) );

		add_shortcode( 'mp_album_builder', array( $this, 'render_builder' ) );

		add_filter( 'plugin_action_links_' . plugin_basename( MPAB_FILE ), array( $this, 'plugin_action_links' ) );
		add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_privacy_exporter' ) );
		add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_privacy_eraser' ) );
		add_filter( 'manage_' . self::POST_TYPE . '_posts_columns', array( $this, 'submission_columns' ) );
		add_action( 'manage_' . self::POST_TYPE . '_posts_custom_column', array( $this, 'render_submission_column' ), 10, 2 );
	}

	/** Load the bundled translation for the current WordPress language. */
	public function load_textdomain() {
		load_plugin_textdomain(
			'mark-pacura-album-builder',
			false,
			dirname( plugin_basename( MPAB_FILE ) ) . '/languages'
		);
	}

	/**
	 * Reload this plugin's translation when WordPress changes language.
	 *
	 * @param string $locale New WordPress locale.
	 */
	public function reload_textdomain( $locale ) {
		unload_textdomain( 'mark-pacura-album-builder', true );

		$translation_file = MPAB_DIR . 'languages/mark-pacura-album-builder-' . $this->normalise_locale( $locale ) . '.mo';
		if ( is_readable( $translation_file ) ) {
			load_textdomain( 'mark-pacura-album-builder', $translation_file );
			return;
		}

		$this->load_textdomain();
	}

	/**
	 * Keep locale values small and predictable before saving them with an order.
	 *
	 * @param mixed $locale Locale supplied by the public form.
	 * @return string
	 */
	private function normalise_locale( $locale ) {
		$locale = is_string( $locale ) ? trim( $locale ) : '';
		if ( ! preg_match( '/^[a-z]{2,3}(?:_[A-Z]{2})?(?:_[A-Za-z0-9]+)?$/', $locale ) ) {
			$locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
		}

		return substr( $locale, 0, 20 );
	}

	/** Suggest clear wording for the site's WordPress privacy policy guide. */
	public function add_privacy_policy_content() {
		if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
			return;
		}
		$settings = $this->settings();
		$months = isset( $settings['order_retention_months'] ) ? absint( $settings['order_retention_months'] ) : 24;
		$content = sprintf(
			'<p>%s</p><p>%s</p>',
			esc_html__( 'When an order is submitted, contact details, messages, product selections, and uploaded image files are securely stored to process the request, coordinate production and payment, and retain order history.', 'mark-pacura-album-builder' ),
			esc_html( sprintf( __( 'Order records are automatically purged after %d months. You may request data export or early deletion at any time.', 'mark-pacura-album-builder' ), $months ) )
		);
		wp_add_privacy_policy_content( __( 'Wooden Banana Studio', 'mark-pacura-album-builder' ), wp_kses_post( $content ) );
	}

	/** Register saved album orders with WordPress's personal-data exporter. */
	public function register_privacy_exporter( $exporters ) {
		$exporters['mpab-album-orders'] = array(
			'exporter_friendly_name' => __( 'Album Studio orders', 'mark-pacura-album-builder' ),
			'callback'               => array( $this, 'export_personal_data' ),
		);
		return $exporters;
	}

	/** Register saved album orders with WordPress's personal-data eraser. */
	public function register_privacy_eraser( $erasers ) {
		$erasers['mpab-album-orders'] = array(
			'eraser_friendly_name' => __( 'Album Studio orders', 'mark-pacura-album-builder' ),
			'callback'             => array( $this, 'erase_personal_data' ),
		);
		return $erasers;
	}

	/** Find order IDs for one validated email in predictable pages. */
	private function personal_data_order_ids( $email_address, $page ) {
		$email_address = sanitize_email( $email_address );
		if ( ! is_email( $email_address ) ) {
			return array();
		}
		return get_posts(
			array(
				'post_type'      => self::POST_TYPE,
				'post_status'    => array( 'private', 'trash' ),
				'posts_per_page' => 50,
				'paged'          => max( 1, absint( $page ) ),
				'fields'         => 'ids',
				'orderby'        => 'ID',
				'order'          => 'ASC',
				'meta_key'       => '_mpab_client_email',
				'meta_value'     => $email_address,
			)
		);
	}

	/** Export all client-visible and client-supplied values from saved orders. */
	public function export_personal_data( $email_address, $page = 1 ) {
		$order_ids = $this->personal_data_order_ids( $email_address, $page );
		$data = array();
		foreach ( $order_ids as $post_id ) {
			$configuration = json_decode( (string) get_post_meta( $post_id, '_mpab_configuration', true ), true );
			$data[] = array(
				'group_id'    => 'mpab-album-orders',
				'group_label' => __( 'Album orders', 'mark-pacura-album-builder' ),
				'item_id'     => 'mpab-order-' . absint( $post_id ),
				'data'        => array(
					array( 'name' => __( 'Order number', 'mark-pacura-album-builder' ), 'value' => (string) get_post_meta( $post_id, '_mpab_reference', true ) ),
					array( 'name' => __( 'Names', 'mark-pacura-album-builder' ), 'value' => (string) get_post_meta( $post_id, '_mpab_client_name', true ) ),
					array( 'name' => __( 'Email', 'mark-pacura-album-builder' ), 'value' => (string) get_post_meta( $post_id, '_mpab_client_email', true ) ),
					array( 'name' => __( 'Client note', 'mark-pacura-album-builder' ), 'value' => (string) get_post_meta( $post_id, '_mpab_client_note', true ) ),
					array( 'name' => __( 'Album choices', 'mark-pacura-album-builder' ), 'value' => wp_json_encode( is_array( $configuration ) ? $configuration : array(), JSON_PRETTY_PRINT ) ),
					array( 'name' => __( 'Submitted', 'mark-pacura-album-builder' ), 'value' => get_post_field( 'post_date', $post_id ) ),
				),
			);
		}
		return array( 'data' => $data, 'done' => count( $order_ids ) < 50 );
	}

	/** Permanently remove matching saved orders when WordPress fulfils erasure. */
	public function erase_personal_data( $email_address, $page = 1 ) {
		// Always remove the first remaining page; deletion closes pagination gaps.
		$order_ids = $this->personal_data_order_ids( $email_address, 1 );
		$removed = false;
		foreach ( $order_ids as $post_id ) {
			$idempotency_key = (string) get_post_meta( $post_id, '_mpab_idempotency_key', true );
			if ( 1 === preg_match( '/^[a-f0-9]{32}$/', $idempotency_key ) ) {
				delete_option( '_mpab_idem_' . $idempotency_key );
			}
			$removed = (bool) wp_delete_post( $post_id, true ) || $removed;
		}
		return array(
			'items_removed'  => $removed,
			'items_retained' => false,
			'messages'       => array(),
			'done'           => count( $order_ids ) < 50,
		);
	}

	/** Permanently delete Studio orders older than the photographer's chosen period. */
	public function delete_expired_orders() {
		$settings = $this->settings();
		$allowed_months = array( 3, 6, 12, 24, 36 );
		$months = isset( $settings['order_retention_months'] ) ? absint( $settings['order_retention_months'] ) : 24;
		if ( ! in_array( $months, $allowed_months, true ) ) {
			$months = 24;
		}
		$cutoff = current_datetime()->modify( '-' . $months . ' months' )->format( 'Y-m-d H:i:s' );

		// Five bounded batches prevent one busy site from monopolising a cron request.
		for ( $batch = 0; $batch < 5; $batch++ ) {
			$order_ids = get_posts(
				array(
					'post_type'      => array( self::POST_TYPE, 'mpab_wall_order' ),
					'post_status'    => array( 'private', 'trash' ),
					'posts_per_page' => 100,
					'fields'         => 'ids',
					'orderby'        => 'ID',
					'order'          => 'ASC',
					'no_found_rows'  => true,
					'date_query'     => array(
						array(
							'column'    => 'post_date',
							'before'    => $cutoff,
							'inclusive' => true,
						),
					),
				)
			);
			if ( empty( $order_ids ) ) {
				break;
			}
			foreach ( $order_ids as $post_id ) {
				$idempotency_key = (string) get_post_meta( $post_id, '_mpab_idempotency_key', true );
				if ( 1 === preg_match( '/^[a-f0-9]{32}$/', $idempotency_key ) ) {
					delete_option( '_mpab_idem_' . $idempotency_key );
				}
				wp_delete_post( $post_id, true );
			}
			if ( count( $order_ids ) < 100 ) {
				break;
			}
		}
	}

	/**
	 * Prototype defaults. Prices are examples and deliberately easy to replace.
	 *
	 * @return array<string,mixed>
	 */
	public static function defaults() {
		$defaults = array(
			'notification_email' => get_option( 'admin_email', '' ),
			'email_sender_name'   => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ),
			'currency'           => 'GBP',
			'show_prices'        => 1,
			'accent_colour'      => '#C47750',
			'promotion_enabled'  => 0,
			'promotion_percent'  => 20,
			'promotion_end_date' => '',
			'promotion_scope'    => 'studio',
			'wall_art_promotion_enabled'  => 0,
			'wall_art_promotion_percent'  => 20,
			'wall_art_promotion_end_date' => '',
			'order_retention_months' => 24,
			'personalisation_text_lines' => 5,
			'album_fine_art_label'  => 'Fine art album',
			'album_photo_label'     => 'Photo album',
			'box_wooden_label'   => 'Wooden Box',
			'box_wooden_price'   => 95,
			'box_fabric_label'   => 'Fabric Box',
			'box_fabric_price'   => 95,
			'enabled_cover_families' => array(),
			'enabled_cover_colours' => array(),
			'enabled_album_types' => array(),
			'enabled_boxes' => array(),
			'enabled_personalisation_methods' => array(),
			'enabled_personalisation_fonts' => array(),
			'enabled_design_collections' => array(),
			'enabled_personalisation_designs' => array(),
			'enabled_foil_colours' => array(),
		);
		foreach ( self::size_definitions() as $album ) {
			foreach ( $album['sizes'] as $size ) {
				$defaults[ $size['prefix'] . '_label' ] = $size['label'];
				$defaults[ $size['prefix'] . '_price' ] = $size['default_price'];
				$defaults[ $size['prefix'] . '_spread_price' ] = $size['default_spread_price'];
			}
		}
		$defaults['enabled_sizes'] = array();
		return $defaults;
	}

	/**
	 * Read settings merged with defaults so upgrades remain safe.
	 *
	 * @return array<string,mixed>
	 */
	private function settings() {
		if ( is_array( $this->preview_settings_override ) ) {
			return $this->preview_settings_override;
		}
		$saved = get_option( self::OPTION_KEY, array() );
		$settings = wp_parse_args( is_array( $saved ) ? $saved : array(), self::defaults() );

		// Refresh only the former built-in size labels. Photographer-edited labels stay untouched.
		foreach ( self::size_definitions() as $album ) {
			foreach ( $album['sizes'] as $size ) {
				$label_key = $size['prefix'] . '_label';
				if ( isset( $settings[ $label_key ], $size['legacy_label'] ) && $settings[ $label_key ] === $size['legacy_label'] ) {
					$settings[ $label_key ] = $size['label'];
				}
			}
		}

		// Translate built-in client labels, while leaving photographer-edited labels untouched.
		$built_in_labels = array(
			'album_fine_art_label' => array( 'Fine art album', __( 'Fine art album', 'mark-pacura-album-builder' ) ),
			'album_photo_label'    => array( 'Photo album', __( 'Photo album', 'mark-pacura-album-builder' ) ),
			'box_wooden_label'     => array( 'Wooden Box', __( 'Wooden Box', 'mark-pacura-album-builder' ) ),
			'box_fabric_label'     => array( 'Fabric Box', __( 'Fabric Box', 'mark-pacura-album-builder' ) ),
		);
		foreach ( $built_in_labels as $key => $labels ) {
			if ( isset( $settings[ $key ] ) && $labels[0] === $settings[ $key ] ) {
				$settings[ $key ] = $labels[1];
			}
		}

		return $settings;
	}

	/** Keep the three starter-offer profiles predictable. */
	private function normalise_starter_profile( $profile ) {
		$profile = sanitize_key( is_string( $profile ) ? $profile : '' );
		return in_array( $profile, array( 'wedding', 'family', 'general' ), true ) ? $profile : 'general';
	}

	/** Human-readable name for a starter-offer profile. */
	private function starter_profile_label( $profile ) {
		$labels = array(
			'wedding' => __( 'Wedding photography', 'mark-pacura-album-builder' ),
			'family'  => __( 'Family photography', 'mark-pacura-album-builder' ),
			'general' => __( 'General photography', 'mark-pacura-album-builder' ),
		);
		$profile = $this->normalise_starter_profile( $profile );
		return $labels[ $profile ];
	}

	/**
	 * Score public site copy locally. Nothing is sent to an outside service.
	 *
	 * @return array<string,mixed>
	 */
	private function detect_site_profile() {
		$keyword_sets = array(
			'wedding' => array(
				'wedding' => 6, 'weddings' => 6, 'bride' => 4, 'groom' => 4, 'ceremony' => 3, 'elopement' => 4, 'engagement' => 3, 'couples' => 2,
				'matrimonio' => 6, 'matrimoni' => 6, 'sposa' => 4, 'sposo' => 4, 'nozze' => 5, 'fidanzamento' => 3,
				'hochzeit' => 6, 'hochzeiten' => 6, 'braut' => 4, 'brautigam' => 4, 'verlobung' => 3,
				'brollop' => 6, 'brud' => 4, 'brudgum' => 4, 'forlovning' => 3,
				'boda' => 6, 'bodas' => 6, 'novia' => 4, 'novio' => 4, 'parejas' => 2, 'compromiso' => 3,
				'slub' => 6, 'wesele' => 6, 'panna mloda' => 5, 'pan mlody' => 5, 'narzeczenstwo' => 3,
			),
			'family' => array(
				'family' => 6, 'families' => 6, 'newborn' => 5, 'baby' => 4, 'babies' => 4, 'maternity' => 4, 'motherhood' => 3, 'children' => 3, 'kids' => 3,
				'famiglia' => 6, 'famiglie' => 6, 'neonato' => 5, 'neonati' => 5, 'bambini' => 3, 'maternita' => 4,
				'familie' => 6, 'familien' => 6, 'neugeboren' => 5, 'kinder' => 3, 'schwangerschaft' => 4,
				'familj' => 6, 'familjer' => 6, 'nyfodd' => 5, 'barn' => 3, 'gravid' => 4,
				'familia' => 6, 'familias' => 6, 'recien nacido' => 5, 'bebe' => 4, 'ninos' => 3, 'maternidad' => 4,
				'rodzina' => 6, 'rodzinne' => 6, 'noworodek' => 5, 'niemowle' => 4, 'dziecko' => 3, 'dzieci' => 3, 'ciaza' => 4,
			),
		);
		$segments = array();
		$identity = trim( wp_strip_all_tags( get_bloginfo( 'name' ) . ' ' . get_bloginfo( 'description' ) ) );
		if ( '' !== $identity ) {
			$segments[] = array( 'text' => $identity, 'weight' => 4 );
		}

		$front_id = absint( get_option( 'page_on_front' ) );
		if ( $front_id ) {
			$front = get_post( $front_id );
			if ( $front instanceof WP_Post && 'publish' === $front->post_status ) {
				$segments[] = array( 'text' => $front->post_title . ' ' . $front->post_excerpt . ' ' . wp_strip_all_tags( strip_shortcodes( $front->post_content ) ), 'weight' => 3 );
			}
		}

		$pages = get_posts(
			array(
				'post_type'      => 'page',
				'post_status'    => 'publish',
				'posts_per_page' => 25,
				'orderby'        => 'menu_order date',
				'order'          => 'ASC',
			)
		);
		foreach ( $pages as $page ) {
			if ( $front_id && (int) $page->ID === $front_id ) {
				continue;
			}
			$segments[] = array( 'text' => $page->post_title . ' ' . $page->post_excerpt . ' ' . wp_strip_all_tags( strip_shortcodes( substr( $page->post_content, 0, 5000 ) ) ), 'weight' => 1 );
		}

		$scores = array( 'wedding' => 0, 'family' => 0 );
		$matches = array( 'wedding' => array(), 'family' => array() );
		foreach ( $segments as $segment ) {
			$text = strtolower( remove_accents( html_entity_decode( (string) $segment['text'], ENT_QUOTES, get_bloginfo( 'charset' ) ) ) );
			foreach ( $keyword_sets as $profile => $keywords ) {
				foreach ( $keywords as $keyword => $value ) {
					$pattern = '/(?<![a-z0-9])' . preg_quote( $keyword, '/' ) . '(?![a-z0-9])/u';
					$count = preg_match_all( $pattern, $text );
					if ( $count ) {
						$scores[ $profile ] += min( 3, $count ) * $value * (int) $segment['weight'];
						$matches[ $profile ][ $keyword ] = isset( $matches[ $profile ][ $keyword ] ) ? $matches[ $profile ][ $keyword ] + $count : $count;
					}
				}
			}
		}

		$winner = $scores['wedding'] >= $scores['family'] ? 'wedding' : 'family';
		$other = 'wedding' === $winner ? 'family' : 'wedding';
		$profile = $scores[ $winner ] >= 12 && ( $scores[ $winner ] - $scores[ $other ] >= 4 || 0 === $scores[ $other ] ) ? $winner : 'general';
		$matched_words = 'general' === $profile ? array_values( array_unique( array_merge( array_keys( $matches['wedding'] ), array_keys( $matches['family'] ) ) ) ) : array_keys( $matches[ $profile ] );

		return array(
			'profile' => $profile,
			'label'   => $this->starter_profile_label( $profile ),
			'scores'  => $scores,
			'matches' => array_slice( $matched_words, 0, 6 ),
		);
	}

	/**
	 * Build a carefully limited starter offer while preserving prices and identity.
	 *
	 * @param string $profile Wedding, family or general.
	 * @param array  $base Existing settings to preserve.
	 * @return array<string,mixed>
	 */
	private function recommended_starter_settings( $profile, $base ) {
		$profile  = $this->normalise_starter_profile( $profile );
		$settings = wp_parse_args( is_array( $base ) ? $base : array(), self::defaults() );
		$settings['enabled_album_types'] = array( 'fine_art', 'photo' );
		$settings['enabled_sizes'] = array( 'fine_art_2020', 'fine_art_2525', 'fine_art_3030', 'photo_2020', 'photo_2525', 'photo_3030' );
		$settings['enabled_cover_families'] = array( 'DS', 'SV', 'CN' );

		$colour_profiles = array(
			'wedding' => array( 'DS-DS21', 'DS-DS24', 'DS-DS30', 'DS-DS31', 'DS-DS32', 'DS-DS35', 'SV-SV21', 'SV-SV22', 'SV-SV25', 'SV-SV26', 'SV-SV27', 'SV-SV29', 'CN-CN4', 'CN-CN5', 'CN-CN6', 'CN-CN7', 'CN-CN9', 'CN-CN10' ),
			'family'  => array( 'DS-DS21', 'DS-DS24', 'DS-DS25', 'DS-DS31', 'DS-DS33', 'DS-DS38', 'SV-SV21', 'SV-SV22', 'SV-SV24', 'SV-SV27', 'SV-SV29', 'CN-CN1', 'CN-CN2', 'CN-CN5', 'CN-CN6', 'CN-CN10', 'CN-CN11' ),
			'general' => array( 'DS-DS23', 'DS-DS24', 'DS-DS27', 'DS-DS30', 'DS-DS31', 'DS-DS32', 'SV-SV21', 'SV-SV24', 'SV-SV25', 'SV-SV26', 'SV-SV29', 'CN-CN3', 'CN-CN4', 'CN-CN5', 'CN-CN9', 'CN-CN10', 'CN-CN12' ),
		);
		$allowed_colours = array_map(
			function ( $colour ) {
				return $colour['colour_id'];
			},
			self::cover_source()['active_colours']
		);
		$settings['enabled_cover_colours'] = array_values( array_intersect( $allowed_colours, $colour_profiles[ $profile ] ) );
		$settings['enabled_personalisation_methods'] = array( 'foil', 'engraving' );
		$settings['enabled_boxes'] = array( 'wooden', 'fabric' );

		$personal = self::personalisation_source();
		$settings['enabled_personalisation_fonts'] = array_slice( array_map( function ( $font ) { return $font['id']; }, $personal['fonts'] ), 0, 8 );
		$settings['enabled_foil_colours'] = array_map( function ( $colour ) { return $colour['id']; }, $personal['foil_colours'] );

		$design_names = array(
			'wedding' => array( 'Interlocking Hearts', 'Leafy Branch', 'Three Patterned Hearts', 'Love', 'Our Story', 'Best Day Ever', 'Our Day', 'You + Me', 'Better Together', 'Story of Love', 'Just Married', 'Love Is Love', 'Mrs and Mrs', 'Him & Him', 'Our Big Day', 'A Perfect Match', 'This Love' ),
			'family'  => array( 'Crescent Moon and Stars', 'Camera with Leaves', 'Leafy Sprig', 'Baby', 'Boy', 'Girl', 'Happy Birthday', 'Be Wild', 'Baby Shoes', 'Rocking Horse', 'Teddy Bear', 'Pram', 'Filled Baby Footprints', 'Dummy', 'Birthday Cake', 'Stork Carrying Baby', 'Unicorn', 'Swaddled Baby', 'Outline Baby Footprints', 'Memories', 'Life', 'Family and Friends', 'Big Memories' ),
			'general' => array( 'Leafy Branch', 'Camera with Leaves', 'Leafy Sprig', 'Delicate Flower Stem', 'Love', 'Our Story', 'Better Together', 'For You', 'Love Is Love', 'Memories', 'Life', 'Good Times', 'Our Story' ),
		);
		$settings['enabled_design_collections'] = array();
		$settings['enabled_personalisation_designs'] = array();
		foreach ( $personal['design_items'] as $design ) {
			if ( empty( $design['selectable'] ) || 'collection.boudoir' === $design['collection_id'] || ! in_array( $design['display_name'], $design_names[ $profile ], true ) ) {
				continue;
			}
			$settings['enabled_personalisation_designs'][] = $design['source_reference'];
			$settings['enabled_design_collections'][] = $design['collection_id'];
		}
		$settings['enabled_design_collections'] = array_values( array_unique( $settings['enabled_design_collections'] ) );
		return $settings;
	}

	/** Apply the suggested selections without replacing photographer prices or email settings. */
	public function apply_starter_offer() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You are not allowed to change this offer.', 'mark-pacura-album-builder' ) );
		}
		check_admin_referer( 'mpab_apply_starter_offer' );
		$profile = $this->normalise_starter_profile( isset( $_POST['mpab_starter_profile'] ) ? wp_unslash( $_POST['mpab_starter_profile'] ) : 'general' );
		$current = get_option( self::OPTION_KEY, array() );
		$updated = $this->recommended_starter_settings( $profile, is_array( $current ) ? $current : array() );
		update_option( self::OPTION_KEY, $updated );
		$wall_current = get_option( MPAB_Wall_Art::OPTION_KEY, array() );
		update_option( MPAB_Wall_Art::OPTION_KEY, MPAB_Wall_Art::suggested_offer_settings( $wall_current ), false );
		update_option( 'mpab_last_starter_profile', $profile, false );
		wp_safe_redirect( add_query_arg( array( 'page' => 'mpab', 'starter-applied' => 1 ), admin_url( 'admin.php' ) ) );
		exit;
	}

	/**
	 * Render a logged-in, no-index preview in the active theme without creating a page.
	 */
	public function render_private_client_preview() {
		if ( empty( $_GET['mpab_client_preview'] ) ) {
			return;
		}
		$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
		if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) || ! wp_verify_nonce( $nonce, 'mpab_client_preview' ) ) {
			wp_die(
				esc_html__( 'The preview link has expired. Please generate a new preview link from Studio Home.', 'mark-pacura-album-builder' ),
				esc_html__( 'Private preview unavailable', 'mark-pacura-album-builder' ),
				array( 'response' => 403 )
			);
		}
		$profile = $this->normalise_starter_profile( isset( $_GET['starter_profile'] ) ? wp_unslash( $_GET['starter_profile'] ) : 'general' );
		$this->preview_settings_override = $this->recommended_starter_settings( $profile, $this->settings() );
		$wall_preview_settings = MPAB_Wall_Art::suggested_offer_settings( get_option( MPAB_Wall_Art::OPTION_KEY, array() ) );
		add_filter(
			'pre_option_' . MPAB_Wall_Art::OPTION_KEY,
			function () use ( $wall_preview_settings ) {
				return $wall_preview_settings;
			}
		);
		$preview = shortcode_exists( 'wb_studio' ) ? do_shortcode( '[wb_studio]' ) : do_shortcode( '[mp_album_builder]' );
		nocache_headers();
		header( 'X-Robots-Tag: noindex, nofollow, noarchive', true );
		get_header();
		?>
		<div class="mpab-private-preview-bar"><strong><?php esc_html_e( 'Private client preview', 'mark-pacura-album-builder' ); ?></strong><span><?php esc_html_e( 'Suggested Studio offer — nothing has been saved yet.', 'mark-pacura-album-builder' ); ?></span><a href="<?php echo esc_url( admin_url( 'admin.php?page=mpab' ) ); ?>"><?php esc_html_e( 'Back to Studio home', 'mark-pacura-album-builder' ); ?></a></div>
		<main class="mpab-private-preview-shell"><?php echo $preview; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></main>
		<style>.mpab-private-preview-bar{box-sizing:border-box;display:flex;gap:1rem;align-items:center;justify-content:center;flex-wrap:wrap;padding:12px 20px;background:#2f3325;color:#fff;font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.mpab-private-preview-bar span{opacity:.78}.mpab-private-preview-bar a{color:#fff;text-decoration:underline}.mpab-private-preview-shell{width:min(1600px,100%);margin:0 auto;padding:24px 0 64px}</style>
		<?php
		// Print queued footer scripts without rendering the active theme's footer
		// template. Some page builders store footer layouts as shortcodes; loading
		// that template inside this synthetic preview can expose those shortcodes as
		// visible text beneath the Studio.
		wp_footer();
		?>
		</body>
		</html>
		<?php
		exit;
	}

	/**
	 * Return the saved promotion together with its current live status.
	 *
	 * The end date is inclusive and follows the timezone configured in WordPress.
	 *
	 * @param array $settings Current settings.
	 * @return array<string,mixed>
	 */
	private function promotion( $settings, $category = 'albums' ) {
		$scope = isset( $settings['promotion_scope'] ) && 'separate' === $settings['promotion_scope'] ? 'separate' : 'studio';
		$prefix = 'separate' === $scope && 'wall-art' === $category ? 'wall_art_' : '';
		$enabled_key = $prefix . 'promotion_enabled';
		$percent_key = $prefix . 'promotion_percent';
		$end_date_key = $prefix . 'promotion_end_date';
		$enabled  = ! empty( $settings[ $enabled_key ] );
		$percent  = max( 1, min( 90, absint( isset( $settings[ $percent_key ] ) ? $settings[ $percent_key ] : 20 ) ) );
		$end_date = isset( $settings[ $end_date_key ] ) ? (string) $settings[ $end_date_key ] : '';
		$today    = current_datetime()->format( 'Y-m-d' );
		$expired  = $enabled && '' !== $end_date && $end_date < $today;
		$end_label = '';
		if ( '' !== $end_date ) {
			$date = date_create_immutable_from_format( '!Y-m-d', $end_date, wp_timezone() );
			if ( $date ) {
				$end_label = wp_date( get_option( 'date_format' ), $date->getTimestamp(), wp_timezone() );
			}
		}
		return array(
			'enabled'   => $enabled,
			'active'    => $enabled && ! $expired,
			'expired'   => $expired,
			'percent'   => $percent,
			'end_date'  => $end_date,
			'end_label' => $end_label,
			'category'  => $category,
			'scope'     => $scope,
		);
	}

	/**
	 * Return the live promotion that applies to one Studio category.
	 *
	 * @param string $category Either albums or wall-art.
	 * @return array<string,mixed>
	 */
	public function promotion_for( $category ) {
		return $this->promotion( $this->settings(), 'wall-art' === $category ? 'wall-art' : 'albums' );
	}

	/**
	 * Calculate a server-authoritative promotional total.
	 *
	 * @param float $normal_total Total before promotion.
	 * @param array $settings Current settings.
	 * @return array<string,mixed>
	 */
	private function promotion_totals( $normal_total, $settings, $category = 'albums' ) {
		$promotion = $this->promotion( $settings, $category );
		$normal_total = round( max( 0, (float) $normal_total ), 2 );
		$discount = $promotion['active'] ? round( $normal_total * ( (float) $promotion['percent'] / 100 ), 2 ) : 0;
		$promotion['normal_total'] = $normal_total;
		$promotion['discount'] = $discount;
		$promotion['total'] = round( max( 0, $normal_total - $discount ), 2 );
		return $promotion;
	}

	/**
	 * Calculate the authoritative total for one Studio category.
	 *
	 * @param float  $normal_total Total before promotion.
	 * @param string $category Either albums or wall-art.
	 * @return array<string,mixed>
	 */
	public function promotion_totals_for( $normal_total, $category ) {
		return $this->promotion_totals( $normal_total, $this->settings(), 'wall-art' === $category ? 'wall-art' : 'albums' );
	}

	/**
	 * Confirm that the saved offer contains one complete route a client can order.
	 *
	 * @param array $catalogue Allow-listed public offer.
	 * @return bool
	 */
	private function offer_is_viable( $catalogue ) {
		if ( empty( $catalogue['album_types'] ) || empty( $catalogue['cover_materials'] ) || empty( $catalogue['personalisation']['none'] ) ) {
			return false;
		}
		foreach ( array_keys( $catalogue['album_types'] ) as $album_id ) {
			if ( empty( $catalogue['sizes'][ $album_id ] ) ) {
				continue;
			}
			foreach ( array_keys( $catalogue['cover_materials'] ) as $material_id ) {
				if ( ! empty( $catalogue['covers'][ $material_id ] ) ) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Read the two-step email verification state. Changing the order address
	 * automatically starts the checks again.
	 *
	 * @param array $settings Current settings.
	 * @return array<string,mixed>
	 */
	private function email_test_state( $settings ) {
		$email = sanitize_email( isset( $settings['notification_email'] ) ? $settings['notification_email'] : '' );
		$defaults = array(
			'email'            => $email,
			'owner_sent'       => false,
			'owner_confirmed'  => false,
			'client_sent'      => false,
			'client_confirmed' => false,
			'failed'           => false,
			'error_code'       => '',
			'last_step'        => 'owner',
		);
		$saved = get_option( self::EMAIL_TEST_STATE, array() );
		$state = wp_parse_args( is_array( $saved ) ? $saved : array(), $defaults );
		if ( $email !== sanitize_email( $state['email'] ) ) {
			return $defaults;
		}
		return $state;
	}

	/**
	 * Whether both real inbox checks have been confirmed by the photographer.
	 *
	 * @param array $settings Current settings.
	 * @return bool
	 */
	private function email_setup_complete( $settings ) {
		$state = $this->email_test_state( $settings );
		return ! empty( $state['owner_confirmed'] ) && ! empty( $state['client_confirmed'] );
	}

	/**
	 * Return a sample order used by both test emails and their on-screen previews.
	 *
	 * @param array $settings Current settings.
	 * @return array<string,mixed>
	 */
	private function sample_email_order( $settings ) {
		$spreads   = 20;
		$box_price = (float) $settings['box_wooden_price'];
		$promotion = $this->promotion_totals( 380 + ( ( $spreads - 10 ) * 20 ) + $box_price, $settings );
		return array(
			'reference' => '1001',
			'configuration' => array(
				'album_type'       => array( 'id' => 'fine_art', 'label' => $settings['album_fine_art_label'] ),
				'size'             => array( 'id' => 'sample_2020', 'label' => '20x20 cm · 8x8 in' ),
				'spreads'          => $spreads,
				'pages'            => $spreads * 2,
				'cover_material'   => array( 'id' => 'ds', 'label' => 'Dolomiti Suede' ),
				'cover'            => array( 'id' => 'ds21', 'label' => 'DS21 · Blush' ),
				'personalisation'  => array( 'id' => 'foil', 'label' => 'Foil embossing' ),
				'personalisation_category' => array( 'id' => 'text', 'label' => 'Personalise with text' ),
				'personalisation_font' => array( 'id' => 'allura', 'label' => 'Allura' ),
				'personalisation_design' => array( 'id' => 'none', 'label' => 'Not applicable' ),
				'personal_text'    => "Alex & Sam\n18 June 2026",
				'foil_colour'      => array( 'id' => 'gold', 'label' => 'Gold' ),
				'box'              => array( 'id' => 'wooden', 'label' => $settings['box_wooden_label'] ),
				'box_colour'       => array( 'id' => 'ebony', 'label' => 'Ebony' ),
				'normal_total'     => $promotion['normal_total'],
				'promotion'        => array(
					'applied'   => $promotion['active'],
					'percent'   => $promotion['percent'],
					'discount'  => $promotion['discount'],
					'end_date'  => $promotion['end_date'],
					'end_label' => $promotion['end_label'],
				),
				'total'            => $promotion['total'],
				'currency_symbol'  => $this->currency_symbol( $settings['currency'] ),
				'order_intent'     => 'confirmed',
				'order_intent_at'  => current_time( 'mysql' ),
			),
		);
	}

	/**
	 * Identify consumer Gmail addresses so recovery instructions can be specific.
	 *
	 * @param string $email Email address.
	 * @return bool
	 */
	private function is_gmail_address( $email ) {
		$parts = explode( '@', strtolower( sanitize_email( $email ) ) );
		return 2 === count( $parts ) && in_array( $parts[1], array( 'gmail.com', 'googlemail.com' ), true );
	}

	/**
	 * Read the fixed full-brochure cover catalogue bundled with the plugin.
	 *
	 * @return array<string,array<int,array<string,mixed>>>
	 */
	private static function cover_source() {
		static $cache = null;
		if ( null === $cache ) {
			$path = MPAB_DIR . 'assets/data/cover-catalogue.json';
			$data = is_readable( $path ) ? json_decode( (string) file_get_contents( $path ), true ) : array();
			$catalogue = isset( $data['catalogue'] ) && is_array( $data['catalogue'] ) ? $data['catalogue'] : array();
			$cache = array(
				'cover_families' => isset( $catalogue['cover_families'] ) && is_array( $catalogue['cover_families'] ) ? $catalogue['cover_families'] : array(),
				'active_colours' => isset( $catalogue['active_colours'] ) && is_array( $catalogue['active_colours'] ) ? $catalogue['active_colours'] : array(),
			);
		}
		return $cache;
	}

	/**
	 * Read the fixed personalisation catalogue and approved visual registry.
	 *
	 * @return array<string,mixed>
	 */
	private static function personalisation_source() {
		static $cache = null;
		if ( null === $cache ) {
			$catalogue_path = MPAB_DIR . 'assets/data/personalisation-catalogue.json';
			$visuals_path   = MPAB_DIR . 'assets/data/personalisation-visuals.json';
			$catalogue      = is_readable( $catalogue_path ) ? json_decode( (string) file_get_contents( $catalogue_path ), true ) : array();
			$visuals        = is_readable( $visuals_path ) ? json_decode( (string) file_get_contents( $visuals_path ), true ) : array();
			$compatible = isset( $catalogue['visual_registry_version'], $visuals['registry_version'], $catalogue['contract_version'], $visuals['contract_version'] )
				&& hash_equals( (string) $catalogue['visual_registry_version'], (string) $visuals['registry_version'] )
				&& hash_equals( (string) $catalogue['contract_version'], (string) $visuals['contract_version'] );
			$cache = array(
				'methods'       => $compatible && isset( $catalogue['methods'] ) && is_array( $catalogue['methods'] ) ? $catalogue['methods'] : array(),
				'foil_colours'  => $compatible && isset( $catalogue['foil_colours'] ) && is_array( $catalogue['foil_colours'] ) ? $catalogue['foil_colours'] : array(),
				'design_items'  => $compatible && isset( $visuals['design_items'] ) && is_array( $visuals['design_items'] ) ? $visuals['design_items'] : array(),
				'fonts'         => $compatible && isset( $visuals['fonts'] ) && is_array( $visuals['fonts'] ) ? $visuals['fonts'] : array(),
			);
		}
		return $cache;
	}

	/**
	 * Return the bundled official image for a cover colour when it exists.
	 *
	 * @param string $official_code Wooden Banana cover code.
	 * @return string
	 */
	private function cover_image( $official_code ) {
		$file = strtolower( sanitize_file_name( $official_code ) ) . '.webp';
		return is_readable( MPAB_DIR . 'assets/images/swatches/' . $file ) ? MPAB_URL . 'assets/images/swatches/' . $file : '';
	}

	/**
	 * Convert a source personalisation asset path to its bundled plugin URL.
	 *
	 * @param string $source_path Source registry path.
	 * @param string $kind Designs, fonts or foil-colours.
	 * @return string
	 */
	private function personalisation_image( $source_path, $kind ) {
		$file = basename( (string) $source_path );
		if ( '' === $file || ! in_array( $kind, array( 'designs', 'fonts', 'foil-colours' ), true ) ) {
			return '';
		}
		return is_readable( MPAB_DIR . 'assets/images/personalisation/' . $kind . '/' . $file ) ? MPAB_URL . 'assets/images/personalisation/' . $kind . '/' . $file : '';
	}

	/**
	 * Return the display symbol for one supported customer currency.
	 *
	 * @param string $currency ISO-style currency code.
	 * @return string
	 */
	private function currency_symbol( $currency ) {
		$symbols = array(
			'GBP' => '£',
			'EUR' => '€',
			'USD' => '$',
		);
		return isset( $symbols[ $currency ] ) ? $symbols[ $currency ] : '£';
	}

	/**
	 * Infer whether client-facing messages should use I or We from WordPress site identity.
	 *
	 * @return string Either i or we.
	 */
	private function site_client_voice() {
		$title       = wp_strip_all_tags( wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) );
		$tagline     = wp_strip_all_tags( wp_specialchars_decode( get_bloginfo( 'description' ), ENT_QUOTES ) );
		$identity    = strtolower( remove_accents( trim( $title . ' ' . $tagline ) ) );
		$title_plain = strtolower( remove_accents( $title ) );
		$voice       = 'i';

		if ( preg_match( '/\b(we|our|ours|us)\b/', $identity ) ) {
			$voice = 'we';
		} elseif ( preg_match( '/\b(i|my|mine|me)\b/', $identity ) ) {
			$voice = 'i';
		} elseif ( preg_match( '/\b(studio|studios|team|collective|company|agency|associates|partners|photographers|group|limited|ltd|llc)\b/', $identity ) || false !== strpos( $title_plain, '&' ) ) {
			$voice = 'we';
		}

		return (string) apply_filters( 'mpab_site_client_voice', $voice, $title, $tagline );
	}

	/**
	 * Return the complete active album-size catalogue used by the Studio form.
	 *
	 * @return array<string,array<string,mixed>>
	 */
	private static function size_definitions() {
		return array(
			'fine_art' => array(
				'product_code' => 'FA',
				'label_setting' => 'album_fine_art_label',
				'sizes' => array(
					array( 'id' => 'fine_art_1515', 'prefix' => 'size_fa_1515', 'code' => '1515', 'label' => '15x15 cm · 6x6 in', 'legacy_label' => 'Fine art album · 15 × 15 cm', 'default_price' => 290, 'default_spread_price' => 10, 'thumb' => 'size-15x15' ),
					array( 'id' => 'fine_art_2020', 'prefix' => 'size_fa_2020', 'code' => '2020', 'label' => '20x20 cm · 8x8 in', 'legacy_label' => 'Fine art album · 20 × 20 cm', 'default_price' => 380, 'default_spread_price' => 20, 'thumb' => 'size-20x20' ),
					array( 'id' => 'fine_art_2525', 'prefix' => 'size_small', 'code' => '2525', 'label' => '25x25 cm · 10x10 in', 'legacy_label' => 'Fine art album · 25 × 25 cm', 'default_price' => 490, 'default_spread_price' => 20, 'thumb' => 'size-25x25' ),
					array( 'id' => 'fine_art_3030', 'prefix' => 'size_large', 'code' => '3030', 'label' => '30x30 cm · 12x12 in', 'legacy_label' => 'Fine art album · 30 × 30 cm', 'default_price' => 550, 'default_spread_price' => 20, 'thumb' => 'size-30x30' ),
					array( 'id' => 'fine_art_2030', 'prefix' => 'size_fa_2030', 'code' => '2030', 'label' => '20x30 cm · 8x12 in', 'legacy_label' => 'Fine art album · 20 × 30 cm', 'default_price' => 490, 'default_spread_price' => 20, 'thumb' => 'size-20x30' ),
					array( 'id' => 'fine_art_3020', 'prefix' => 'size_fa_3020', 'code' => '3020', 'label' => '30x20 cm · 12x8 in', 'legacy_label' => 'Fine art album · 30 × 20 cm', 'default_price' => 490, 'default_spread_price' => 20, 'thumb' => 'size-30x20' ),
					array( 'id' => 'fine_art_3825', 'prefix' => 'size_fa_3825', 'code' => '3825', 'label' => '38x25 cm · 15x10 in', 'legacy_label' => 'Fine art album · 38 × 25 cm', 'default_price' => 580, 'default_spread_price' => 40, 'thumb' => 'size-38x25' ),
				),
			),
			'photo' => array(
				'product_code' => 'PA',
				'label_setting' => 'album_photo_label',
				'sizes' => array(
					array( 'id' => 'photo_1515', 'prefix' => 'size_pa_1515', 'code' => '1515', 'label' => '15x15 cm · 6x6 in', 'legacy_label' => 'Photo album · 15 × 15 cm', 'default_price' => 240, 'default_spread_price' => 10, 'thumb' => 'size-15x15' ),
					array( 'id' => 'photo_2020', 'prefix' => 'size_pa_2020', 'code' => '2020', 'label' => '20x20 cm · 8x8 in', 'legacy_label' => 'Photo album · 20 × 20 cm', 'default_price' => 340, 'default_spread_price' => 10, 'thumb' => 'size-20x20' ),
					array( 'id' => 'photo_2525', 'prefix' => 'size_photo_small', 'code' => '2525', 'label' => '25x25 cm · 10x10 in', 'legacy_label' => 'Photo album · 25 × 25 cm', 'default_price' => 450, 'default_spread_price' => 20, 'thumb' => 'size-25x25' ),
					array( 'id' => 'photo_3030', 'prefix' => 'size_photo_large', 'code' => '3030', 'label' => '30x30 cm · 12x12 in', 'legacy_label' => 'Photo album · 30 × 30 cm', 'default_price' => 490, 'default_spread_price' => 30, 'thumb' => 'size-30x30' ),
					array( 'id' => 'photo_2030', 'prefix' => 'size_pa_2030', 'code' => '2030', 'label' => '20x30 cm · 8x12 in', 'legacy_label' => 'Photo album · 20 × 30 cm', 'default_price' => 450, 'default_spread_price' => 20, 'thumb' => 'size-20x30' ),
					array( 'id' => 'photo_3020', 'prefix' => 'size_pa_3020', 'code' => '3020', 'label' => '30x20 cm · 12x8 in', 'legacy_label' => 'Photo album · 30 × 20 cm', 'default_price' => 450, 'default_spread_price' => 20, 'thumb' => 'size-30x20' ),
					array( 'id' => 'photo_3825', 'prefix' => 'size_pa_3825', 'code' => '3825', 'label' => '38x25 cm · 15x10 in', 'legacy_label' => 'Photo album · 38 × 25 cm', 'default_price' => 530, 'default_spread_price' => 30, 'thumb' => 'size-38x25' ),
				),
			),
		);
	}

	/**
	 * Pinned Wooden Banana prices effective 15 October 2025 for all active sizes.
	 *
	 * These are read-only comparison values. Photographer client prices remain
	 * explicit settings and are never calculated from these values in WordPress.
	 *
	 * @return array<string,array<string,array<string,float>>>
	 */
	private static function wb_prices() {
		return array(
			'size_fa_1515' => array(
				'GBP' => array( 'album' => 89, 'spread' => 4.20 ), 'EUR' => array( 'album' => 92.40, 'spread' => 4.25 ), 'USD' => array( 'album' => 107.80, 'spread' => 4.96 ),
			),
			'size_fa_2020' => array(
				'GBP' => array( 'album' => 120, 'spread' => 5.40 ), 'EUR' => array( 'album' => 126, 'spread' => 5.67 ), 'USD' => array( 'album' => 147, 'spread' => 6.61 ),
			),
			'size_small' => array(
				'GBP' => array( 'album' => 156, 'spread' => 7.10 ),
				'EUR' => array( 'album' => 163.20, 'spread' => 7.32 ),
				'USD' => array( 'album' => 190.40, 'spread' => 8.54 ),
			),
			'size_fa_2030' => array(
				'GBP' => array( 'album' => 156, 'spread' => 7.10 ), 'EUR' => array( 'album' => 163.20, 'spread' => 7.32 ), 'USD' => array( 'album' => 190.40, 'spread' => 8.54 ),
			),
			'size_large' => array(
				'GBP' => array( 'album' => 175, 'spread' => 8.10 ), 'EUR' => array( 'album' => 182, 'spread' => 8.52 ), 'USD' => array( 'album' => 212.80, 'spread' => 9.94 ),
			),
			'size_fa_3020' => array(
				'GBP' => array( 'album' => 156, 'spread' => 7.10 ), 'EUR' => array( 'album' => 163.20, 'spread' => 7.32 ), 'USD' => array( 'album' => 190.40, 'spread' => 8.54 ),
			),
			'size_fa_3825' => array(
				'GBP' => array( 'album' => 188, 'spread' => 12 ), 'EUR' => array( 'album' => 196.80, 'spread' => 12.72 ), 'USD' => array( 'album' => 229.60, 'spread' => 14.84 ),
			),
			'size_pa_1515' => array(
				'GBP' => array( 'album' => 74, 'spread' => 3.50 ), 'EUR' => array( 'album' => 78, 'spread' => 3.54 ), 'USD' => array( 'album' => 91, 'spread' => 4.13 ),
			),
			'size_pa_2020' => array(
				'GBP' => array( 'album' => 106, 'spread' => 4.50 ), 'EUR' => array( 'album' => 111.60, 'spread' => 4.68 ), 'USD' => array( 'album' => 130.20, 'spread' => 5.46 ),
			),
			'size_photo_small' => array(
				'GBP' => array( 'album' => 142, 'spread' => 6.30 ),
				'EUR' => array( 'album' => 148.80, 'spread' => 6.48 ),
				'USD' => array( 'album' => 173.60, 'spread' => 7.56 ),
			),
			'size_pa_2030' => array(
				'GBP' => array( 'album' => 142, 'spread' => 6.30 ), 'EUR' => array( 'album' => 148.80, 'spread' => 6.48 ), 'USD' => array( 'album' => 173.60, 'spread' => 7.56 ),
			),
			'size_photo_large' => array(
				'GBP' => array( 'album' => 156, 'spread' => 8.40 ), 'EUR' => array( 'album' => 162, 'spread' => 8.76 ), 'USD' => array( 'album' => 184, 'spread' => 10.22 ),
			),
			'size_pa_3020' => array(
				'GBP' => array( 'album' => 142, 'spread' => 6.30 ), 'EUR' => array( 'album' => 148.80, 'spread' => 6.48 ), 'USD' => array( 'album' => 173.60, 'spread' => 7.56 ),
			),
			'size_pa_3825' => array(
				'GBP' => array( 'album' => 170, 'spread' => 10.90 ), 'EUR' => array( 'album' => 176.40, 'spread' => 11.33 ), 'USD' => array( 'album' => 205.80, 'spread' => 13.22 ),
			),
		);
	}

	/**
	 * Return the read-only WB presentation-box reference price.
	 *
	 * @param string $currency Selected currency.
	 * @return float
	 */
	private static function wb_box_price( $currency ) {
		$prices = array( 'GBP' => 40, 'EUR' => 48, 'USD' => 56 );
		return isset( $prices[ $currency ] ) ? $prices[ $currency ] : $prices['GBP'];
	}

	/**
	 * Register a private record type for saved album requests.
	 *
	 * @return void
	 */
	public function register_submission_type() {
		register_post_type(
			self::POST_TYPE,
			array(
				'labels' => array(
					'name'          => __( 'Previous orders', 'mark-pacura-album-builder' ),
					'singular_name' => __( 'Album order', 'mark-pacura-album-builder' ),
					'edit_item'     => __( 'View album order', 'mark-pacura-album-builder' ),
					'all_items'     => __( 'Previous orders', 'mark-pacura-album-builder' ),
					'menu_name'     => __( 'Previous orders', 'mark-pacura-album-builder' ),
				),
				'public'              => false,
				'publicly_queryable'  => false,
				'exclude_from_search' => true,
				'show_ui'             => true,
				'show_in_menu'        => 'mpab',
				'show_in_rest'        => false,
				'supports'            => array( 'title' ),
				'capability_type'     => array( 'mpab_order', 'mpab_orders' ),
				'map_meta_cap'        => false,
				'capabilities'        => array(
					'edit_post'              => self::ORDER_CAPABILITY,
					'read_post'              => self::ORDER_CAPABILITY,
					'delete_post'            => self::ORDER_CAPABILITY,
					'edit_posts'             => self::ORDER_CAPABILITY,
					'edit_others_posts'      => self::ORDER_CAPABILITY,
					'delete_posts'           => self::ORDER_CAPABILITY,
					'publish_posts'          => self::ORDER_CAPABILITY,
					'read_private_posts'     => self::ORDER_CAPABILITY,
					'delete_private_posts'   => self::ORDER_CAPABILITY,
					'delete_published_posts' => self::ORDER_CAPABILITY,
					'delete_others_posts'    => self::ORDER_CAPABILITY,
					'edit_private_posts'     => self::ORDER_CAPABILITY,
					'edit_published_posts'   => self::ORDER_CAPABILITY,
					'create_posts'           => 'do_not_allow',
				),
			)
		);
	}

	/**
	 * Add the small configuration area.
	 *
	 * @return void
	 */
	public function register_admin_menu() {
		add_menu_page(
			__( 'Wooden Banana Studio', 'mark-pacura-album-builder' ),
			__( 'Studio', 'mark-pacura-album-builder' ),
			'manage_options',
			'mpab',
			array( $this, 'render_studio_home' ),
			'dashicons-album',
			56
		);

		add_submenu_page(
			'mpab',
			__( 'Studio home', 'mark-pacura-album-builder' ),
			__( 'Studio home', 'mark-pacura-album-builder' ),
			'manage_options',
			'mpab',
			array( $this, 'render_studio_home' )
		);

		add_submenu_page(
			'mpab',
			__( 'Album setup', 'mark-pacura-album-builder' ),
			__( 'Albums', 'mark-pacura-album-builder' ),
			'manage_options',
			'mpab-albums',
			array( $this, 'render_settings_page' )
		);
	}

	/** Render a calm route into the separate album and wall-art setup journeys. */
	public function render_studio_home() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		update_option( self::SETUP_SEEN, 1, false );
		$settings       = $this->settings();
		$email_ready    = $this->email_setup_complete( $settings );
		$album_types    = count( (array) $settings['enabled_album_types'] );
		$album_sizes    = count( (array) $settings['enabled_sizes'] );
		$album_ready    = $album_types > 0 && $album_sizes > 0 && ! empty( $settings['enabled_cover_colours'] );
		$wall_settings  = get_option( 'mpab_wall_art_settings', array() );
		$wall_settings  = is_array( $wall_settings ) ? $wall_settings : array();
		$wall_products  = count( isset( $wall_settings['product_enabled'] ) ? (array) $wall_settings['product_enabled'] : array() );
		$wall_enabled   = ! empty( $wall_settings['enabled'] ) && $wall_products > 0;
		$wall_ready     = $wall_enabled && ! empty( $wall_settings['setup_complete'] );
		$studio_ready   = $email_ready && ( $album_ready || $wall_ready );
		$album_counts   = wp_count_posts( self::POST_TYPE );
		$wall_counts    = post_type_exists( 'mpab_wall_order' ) ? wp_count_posts( 'mpab_wall_order' ) : null;
		$album_orders   = $album_counts ? (int) $album_counts->private : 0;
		$wall_orders    = $wall_counts ? (int) $wall_counts->private : 0;
		$studio_code    = '[wb_studio]';
		$site_profile   = $this->detect_site_profile();
		$starter_profile = $site_profile['profile'];
		$starter_preview_url = add_query_arg(
			array(
				'mpab_client_preview' => 1,
				'starter_profile'     => $starter_profile,
				'_wpnonce'            => wp_create_nonce( 'mpab_client_preview' ),
			),
			home_url( '/' )
		);
		?>
		<div class="wrap mpab-admin-shell mpab-studio-home">
			<h1 class="screen-reader-text"><?php esc_html_e( 'Wooden Banana Studio', 'mark-pacura-album-builder' ); ?></h1>
			<?php self::render_studio_hero(); ?>
			<?php if ( isset( $_GET['mpab_studio_saved'] ) && '1' === sanitize_key( wp_unslash( $_GET['mpab_studio_saved'] ) ) ) : ?>
				<div class="mpab-admin-feedback mpab-admin-feedback--success" role="status"><p><strong><?php esc_html_e( 'Studio settings saved.', 'mark-pacura-album-builder' ); ?></strong> <?php esc_html_e( 'Albums and Wall Art now use these shared settings.', 'mark-pacura-album-builder' ); ?></p></div>
			<?php endif; ?>
			<?php if ( isset( $_GET['mpab_promotions_saved'] ) && '1' === sanitize_key( wp_unslash( $_GET['mpab_promotions_saved'] ) ) ) : ?>
				<div class="mpab-admin-feedback mpab-admin-feedback--success" role="status"><p><strong><?php esc_html_e( 'Promotions saved.', 'mark-pacura-album-builder' ); ?></strong> <?php esc_html_e( 'Updated promotional pricing is now active for Albums and Wall Art.', 'mark-pacura-album-builder' ); ?></p></div>
			<?php endif; ?>
			<?php if ( isset( $_GET['starter-applied'] ) && '1' === sanitize_key( wp_unslash( $_GET['starter-applied'] ) ) ) : ?>
				<div class="mpab-admin-feedback mpab-admin-feedback--success" role="status"><p><strong><?php esc_html_e( 'Suggested Studio offer confirmed.', 'mark-pacura-album-builder' ); ?></strong> <?php esc_html_e( 'Album and Wall Art setups are complete. Open either section anytime to update settings.', 'mark-pacura-album-builder' ); ?></p></div>
			<?php endif; ?>
			<?php if ( isset( $_GET['mpab_albums_saved'] ) && '1' === sanitize_key( wp_unslash( $_GET['mpab_albums_saved'] ) ) ) : ?>
				<div class="mpab-admin-feedback mpab-admin-feedback--success" role="status"><p><strong><?php esc_html_e( 'Your offer has been saved.', 'mark-pacura-album-builder' ); ?></strong> <?php esc_html_e( 'The album ordering tool on your website is now up to date.', 'mark-pacura-album-builder' ); ?></p></div>
			<?php endif; ?>
			<?php if ( isset( $_GET['mpab_wall_art_saved'] ) && '1' === sanitize_key( wp_unslash( $_GET['mpab_wall_art_saved'] ) ) ) : ?>
				<div class="mpab-admin-feedback mpab-admin-feedback--success" role="status"><p><strong><?php esc_html_e( 'Your wall art offer has been saved.', 'mark-pacura-album-builder' ); ?></strong></p></div>
			<?php endif; ?>

			<section class="mpab-studio-code-card mpab-studio-code-card--top <?php echo $studio_ready ? 'is-ready' : 'is-blocked'; ?>" aria-labelledby="mpab-studio-code-title">
				<div class="mpab-studio-code-card__copy"><span class="mpab-section-kicker"><?php echo esc_html( $studio_ready ? __( 'Ready to add to your website', 'mark-pacura-album-builder' ) : __( 'Finish your setup first', 'mark-pacura-album-builder' ) ); ?></span><h2 id="mpab-studio-code-title"><?php esc_html_e( 'Your Studio page code', 'mark-pacura-album-builder' ); ?></h2><p><?php echo esc_html( $studio_ready ? __( 'Paste this code block into your page editor to display active Studio products.', 'mark-pacura-album-builder' ) : __( 'Verify notification emails and configure at least one product offer to proceed.', 'mark-pacura-album-builder' ) ); ?></p></div>
				<div class="mpab-studio-code-card__code"><code><?php echo esc_html( $studio_code ); ?></code><small><?php echo esc_html( $studio_ready ? __( 'One code for Albums and Wall Art.', 'mark-pacura-album-builder' ) : __( 'Your embed code will generate once email testing and at least one product line are configured.', 'mark-pacura-album-builder' ) ); ?></small></div>
			</section>

			<section class="mpab-studio-group mpab-studio-group--main" aria-labelledby="mpab-main-settings-title">
				<header class="mpab-studio-group__header"><span class="mpab-studio-group__number">01</span><div><span class="mpab-section-kicker"><?php esc_html_e( 'Shared across your Studio', 'mark-pacura-album-builder' ); ?></span><h2 id="mpab-main-settings-title"><?php esc_html_e( 'Main settings', 'mark-pacura-album-builder' ); ?></h2><p><?php esc_html_e( 'Configure global order settings. Select any section to update preferences.', 'mark-pacura-album-builder' ); ?></p></div></header>
				<div class="mpab-studio-accordion-list">
					<details class="mpab-studio-accordion">
						<summary><span class="mpab-studio-accordion__copy"><strong><?php esc_html_e( 'Order emails', 'mark-pacura-album-builder' ); ?></strong><small><?php esc_html_e( 'Email verification covers both Album and Wall Art notifications.', 'mark-pacura-album-builder' ); ?></small></span><span class="mpab-studio-status <?php echo $email_ready ? 'is-ready' : 'is-needed'; ?>"><?php echo esc_html( $email_ready ? __( 'Ready', 'mark-pacura-album-builder' ) : __( 'To do', 'mark-pacura-album-builder' ) ); ?></span></summary>
						<div class="mpab-studio-accordion__panel"><p><?php esc_html_e( 'Specify your order notification inbox and verify message delivery settings.', 'mark-pacura-album-builder' ); ?></p><a class="button button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=mpab-albums&mpab_email_setup=1#mpab-email-setup' ) ); ?>"><?php echo esc_html( $email_ready ? __( 'Review email setup', 'mark-pacura-album-builder' ) : __( 'Set up emails', 'mark-pacura-album-builder' ) ); ?></a></div>
					</details>
					<details class="mpab-studio-accordion">
						<summary><span class="mpab-studio-accordion__copy"><strong><?php esc_html_e( 'Studio settings', 'mark-pacura-album-builder' ); ?></strong><small><?php esc_html_e( 'Set store currency, brand accent colour, and order retention policies.', 'mark-pacura-album-builder' ); ?></small></span><span class="mpab-studio-status is-ready"><?php esc_html_e( 'Ready', 'mark-pacura-album-builder' ); ?></span></summary>
						<div class="mpab-studio-accordion__panel"><form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post">
					<input type="hidden" name="action" value="mpab_save_studio_settings">
					<?php wp_nonce_field( 'mpab_save_studio_settings' ); ?>
					<div class="mpab-studio-settings__grid">
						<div class="mpab-setting-card">
							<div class="mpab-setting-card__copy"><label for="mpab-studio-currency"><?php esc_html_e( 'Customer currency', 'mark-pacura-album-builder' ); ?></label><p><?php esc_html_e( 'Select your trading currency. Reference costs will update automatically without changing your retail prices.', 'mark-pacura-album-builder' ); ?></p></div>
							<div class="mpab-setting-card__control"><select id="mpab-studio-currency" name="mpab_studio[currency]">
								<option value="GBP" <?php selected( $settings['currency'], 'GBP' ); ?>><?php esc_html_e( 'GBP · £', 'mark-pacura-album-builder' ); ?></option>
								<option value="EUR" <?php selected( $settings['currency'], 'EUR' ); ?>><?php esc_html_e( 'EUR · €', 'mark-pacura-album-builder' ); ?></option>
								<option value="USD" <?php selected( $settings['currency'], 'USD' ); ?>><?php esc_html_e( 'USD · $', 'mark-pacura-album-builder' ); ?></option>
							</select></div>
						</div>
						<div class="mpab-setting-card mpab-setting-card--accent">
							<div class="mpab-setting-card__copy"><label for="mpab-studio-accent-colour"><?php esc_html_e( 'Accent colour', 'mark-pacura-album-builder' ); ?></label><p><?php esc_html_e( 'Choose an accent color for interactive elements. Typography and styling adapt automatically to your website theme.', 'mark-pacura-album-builder' ); ?></p></div>
							<label class="mpab-colour-control" for="mpab-studio-accent-colour"><input id="mpab-studio-accent-colour" type="color" name="mpab_studio[accent_colour]" value="<?php echo esc_attr( $settings['accent_colour'] ); ?>" data-mpab-accent-colour><span data-mpab-accent-value><?php echo esc_html( strtoupper( $settings['accent_colour'] ) ); ?></span></label>
						</div>
						<div class="mpab-setting-card">
							<div class="mpab-setting-card__copy"><label for="mpab-studio-order-retention"><?php esc_html_e( 'How long should orders be kept?', 'mark-pacura-album-builder' ); ?></label><p><?php esc_html_e( 'Order records older than this threshold are automatically deleted. 24 months is recommended for record-keeping and data privacy compliance.', 'mark-pacura-album-builder' ); ?></p></div>
							<div class="mpab-setting-card__control"><select id="mpab-studio-order-retention" name="mpab_studio[order_retention_months]">
								<?php foreach ( array( 3, 6, 12, 24, 36 ) as $retention_months ) : ?>
								<option value="<?php echo esc_attr( $retention_months ); ?>" <?php selected( (int) $settings['order_retention_months'], $retention_months ); ?>><?php echo esc_html( sprintf( _n( '%d month', '%d months', $retention_months, 'mark-pacura-album-builder' ), $retention_months ) ); ?></option>
								<?php endforeach; ?>
							</select></div>
						</div>
					</div>
					<?php submit_button( __( 'Save Studio settings', 'mark-pacura-album-builder' ), 'primary', 'submit', false ); ?>
						</form></div>
					</details>
					<details class="mpab-studio-accordion">
						<summary><span class="mpab-studio-accordion__copy"><strong><?php esc_html_e( 'Promotions', 'mark-pacura-album-builder' ); ?></strong><small><?php esc_html_e( 'Configure studio-wide or product-specific promotional discounts.', 'mark-pacura-album-builder' ); ?></small></span><span class="mpab-studio-status is-optional"><?php esc_html_e( 'Optional', 'mark-pacura-album-builder' ); ?></span></summary>
						<div class="mpab-studio-accordion__panel mpab-studio-accordion__panel--promotions"><?php $this->render_studio_promotions( $settings ); ?></div>
					</details>
				</div>
			</section>

			<section class="mpab-studio-group mpab-studio-group--offers" aria-labelledby="mpab-offer-settings-title">
				<header class="mpab-studio-group__header"><span class="mpab-studio-group__number">02</span><div><span class="mpab-section-kicker"><?php esc_html_e( 'What clients can order', 'mark-pacura-album-builder' ); ?></span><h2 id="mpab-offer-settings-title"><?php esc_html_e( 'Offer settings', 'mark-pacura-album-builder' ); ?></h2><p><?php esc_html_e( 'Enable Albums, Wall Art, or both. At least one product range must be active before publishing.', 'mark-pacura-album-builder' ); ?></p></div></header>
				<div class="mpab-studio-suggested-offer">
					<div class="mpab-studio-suggested-offer__copy"><span class="mpab-section-kicker"><?php esc_html_e( 'An easy place to start', 'mark-pacura-album-builder' ); ?></span><h3><?php esc_html_e( 'Your suggested Studio offer', 'mark-pacura-album-builder' ); ?></h3><p><?php esc_html_e( 'A recommended offer has been generated for your studio. Confirm below to activate, or customize product lines manually.', 'mark-pacura-album-builder' ); ?></p></div>
					<div class="mpab-studio-suggested-offer__actions"><form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post"><input type="hidden" name="action" value="mpab_apply_starter_offer"><input type="hidden" name="mpab_starter_profile" value="<?php echo esc_attr( $starter_profile ); ?>"><?php wp_nonce_field( 'mpab_apply_starter_offer' ); ?><button type="submit" class="button button-primary mpab-studio-suggested-confirm" data-mpab-starter-apply data-confirm="<?php echo esc_attr__( 'This action will apply recommended product selections to your Studio. Existing pricing, email, and general settings will remain intact. Proceed?', 'mark-pacura-album-builder' ); ?>"><?php esc_html_e( 'Confirm suggested offer', 'mark-pacura-album-builder' ); ?></button></form><a class="button mpab-studio-suggested-preview" href="<?php echo esc_url( $starter_preview_url ); ?>" target="_blank" rel="noopener"><?php esc_html_e( 'Preview suggested offer as a client', 'mark-pacura-album-builder' ); ?><span aria-hidden="true">&#8599;</span></a></div>
				</div>
				<div class="mpab-studio-offer-grid">
					<article class="mpab-studio-home-card mpab-studio-home-card--product">
						<div class="mpab-studio-home-card__top"><span class="mpab-section-kicker"><?php esc_html_e( 'Albums', 'mark-pacura-album-builder' ); ?></span><span class="mpab-studio-status <?php echo $album_ready ? 'is-ready' : 'is-needed'; ?>"><?php echo esc_html( $album_ready ? __( 'Ready', 'mark-pacura-album-builder' ) : __( 'To do', 'mark-pacura-album-builder' ) ); ?></span></div>
						<div class="mpab-studio-home-card__visual" aria-hidden="true"><img src="<?php echo esc_url( MPAB_URL . 'assets/images/wall-art/studio-entry/albums.webp' ); ?>" alt=""></div><h3><?php esc_html_e( 'Your albums', 'mark-pacura-album-builder' ); ?></h3><p><?php echo esc_html( sprintf( __( '%1$d album types and %2$d sizes selected. Open the setup to choose or change your offer.', 'mark-pacura-album-builder' ), $album_types, $album_sizes ) ); ?></p><a class="button mpab-studio-product-action" href="<?php echo esc_url( admin_url( 'admin.php?page=mpab-albums' ) ); ?>"><?php esc_html_e( 'Open album setup', 'mark-pacura-album-builder' ); ?><span aria-hidden="true">&#8594;</span></a>
					</article>
					<article class="mpab-studio-home-card mpab-studio-home-card--product">
						<div class="mpab-studio-home-card__top"><span class="mpab-section-kicker"><?php esc_html_e( 'Wall art', 'mark-pacura-album-builder' ); ?></span><span class="mpab-studio-status <?php echo $wall_ready ? 'is-ready' : 'is-needed'; ?>"><?php echo esc_html( $wall_ready ? __( 'Ready', 'mark-pacura-album-builder' ) : __( 'To do', 'mark-pacura-album-builder' ) ); ?></span></div>
						<div class="mpab-studio-home-card__visual" aria-hidden="true"><img src="<?php echo esc_url( MPAB_URL . 'assets/images/wall-art/studio-entry/wall-art.webp' ); ?>" alt=""></div><h3><?php esc_html_e( 'Your wall art', 'mark-pacura-album-builder' ); ?></h3><p><?php echo esc_html( $wall_ready ? sprintf( _n( '%d wall-art product is available to clients.', '%d wall-art products are available to clients.', $wall_products, 'mark-pacura-album-builder' ), $wall_products ) : __( 'Open the setup to choose products, sizes and client prices.', 'mark-pacura-album-builder' ) ); ?></p><a class="button mpab-studio-product-action" href="<?php echo esc_url( admin_url( 'admin.php?page=mpab-wall-art' ) ); ?>"><?php echo esc_html( $wall_ready ? __( 'Open wall-art setup', 'mark-pacura-album-builder' ) : __( 'Set up wall art', 'mark-pacura-album-builder' ) ); ?><span aria-hidden="true">&#8594;</span></a>
					</article>
				</div>
			</section>

			<details class="mpab-studio-orders-drawer"><summary><span><strong><?php esc_html_e( 'Orders', 'mark-pacura-album-builder' ); ?></strong><small><?php esc_html_e( 'Open your saved Album and Wall Art orders.', 'mark-pacura-album-builder' ); ?></small></span><span class="mpab-studio-orders-drawer__count"><?php echo esc_html( $album_orders + $wall_orders ); ?></span></summary><div class="mpab-studio-orders-card__links"><a href="<?php echo esc_url( admin_url( 'edit.php?post_type=' . self::POST_TYPE ) ); ?>"><strong><?php echo esc_html( $album_orders ); ?></strong><span><?php esc_html_e( 'Album orders', 'mark-pacura-album-builder' ); ?></span></a><a href="<?php echo esc_url( admin_url( 'edit.php?post_type=mpab_wall_order' ) ); ?>"><strong><?php echo esc_html( $wall_orders ); ?></strong><span><?php esc_html_e( 'Wall-art orders', 'mark-pacura-album-builder' ); ?></span></a></div></details>
		</div>
		<?php
	}

	/**
	 * Add an obvious route to setup from the Plugins screen.
	 *
	 * @param array<int,string> $links Existing plugin action links.
	 * @return array<int,string>
	 */
	public function plugin_action_links( $links ) {
		$setup_link = sprintf(
			'<a href="%1$s"><strong>%2$s</strong></a>',
			esc_url( admin_url( 'admin.php?page=mpab' ) ),
			esc_html__( 'Open Studio', 'mark-pacura-album-builder' )
		);
		array_unshift( $links, $setup_link );
		return $links;
	}

	/**
	 * Point administrators to the setup screen until it has been opened once.
	 *
	 * @return void
	 */
	public function render_setup_notice() {
		if ( ! current_user_can( 'manage_options' ) || get_option( self::SETUP_SEEN, false ) ) {
			return;
		}

		$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
		if ( in_array( $page, array( 'mpab', 'mpab-albums', 'mpab-wall-art' ), true ) ) {
			return;
		}
		?>
		<div class="notice notice-info">
			<p><strong><?php esc_html_e( 'Welcome to Wooden Banana Studio.', 'mark-pacura-album-builder' ); ?></strong> <?php esc_html_e( 'Configure your album catalog, add wall art options, and embed Studio on your website.', 'mark-pacura-album-builder' ); ?></p>
			<p><a class="button button-primary" href="<?php echo esc_url( admin_url( 'admin.php?page=mpab' ) ); ?>"><?php esc_html_e( 'Start setting up', 'mark-pacura-album-builder' ); ?></a></p>
		</div>
		<?php
	}

	/**
	 * Load the small copy-to-clipboard helper only on the plugin setup screen.
	 *
	 * @param string $hook_suffix Current admin screen identifier.
	 * @return void
	 */
	public function enqueue_admin_assets( $hook_suffix ) {
		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		$is_submission_screen = $screen && self::POST_TYPE === $screen->post_type;
		$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
		$is_plugin_page = in_array( $page, array( 'mpab', 'mpab-albums' ), true );
		if ( ! $is_plugin_page && ! $is_submission_screen ) {
			return;
		}
		wp_enqueue_style( 'mpab-thumbnails', MPAB_URL . 'assets/css/thumbnails.css', array(), MPAB_VERSION );
		wp_enqueue_style( 'mpab-admin', MPAB_URL . 'assets/css/admin.css', array( 'mpab-thumbnails' ), MPAB_VERSION );
		if ( in_array( $page, array( 'mpab', 'mpab-albums' ), true ) ) {
			wp_enqueue_script( 'mpab-admin', MPAB_URL . 'assets/js/admin.js', array(), MPAB_VERSION, true );
			wp_localize_script( 'mpab-admin', 'MPAB_ADMIN_DATA', array( 'messages' => $this->admin_script_messages() ) );
		}
	}

	/** Text used by the interactive dashboard controls. */
	private function admin_script_messages() {
		return array(
			'copied'                    => __( 'Copied!', 'mark-pacura-album-builder' ),
			'copyShortcode'             => __( 'Copy page code', 'mark-pacura-album-builder' ),
			'yourStudio'                => __( 'Your studio', 'mark-pacura-album-builder' ),
			'yourEmail'                 => __( 'your@email.com', 'mark-pacura-album-builder' ),
			'promotionOffStatus'        => __( 'Not currently showing', 'mark-pacura-album-builder' ),
			'promotionOffSummary'       => __( 'Clients are seeing your usual prices.', 'mark-pacura-album-builder' ),
			'promotionOffNote'          => __( 'Enable this promotion when you are ready to publish a special offer.', 'mark-pacura-album-builder' ),
			'promotionEndedStatus'      => __( 'This promotion has ended', 'mark-pacura-album-builder' ),
			'promotionEndedSummary'     => __( 'The %d%% promotional discount is currently hidden from clients.', 'mark-pacura-album-builder' ),
			'promotionEndedNote'        => __( 'Set a new expiration date or remove the deadline to reactivate this promotion.', 'mark-pacura-album-builder' ),
			'promotionLiveStatus'       => __( 'Live on your client form', 'mark-pacura-album-builder' ),
			'promotionLiveUntil'        => __( '%1$d%% off until %2$s', 'mark-pacura-album-builder' ),
			'promotionLiveOpen'         => __( '%d%% off until you switch it off', 'mark-pacura-album-builder' ),
			'promotionLiveNote'         => __( 'The discount applies automatically at checkout and is recorded on order details.', 'mark-pacura-album-builder' ),
			'albumPromotion'            => __( 'Album promotion', 'mark-pacura-album-builder' ),
			'studioPromotion'           => __( 'Studio promotion', 'mark-pacura-album-builder' ),
			'selectedCount'             => __( '%1$d of %2$d selected', 'mark-pacura-album-builder' ),
			'edit'                      => __( 'Edit', 'mark-pacura-album-builder' ),
			'albumsAndSizes'            => __( 'Albums and sizes', 'mark-pacura-album-builder' ),
			'albumSizeCount'            => __( '%1$d album %2$s · %3$d sizes', 'mark-pacura-album-builder' ),
			'typeSingular'              => __( 'type', 'mark-pacura-album-builder' ),
			'typePlural'                => __( 'types', 'mark-pacura-album-builder' ),
			'albumCheckNote'            => __( 'These are the choices your clients can start with.', 'mark-pacura-album-builder' ),
			'coversAndPersonalisation'  => __( 'Covers and personalisation', 'mark-pacura-album-builder' ),
			'coverMethodCount'          => __( '%1$d cover colours · %2$d personalisation options', 'mark-pacura-album-builder' ),
			'coverCheckNote'            => __( 'Only compatible options will be displayed as clients customize their album.', 'mark-pacura-album-builder' ),
			'presentation'              => __( 'Presentation', 'mark-pacura-album-builder' ),
			'boxCount'                  => __( '%1$d box %2$s', 'mark-pacura-album-builder' ),
			'optionSingular'            => __( 'option', 'mark-pacura-album-builder' ),
			'optionPlural'              => __( 'options', 'mark-pacura-album-builder' ),
			'noBoxes'                   => __( 'No presentation boxes', 'mark-pacura-album-builder' ),
			'boxesShown'                => __( 'Clients may select a presentation box before completing their order.', 'mark-pacura-album-builder' ),
			'boxesHidden'               => __( 'The presentation box selection step remains hidden from clients.', 'mark-pacura-album-builder' ),
			'pricing'                   => __( 'Pricing', 'mark-pacura-album-builder' ),
			'pricingCount'              => __( '%1$d selected sizes · %2$s', 'mark-pacura-album-builder' ),
			'pricingNote'               => __( 'Clients will view the calculated order total on the final review step.', 'mark-pacura-album-builder' ),
			'promotion'                 => __( 'Promotion', 'mark-pacura-album-builder' ),
			'promotionOn'               => __( 'Promotion switched on', 'mark-pacura-album-builder' ),
			'noPromotion'               => __( 'No promotion', 'mark-pacura-album-builder' ),
			'promotionCheckOn'          => __( 'The promotional discount will be reflected in the final order total.', 'mark-pacura-album-builder' ),
			'promotionCheckOff'         => __( 'Standard pricing is displayed to clients.', 'mark-pacura-album-builder' ),
		);
	}

	/**
	 * Register the single options array.
	 *
	 * @return void
	 */
	public function register_settings() {
		register_setting(
			'mpab_settings_group',
			self::OPTION_KEY,
			array(
				'type'              => 'array',
				'sanitize_callback' => array( $this, 'sanitize_settings' ),
				'default'           => self::defaults(),
			)
		);
	}

	/** Save only the settings shared by Albums and Wall Art without touching either offer. */
	public function save_studio_settings() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You cannot change these Studio settings.', 'mark-pacura-album-builder' ) );
		}
		check_admin_referer( 'mpab_save_studio_settings' );

		$input = isset( $_POST['mpab_studio'] ) && is_array( $_POST['mpab_studio'] ) ? wp_unslash( $_POST['mpab_studio'] ) : array();
		$settings = $this->settings();

		$currency = isset( $input['currency'] ) ? strtoupper( sanitize_key( $input['currency'] ) ) : $settings['currency'];
		$settings['currency'] = in_array( $currency, array( 'GBP', 'EUR', 'USD' ), true ) ? $currency : 'GBP';

		$accent = sanitize_hex_color( isset( $input['accent_colour'] ) ? $input['accent_colour'] : '' );
		$settings['accent_colour'] = $accent ? $accent : self::defaults()['accent_colour'];

		$retention = absint( isset( $input['order_retention_months'] ) ? $input['order_retention_months'] : $settings['order_retention_months'] );
		$settings['order_retention_months'] = in_array( $retention, array( 3, 6, 12, 24, 36 ), true ) ? $retention : 24;

		update_option( self::OPTION_KEY, $settings, false );
		self::ensure_retention_schedule();
		wp_safe_redirect( add_query_arg( array( 'page' => 'mpab', 'mpab_studio_saved' => 1 ), admin_url( 'admin.php' ) ) );
		exit;
	}

	/** Save the Studio promotion controls without touching either product offer. */
	public function save_studio_promotions() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You cannot change these Studio promotions.', 'mark-pacura-album-builder' ) );
		}
		check_admin_referer( 'mpab_save_studio_promotions' );

		$input = isset( $_POST['mpab_promotions'] ) && is_array( $_POST['mpab_promotions'] ) ? wp_unslash( $_POST['mpab_promotions'] ) : array();
		$settings = $this->settings();
		$settings['promotion_scope'] = isset( $input['promotion_scope'] ) && 'separate' === sanitize_key( $input['promotion_scope'] ) ? 'separate' : 'studio';
		$settings['promotion_enabled'] = ! empty( $input['promotion_enabled'] ) ? 1 : 0;
		$settings['promotion_percent'] = max( 1, min( 90, absint( isset( $input['promotion_percent'] ) ? $input['promotion_percent'] : 20 ) ) );
		$settings['wall_art_promotion_enabled'] = ! empty( $input['wall_art_promotion_enabled'] ) ? 1 : 0;
		$settings['wall_art_promotion_percent'] = max( 1, min( 90, absint( isset( $input['wall_art_promotion_percent'] ) ? $input['wall_art_promotion_percent'] : 20 ) ) );

		foreach ( array( 'promotion_end_date', 'wall_art_promotion_end_date' ) as $date_key ) {
			$value = sanitize_text_field( isset( $input[ $date_key ] ) ? $input[ $date_key ] : '' );
			$settings[ $date_key ] = '';
			if ( preg_match( '/^(\d{4})-(\d{2})-(\d{2})$/', $value, $date_parts ) && checkdate( (int) $date_parts[2], (int) $date_parts[3], (int) $date_parts[1] ) ) {
				$settings[ $date_key ] = $value;
			}
		}

		update_option( self::OPTION_KEY, $settings, false );
		wp_safe_redirect( add_query_arg( array( 'page' => 'mpab', 'mpab_promotions_saved' => 1 ), admin_url( 'admin.php' ) ) . '#mpab-promotions' );
		exit;
	}

	/** Render the one promotion area shared by Album and Wall Art setup. */
	private function render_studio_promotions( $settings ) {
		$album_promotion = $this->promotion( $settings, 'albums' );
		$wall_settings = $settings;
		$wall_settings['promotion_scope'] = 'separate';
		$wall_promotion = $this->promotion( $wall_settings, 'wall-art' );
		?>
		<form class="mpab-promotion-manager mpab-studio-promotions" id="mpab-promotions" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" data-mpab-promotion-manager>
			<input type="hidden" name="action" value="mpab_save_studio_promotions">
			<?php wp_nonce_field( 'mpab_save_studio_promotions' ); ?>
			<header><span class="mpab-section-kicker"><?php esc_html_e( 'Promotions', 'mark-pacura-album-builder' ); ?></span><h2><?php esc_html_e( 'Add an offer when you need one', 'mark-pacura-album-builder' ); ?></h2><p><?php esc_html_e( 'Apply a single promotional discount studio-wide, or manage Albums and Wall Art promotions separately.', 'mark-pacura-album-builder' ); ?></p></header>
			<div class="mpab-promotion-scope" role="radiogroup" aria-label="<?php esc_attr_e( 'Promotion applies to', 'mark-pacura-album-builder' ); ?>">
				<label><input type="radio" name="mpab_promotions[promotion_scope]" value="studio" data-mpab-promotion-scope <?php checked( $settings['promotion_scope'], 'studio' ); ?>><span><strong><?php esc_html_e( 'One Studio promotion', 'mark-pacura-album-builder' ); ?></strong><small><?php esc_html_e( 'The promotional discount applies equally to Albums and Wall Art.', 'mark-pacura-album-builder' ); ?></small></span></label>
				<label><input type="radio" name="mpab_promotions[promotion_scope]" value="separate" data-mpab-promotion-scope <?php checked( $settings['promotion_scope'], 'separate' ); ?>><span><strong><?php esc_html_e( 'Separate promotions', 'mark-pacura-album-builder' ); ?></strong><small><?php esc_html_e( 'Manage promotional offers separately for Albums and Wall Art.', 'mark-pacura-album-builder' ); ?></small></span></label>
			</div>
			<div class="mpab-promotion-groups">
				<?php foreach ( array( 'albums' => $album_promotion, 'wall-art' => $wall_promotion ) as $promotion_category => $category_promotion ) : $is_wall = 'wall-art' === $promotion_category; ?>
				<section class="mpab-promotion-card" data-mpab-promotion data-promotion-category="<?php echo esc_attr( $promotion_category ); ?>" data-mpab-today="<?php echo esc_attr( current_datetime()->format( 'Y-m-d' ) ); ?>" <?php echo $is_wall && 'studio' === $settings['promotion_scope'] ? 'hidden' : ''; ?>>
					<header class="mpab-promotion-card__header"><div><h3><?php echo esc_html( $is_wall ? __( 'Wall Art promotion', 'mark-pacura-album-builder' ) : ( 'studio' === $settings['promotion_scope'] ? __( 'Studio promotion', 'mark-pacura-album-builder' ) : __( 'Album promotion', 'mark-pacura-album-builder' ) ) ); ?></h3><p><?php echo esc_html( $is_wall ? __( 'Applies to all Wall Art products and quantities.', 'mark-pacura-album-builder' ) : __( 'Applies to base album pricing, additional spreads, and presentation boxes.', 'mark-pacura-album-builder' ) ); ?></p></div><label class="mpab-promotion-switch"><input type="checkbox" name="mpab_promotions[<?php echo $is_wall ? 'wall_art_' : ''; ?>promotion_enabled]" value="1" data-mpab-promotion-toggle <?php checked( $category_promotion['enabled'] ); ?>><span><strong><?php esc_html_e( 'Run this promotion', 'mark-pacura-album-builder' ); ?></strong><small><?php esc_html_e( 'Switch it off whenever you like.', 'mark-pacura-album-builder' ); ?></small></span></label></header>
					<div class="mpab-promotion-card__body" data-mpab-promotion-body <?php echo $category_promotion['enabled'] ? '' : 'hidden'; ?>><div class="mpab-promotion-controls" data-mpab-promotion-controls><label><span><?php esc_html_e( 'Client saving', 'mark-pacura-album-builder' ); ?></span><span class="mpab-promotion-percent"><input type="number" min="1" max="90" step="1" name="mpab_promotions[<?php echo $is_wall ? 'wall_art_' : ''; ?>promotion_percent]" value="<?php echo esc_attr( $category_promotion['percent'] ); ?>" data-mpab-promotion-percent><b>% off</b></span></label><label><span><?php esc_html_e( 'Offer ends', 'mark-pacura-album-builder' ); ?> <em><?php esc_html_e( 'Optional', 'mark-pacura-album-builder' ); ?></em></span><input type="date" min="<?php echo esc_attr( current_datetime()->format( 'Y-m-d' ) ); ?>" name="mpab_promotions[<?php echo $is_wall ? 'wall_art_' : ''; ?>promotion_end_date]" value="<?php echo esc_attr( $category_promotion['end_date'] ); ?>" data-mpab-promotion-end-date></label></div><aside class="mpab-promotion-preview <?php echo $category_promotion['active'] ? 'is-live' : ( $category_promotion['expired'] ? 'is-expired' : '' ); ?>" data-mpab-promotion-preview><span data-mpab-promotion-status></span><strong data-mpab-promotion-summary></strong><p data-mpab-promotion-note></p></aside></div>
				</section>
				<?php endforeach; ?>
			</div>
			<button class="button button-primary" type="submit"><?php esc_html_e( 'Save promotions', 'mark-pacura-album-builder' ); ?></button>
		</form>
		<?php
	}

	/**
	 * Sanitize photographer-controlled labels and prices.
	 *
	 * @param mixed $input Raw settings input.
	 * @return array<string,mixed>
	 */
	public function sanitize_settings( $input ) {
		$input    = is_array( $input ) ? $input : array();
		$defaults = self::defaults();
		$output   = $defaults;

		$output['notification_email'] = sanitize_email( isset( $input['notification_email'] ) ? $input['notification_email'] : $defaults['notification_email'] );
		if ( ! is_email( $output['notification_email'] ) ) {
			$output['notification_email'] = get_option( 'admin_email', '' );
		}
		$output['email_sender_name'] = $this->truncate( sanitize_text_field( isset( $input['email_sender_name'] ) ? $input['email_sender_name'] : $defaults['email_sender_name'] ), 120 );
		if ( '' === $output['email_sender_name'] ) {
			$output['email_sender_name'] = $defaults['email_sender_name'];
		}
		$currency = isset( $input['currency'] ) ? strtoupper( sanitize_key( $input['currency'] ) ) : $defaults['currency'];
		$output['currency']        = in_array( $currency, array( 'GBP', 'EUR', 'USD' ), true ) ? $currency : $defaults['currency'];
		$output['show_prices']     = 1;
		$output['accent_colour']   = sanitize_hex_color( isset( $input['accent_colour'] ) ? $input['accent_colour'] : '' );
		if ( ! $output['accent_colour'] ) {
			$output['accent_colour'] = $defaults['accent_colour'];
		}
		$output['promotion_enabled'] = ! empty( $input['promotion_enabled'] ) ? 1 : 0;
		$output['promotion_percent'] = max( 1, min( 90, absint( isset( $input['promotion_percent'] ) ? $input['promotion_percent'] : $defaults['promotion_percent'] ) ) );
		$output['promotion_scope'] = isset( $input['promotion_scope'] ) && 'separate' === sanitize_key( $input['promotion_scope'] ) ? 'separate' : 'studio';
		$promotion_end_date = sanitize_text_field( isset( $input['promotion_end_date'] ) ? $input['promotion_end_date'] : '' );
		$output['promotion_end_date'] = '';
		if ( preg_match( '/^(\d{4})-(\d{2})-(\d{2})$/', $promotion_end_date, $date_parts ) && checkdate( (int) $date_parts[2], (int) $date_parts[3], (int) $date_parts[1] ) ) {
			$output['promotion_end_date'] = $promotion_end_date;
		}
		$output['wall_art_promotion_enabled'] = ! empty( $input['wall_art_promotion_enabled'] ) ? 1 : 0;
		$output['wall_art_promotion_percent'] = max( 1, min( 90, absint( isset( $input['wall_art_promotion_percent'] ) ? $input['wall_art_promotion_percent'] : $defaults['wall_art_promotion_percent'] ) ) );
		$wall_art_promotion_end_date = sanitize_text_field( isset( $input['wall_art_promotion_end_date'] ) ? $input['wall_art_promotion_end_date'] : '' );
		$output['wall_art_promotion_end_date'] = '';
		if ( preg_match( '/^(\d{4})-(\d{2})-(\d{2})$/', $wall_art_promotion_end_date, $date_parts ) && checkdate( (int) $date_parts[2], (int) $date_parts[3], (int) $date_parts[1] ) ) {
			$output['wall_art_promotion_end_date'] = $wall_art_promotion_end_date;
		}
		$retention_months = absint( isset( $input['order_retention_months'] ) ? $input['order_retention_months'] : $defaults['order_retention_months'] );
		$output['order_retention_months'] = in_array( $retention_months, array( 3, 6, 12, 24, 36 ), true ) ? $retention_months : $defaults['order_retention_months'];
		$output['personalisation_text_lines'] = max( 1, min( 5, absint( isset( $input['personalisation_text_lines'] ) ? $input['personalisation_text_lines'] : $defaults['personalisation_text_lines'] ) ) );

		$labels = array(
			'album_fine_art_label',
			'album_photo_label',
			'box_wooden_label',
			'box_fabric_label',
		);
		foreach ( self::size_definitions() as $album ) {
			foreach ( $album['sizes'] as $size ) {
				$labels[] = $size['prefix'] . '_label';
			}
		}
		foreach ( $labels as $key ) {
			$output[ $key ] = $this->truncate( sanitize_text_field( isset( $input[ $key ] ) ? $input[ $key ] : $defaults[ $key ] ), 120 );
		}

		$prices = array(
			'box_wooden_price',
			'box_fabric_price',
		);
		foreach ( self::size_definitions() as $album ) {
			foreach ( $album['sizes'] as $size ) {
				$prices[] = $size['prefix'] . '_price';
				$prices[] = $size['prefix'] . '_spread_price';
			}
		}
		foreach ( $prices as $key ) {
			$value          = isset( $input[ $key ] ) ? (float) $input[ $key ] : (float) $defaults[ $key ];
			$output[ $key ] = max( 0, min( 100000, round( $value, 2 ) ) );
		}

		$requested_album_types = isset( $input['enabled_album_types'] ) && is_array( $input['enabled_album_types'] ) ? array_map( 'sanitize_key', $input['enabled_album_types'] ) : array();
		$output['enabled_album_types'] = array_values( array_intersect( array( 'fine_art', 'photo' ), $requested_album_types ) );
		$requested_boxes = isset( $input['enabled_boxes'] ) && is_array( $input['enabled_boxes'] ) ? array_map( 'sanitize_key', $input['enabled_boxes'] ) : array();
		$output['enabled_boxes'] = array_values( array_intersect( array( 'wooden', 'fabric' ), $requested_boxes ) );
		$requested_sizes = isset( $input['enabled_sizes'] ) && is_array( $input['enabled_sizes'] ) ? array_map( 'sanitize_key', $input['enabled_sizes'] ) : array();
		$output['enabled_sizes'] = array();
		foreach ( self::size_definitions() as $album_id => $album ) {
			if ( ! in_array( $album_id, $output['enabled_album_types'], true ) ) {
				continue;
			}
			$album_size_ids = array_map(
				function ( $size ) {
					return $size['id'];
				},
				$album['sizes']
			);
			$selected_album_sizes = array_values( array_intersect( $album_size_ids, $requested_sizes ) );
			$output['enabled_sizes'] = array_merge( $output['enabled_sizes'], $selected_album_sizes );
		}

		$cover_source = self::cover_source();
		$allowed_families = array();
		$family_for_colour = array();
		foreach ( $cover_source['cover_families'] as $family ) {
			$allowed_families[] = $family['family_code'];
		}
		foreach ( $cover_source['active_colours'] as $colour ) {
			$family_for_colour[ $colour['colour_id'] ] = $colour['family_code'];
		}
		$requested_families = isset( $input['enabled_cover_families'] ) && is_array( $input['enabled_cover_families'] ) ? array_map( 'sanitize_text_field', $input['enabled_cover_families'] ) : array();
		$output['enabled_cover_families'] = array_values( array_intersect( $allowed_families, $requested_families ) );

		$requested_colours = isset( $input['enabled_cover_colours'] ) && is_array( $input['enabled_cover_colours'] ) ? array_map( 'sanitize_text_field', $input['enabled_cover_colours'] ) : array();
		$output['enabled_cover_colours'] = array();
		foreach ( $requested_colours as $colour_id ) {
			if ( isset( $family_for_colour[ $colour_id ] ) && in_array( $family_for_colour[ $colour_id ], $output['enabled_cover_families'], true ) ) {
				$output['enabled_cover_colours'][] = $colour_id;
			}
		}
		$requested_methods = isset( $input['enabled_personalisation_methods'] ) && is_array( $input['enabled_personalisation_methods'] ) ? array_map( 'sanitize_key', $input['enabled_personalisation_methods'] ) : array();
		$output['enabled_personalisation_methods'] = array_values( array_intersect( array( 'foil', 'engraving' ), $requested_methods ) );

		$personal = self::personalisation_source();
		$allowed_fonts = array_map(
			function ( $font ) {
				return $font['id'];
			},
			$personal['fonts']
		);
		$allowed_designs = array();
		$allowed_design_collections = array();
		$collection_for_design = array();
		foreach ( $personal['design_items'] as $design ) {
			if ( ! empty( $design['selectable'] ) ) {
				$allowed_designs[] = $design['source_reference'];
				$allowed_design_collections[] = $design['collection_id'];
				$collection_for_design[ $design['source_reference'] ] = $design['collection_id'];
			}
		}
		$allowed_design_collections = array_values( array_unique( $allowed_design_collections ) );
		$allowed_foil_colours = array_map(
			function ( $colour ) {
				return $colour['id'];
			},
			$personal['foil_colours']
		);
		$requested_fonts = isset( $input['enabled_personalisation_fonts'] ) && is_array( $input['enabled_personalisation_fonts'] ) ? array_map( 'sanitize_text_field', $input['enabled_personalisation_fonts'] ) : array();
		$requested_design_collections = isset( $input['enabled_design_collections'] ) && is_array( $input['enabled_design_collections'] ) ? array_map( 'sanitize_text_field', $input['enabled_design_collections'] ) : array();
		$requested_designs = isset( $input['enabled_personalisation_designs'] ) && is_array( $input['enabled_personalisation_designs'] ) ? array_map( 'sanitize_text_field', $input['enabled_personalisation_designs'] ) : array();
		$requested_foil_colours = isset( $input['enabled_foil_colours'] ) && is_array( $input['enabled_foil_colours'] ) ? array_map( 'sanitize_text_field', $input['enabled_foil_colours'] ) : array();
		$output['enabled_personalisation_fonts'] = array_values( array_intersect( $allowed_fonts, $requested_fonts ) );
		$output['enabled_design_collections'] = array_values( array_intersect( $allowed_design_collections, $requested_design_collections ) );
		$output['enabled_personalisation_designs'] = array();
		foreach ( array_intersect( $allowed_designs, $requested_designs ) as $design_reference ) {
			if ( isset( $collection_for_design[ $design_reference ] ) && in_array( $collection_for_design[ $design_reference ], $output['enabled_design_collections'], true ) ) {
				$output['enabled_personalisation_designs'][] = $design_reference;
			}
		}
		$output['enabled_foil_colours'] = array_values( array_intersect( $allowed_foil_colours, $requested_foil_colours ) );
		return $output;
	}

	/**
	 * Truncate UTF-8 text without requiring the optional mbstring extension.
	 *
	 * @param string $value Text to truncate.
	 * @param int    $length Maximum character length.
	 * @return string
	 */
	private function truncate( $value, $length ) {
		if ( function_exists( 'mb_substr' ) ) {
			return mb_substr( $value, 0, $length );
		}
		return substr( $value, 0, $length );
	}

	/**
	 * Count characters with a safe fallback when mbstring is unavailable.
	 *
	 * @param string $value Text to measure.
	 * @return int
	 */
	private function text_length( $value ) {
		if ( function_exists( 'mb_strlen' ) ) {
			return mb_strlen( $value );
		}
		return strlen( $value );
	}

	/**
	 * Render settings and embedding instructions.
	 *
	 * @return void
	 */
	public function render_settings_page() {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		update_option( self::SETUP_SEEN, 1, false );
		$settings = $this->settings();
		$email_state = $this->email_test_state( $settings );
		$email_ready = $this->email_setup_complete( $settings );
		$email_only = isset( $_GET['mpab_email_setup'] ) && '1' === sanitize_key( wp_unslash( $_GET['mpab_email_setup'] ) );
		$sample_order = $this->sample_email_order( $settings );
		$preview_step = ! empty( $email_state['client_sent'] ) && empty( $email_state['client_confirmed'] ) ? 'client' : 'owner';
		$preview_html = 'client' === $preview_step
			? $this->client_email_html( $sample_order['reference'], 'Alex & Sam', $sample_order['configuration'], $settings )
			: $this->owner_email_html( $sample_order['reference'], 'Alex & Sam', 'alex@example.com', 'Please call before sending the album to production.', $sample_order['configuration'], $settings, admin_url( 'edit.php?post_type=' . self::POST_TYPE ) );
		$status_class = ! empty( $email_state['failed'] ) ? 'failed' : ( $email_ready ? 'ready' : ( ! empty( $email_state['owner_sent'] ) || ! empty( $email_state['owner_confirmed'] ) ? 'testing' : 'untested' ) );
		$status_label = ! empty( $email_state['failed'] ) ? __( 'Email problem', 'mark-pacura-album-builder' ) : ( $email_ready ? __( 'Email ready', 'mark-pacura-album-builder' ) : ( 'testing' === $status_class ? __( 'Sending test email...', 'mark-pacura-album-builder' ) : __( 'Not tested', 'mark-pacura-album-builder' ) ) );
		?>
		<div class="wrap mpab-admin-shell">
			<h1 class="screen-reader-text"><?php echo esc_html( $email_only ? __( 'Studio email setup', 'mark-pacura-album-builder' ) : __( 'Album Studio', 'mark-pacura-album-builder' ) ); ?></h1>
			<a class="mpab-admin-back" href="<?php echo esc_url( admin_url( 'admin.php?page=mpab' ) ); ?>">&larr; <?php esc_html_e( 'Back to Studio home', 'mark-pacura-album-builder' ); ?></a>
			<?php self::render_studio_hero(); ?>
			<?php if ( isset( $_GET['mpab_order_removed'] ) && '1' === sanitize_key( wp_unslash( $_GET['mpab_order_removed'] ) ) ) : ?>
				<div class="mpab-admin-feedback mpab-admin-feedback--success" role="status"><p><strong><?php esc_html_e( 'Order removed.', 'mark-pacura-album-builder' ); ?></strong> <?php esc_html_e( 'It has been moved to Trash and can be recovered if needed.', 'mark-pacura-album-builder' ); ?></p></div>
			<?php endif; ?>
			<?php if ( isset( $_GET['settings-updated'] ) && 'true' === sanitize_key( wp_unslash( $_GET['settings-updated'] ) ) ) : ?>
				<div class="mpab-admin-feedback mpab-admin-feedback--success" role="status" data-mpab-offer-saved><p><strong><?php esc_html_e( 'Your offer has been saved.', 'mark-pacura-album-builder' ); ?></strong> <?php esc_html_e( 'The album ordering tool on your website is now up to date.', 'mark-pacura-album-builder' ); ?></p></div>
			<?php endif; ?>

			<?php if ( ! $email_only ) : ?>
			<nav class="mpab-setup-guide" data-mpab-guided-setup aria-label="<?php esc_attr_e( 'Album Studio setup', 'mark-pacura-album-builder' ); ?>">
				<div class="mpab-setup-guide__intro"><span class="mpab-section-kicker"><?php esc_html_e( 'Your guided setup', 'mark-pacura-album-builder' ); ?></span><h2><?php esc_html_e( 'Album setup', 'mark-pacura-album-builder' ); ?></h2><p><?php esc_html_e( 'Complete each step at your own pace. All album configuration options are available here.', 'mark-pacura-album-builder' ); ?></p></div>
				<ol>
					<li><button type="button" data-mpab-setup-step="2" aria-current="step"><span>1</span><strong><?php esc_html_e( 'Your albums', 'mark-pacura-album-builder' ); ?></strong><small><?php esc_html_e( 'Choose the offer', 'mark-pacura-album-builder' ); ?></small></button></li>
					<li><button type="button" data-mpab-setup-step="3"><span>2</span><strong><?php esc_html_e( 'Pricing', 'mark-pacura-album-builder' ); ?></strong><small><?php esc_html_e( 'Set your prices', 'mark-pacura-album-builder' ); ?></small></button></li>
					<li><button type="button" data-mpab-setup-step="4"><span>3</span><strong><?php esc_html_e( 'Check', 'mark-pacura-album-builder' ); ?></strong><small><?php esc_html_e( 'Review everything', 'mark-pacura-album-builder' ); ?></small></button></li>
				</ol>
			</nav>
			<?php endif; ?>

			<?php if ( $email_only ) : ?>
			<section class="mpab-email-setup" id="mpab-email-setup">
				<header class="mpab-email-setup__header">
					<div>
						<span class="mpab-section-kicker"><?php esc_html_e( 'Email notifications', 'mark-pacura-album-builder' ); ?></span>
						<h2><?php esc_html_e( 'Ensure you receive every order notification', 'mark-pacura-album-builder' ); ?></h2>
						<p><?php esc_html_e( 'Run a quick email test to confirm message delivery. Once confirmed, your full Studio setup will unlock.', 'mark-pacura-album-builder' ); ?></p>
					</div>
					<span class="mpab-email-status mpab-email-status--<?php echo esc_attr( $status_class ); ?>"><?php echo esc_html( $status_label ); ?></span>
				</header>
				<div class="mpab-email-setup__body">
					<?php if ( ! empty( $email_state['failed'] ) ) : ?>
						<div class="mpab-email-problem">
							<span class="mpab-email-step-label"><?php esc_html_e( 'Email check paused', 'mark-pacura-album-builder' ); ?></span>
							<h3><?php esc_html_e( 'The test email was not received. Follow the steps below to resolve delivery.', 'mark-pacura-album-builder' ); ?></h3>
							<p><?php echo esc_html( 'send_failed' === $email_state['error_code'] ? __( 'Your website was unable to send the email.', 'mark-pacura-album-builder' ) : __( 'The test email was dispatched but did not arrive in your inbox.', 'mark-pacura-album-builder' ) ); ?></p>
							<?php if ( $this->is_gmail_address( $settings['notification_email'] ) ) : ?>
								<p><strong><?php esc_html_e( 'Using Gmail?', 'mark-pacura-album-builder' ); ?></strong> <?php esc_html_e( 'To use Gmail reliably, connect your website to your Gmail account. Contact your website host for setup assistance, then retest.', 'mark-pacura-album-builder' ); ?></p>
							<?php else : ?>
								<p><?php esc_html_e( 'Check your spam folder first. If the message is missing, contact your website host to review outgoing email settings.', 'mark-pacura-album-builder' ); ?></p>
							<?php endif; ?>
							<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post">
								<input type="hidden" name="action" value="mpab_send_test_email">
								<input type="hidden" name="step" value="<?php echo esc_attr( $email_state['last_step'] ); ?>">
								<?php wp_nonce_field( 'mpab_send_test_email' ); ?>
								<?php submit_button( __( 'Try this email again', 'mark-pacura-album-builder' ), 'primary mpab-button-danger', 'submit', false ); ?>
							</form>
							<details class="mpab-email-change-address"><summary><?php esc_html_e( 'Try a different email address', 'mark-pacura-album-builder' ); ?></summary>
								<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post">
									<input type="hidden" name="action" value="mpab_send_test_email"><input type="hidden" name="step" value="owner">
									<?php wp_nonce_field( 'mpab_send_test_email' ); ?>
									<label for="mpab-retry-sender-name"><?php esc_html_e( 'Business name', 'mark-pacura-album-builder' ); ?></label><input id="mpab-retry-sender-name" type="text" name="sender_name" value="<?php echo esc_attr( $settings['email_sender_name'] ); ?>" required>
									<label for="mpab-retry-email"><?php esc_html_e( 'New order email address', 'mark-pacura-album-builder' ); ?></label><input id="mpab-retry-email" type="email" name="photographer_email" value="<?php echo esc_attr( $settings['notification_email'] ); ?>" required>
									<?php submit_button( __( 'Save and send new test', 'mark-pacura-album-builder' ), 'secondary', 'submit', false ); ?>
								</form>
							</details>
						</div>
					<?php elseif ( $email_ready ) : ?>
						<div class="mpab-email-complete">
							<span class="mpab-email-complete__icon" aria-hidden="true">✓</span>
							<div><h3><?php esc_html_e( 'Order emails are ready', 'mark-pacura-album-builder' ); ?></h3><p><?php esc_html_e( 'Both test emails were successfully received. You can now complete your Album and Wall Art offers and publish Studio to your website.', 'mark-pacura-album-builder' ); ?></p></div>
							<ul><li><?php esc_html_e( 'Test order received', 'mark-pacura-album-builder' ); ?></li><li><?php esc_html_e( 'Client confirmation received', 'mark-pacura-album-builder' ); ?></li></ul>
						</div>
					<?php elseif ( ( 'owner' === $preview_step && ! empty( $email_state['owner_sent'] ) && empty( $email_state['owner_confirmed'] ) ) || ( 'client' === $preview_step && ! empty( $email_state['client_sent'] ) && empty( $email_state['client_confirmed'] ) ) ) : ?>
						<div class="mpab-email-test-layout">
							<div class="mpab-email-test-check">
								<span class="mpab-email-step-label"><?php echo esc_html( 'owner' === $preview_step ? __( 'Check 1 of 2', 'mark-pacura-album-builder' ) : __( 'Check 2 of 2', 'mark-pacura-album-builder' ) ); ?></span>
								<h3><?php echo esc_html( 'owner' === $preview_step ? __( 'Did the test order make it to your inbox?', 'mark-pacura-album-builder' ) : __( 'Did the client confirmation reach your inbox?', 'mark-pacura-album-builder' ) ); ?></h3>
								<p><?php printf( esc_html__( 'A test email has been sent to %s. Check your inbox and spam folder, then confirm arrival below.', 'mark-pacura-album-builder' ), '<strong>' . esc_html( $settings['notification_email'] ) . '</strong>' ); ?></p>
								<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post" class="mpab-email-confirm-actions">
									<input type="hidden" name="action" value="mpab_confirm_test_email">
									<input type="hidden" name="step" value="<?php echo esc_attr( $preview_step ); ?>">
									<?php wp_nonce_field( 'mpab_confirm_test_email' ); ?>
									<button class="button button-primary" type="submit" name="result" value="received"><?php esc_html_e( 'Yes, it arrived', 'mark-pacura-album-builder' ); ?></button>
									<button class="button" type="submit" name="result" value="missing"><?php esc_html_e( 'No, it didn’t arrive', 'mark-pacura-album-builder' ); ?></button>
								</form>
							</div>
							<div class="mpab-email-preview">
								<strong><?php echo esc_html( 'owner' === $preview_step ? __( 'Here’s what an order email looks like', 'mark-pacura-album-builder' ) : __( 'Here’s what the client confirmation looks like', 'mark-pacura-album-builder' ) ); ?></strong>
								<p class="mpab-email-preview__note"><?php esc_html_e( 'This test generates a sample album order. Wall Art orders utilize the same notification settings.', 'mark-pacura-album-builder' ); ?></p>
								<iframe title="<?php esc_attr_e( 'Preview of the test email', 'mark-pacura-album-builder' ); ?>" srcdoc="<?php echo esc_attr( $preview_html ); ?>"></iframe>
							</div>
						</div>
					<?php elseif ( ! empty( $email_state['owner_confirmed'] ) ) : ?>
						<div class="mpab-email-next-test">
							<span class="mpab-email-step-label"><?php esc_html_e( 'Check 1 complete', 'mark-pacura-album-builder' ); ?></span>
							<h3><?php esc_html_e( 'Let’s test the email your clients will receive', 'mark-pacura-album-builder' ); ?></h3>
							<p><?php esc_html_e( 'A sample client confirmation email will be sent to your inbox. No clients will be contacted during this test.', 'mark-pacura-album-builder' ); ?></p>
							<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post">
								<input type="hidden" name="action" value="mpab_send_test_email"><input type="hidden" name="step" value="client">
								<?php wp_nonce_field( 'mpab_send_test_email' ); ?>
								<?php submit_button( __( 'Send client confirmation to my inbox', 'mark-pacura-album-builder' ), 'primary', 'submit', false ); ?>
							</form>
						</div>
					<?php else : ?>
					<div class="mpab-email-setup__form">
						<form action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" method="post">
							<input type="hidden" name="action" value="mpab_send_test_email"><input type="hidden" name="step" value="owner">
							<?php wp_nonce_field( 'mpab_send_test_email' ); ?>
							<label for="mpab-sender-name"><?php esc_html_e( 'Business name', 'mark-pacura-album-builder' ); ?></label>
							<input id="mpab-sender-name" class="regular-text" type="text" name="sender_name" value="<?php echo esc_attr( $settings['email_sender_name'] ); ?>" maxlength="120" data-mpab-sender-name required>
							<p class="description"><?php esc_html_e( 'Your business name as it will appear on client confirmation emails.', 'mark-pacura-album-builder' ); ?></p>
							<label for="mpab-photographer-email"><?php esc_html_e( 'Where should we send new client orders?', 'mark-pacura-album-builder' ); ?></label>
							<input id="mpab-photographer-email" class="regular-text" type="email" name="photographer_email" value="<?php echo esc_attr( $settings['notification_email'] ); ?>" data-mpab-sender-email required>
							<p class="description"><?php esc_html_e( 'All new Album and Wall Art orders will be sent to this address. Our email test verifies that your mailbox is correctly connected.', 'mark-pacura-album-builder' ); ?></p>
							<div class="mpab-email-test-note"><strong><?php esc_html_e( 'First check', 'mark-pacura-album-builder' ); ?></strong><span><?php esc_html_e( 'A test order will be sent to your email address. You can preview the email format below before confirming delivery.', 'mark-pacura-album-builder' ); ?></span></div>
							<?php submit_button( __( 'Save and send test order', 'mark-pacura-album-builder' ), 'primary', 'submit', false ); ?>
						</form>
					</div>
					<?php endif; ?>
				</div>
			</section>
			<?php endif; ?>

			<?php if ( ! $email_only ) : ?>
			<form method="post" action="options.php" class="mpab-guided-form" data-mpab-guided-form>
				<?php settings_fields( 'mpab_settings_group' ); ?>
				<input type="hidden" name="_wp_http_referer" value="<?php echo esc_url( add_query_arg( array( 'page' => 'mpab', 'mpab_albums_saved' => 1 ), admin_url( 'admin.php' ) ) ); ?>">
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[notification_email]" value="<?php echo esc_attr( $settings['notification_email'] ); ?>">
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[email_sender_name]" value="<?php echo esc_attr( $settings['email_sender_name'] ); ?>">
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[currency]" value="<?php echo esc_attr( $settings['currency'] ); ?>" data-mpab-currency>
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[accent_colour]" value="<?php echo esc_attr( $settings['accent_colour'] ); ?>">
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[order_retention_months]" value="<?php echo esc_attr( $settings['order_retention_months'] ); ?>">
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[promotion_scope]" value="<?php echo esc_attr( $settings['promotion_scope'] ); ?>">
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[promotion_enabled]" value="<?php echo esc_attr( ! empty( $settings['promotion_enabled'] ) ? 1 : 0 ); ?>" data-mpab-album-promotion-enabled>
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[promotion_percent]" value="<?php echo esc_attr( $settings['promotion_percent'] ); ?>">
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[promotion_end_date]" value="<?php echo esc_attr( $settings['promotion_end_date'] ); ?>">
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[wall_art_promotion_enabled]" value="<?php echo esc_attr( ! empty( $settings['wall_art_promotion_enabled'] ) ? 1 : 0 ); ?>">
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[wall_art_promotion_percent]" value="<?php echo esc_attr( $settings['wall_art_promotion_percent'] ); ?>">
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[wall_art_promotion_end_date]" value="<?php echo esc_attr( $settings['wall_art_promotion_end_date'] ); ?>">
				<section class="mpab-setup-panel" data-mpab-setup-panel="2">
				<header class="mpab-setup-panel__header"><span class="mpab-section-kicker"><?php esc_html_e( 'Step 1', 'mark-pacura-album-builder' ); ?></span><h2 id="mpab-options"><?php esc_html_e( 'Build the album offer your clients will see', 'mark-pacura-album-builder' ); ?></h2><p><?php esc_html_e( 'Select your albums, sizes, cover materials, personalisation, and presentation options.', 'mark-pacura-album-builder' ); ?></p></header>

				<div class="mpab-shared-pricing mpab-pricing-mode--offer" id="mpab-offer-basics" data-mpab-shared-pricing>
					<?php $this->render_pricing_table( $settings ); ?>
				</div>
				<div data-mpab-pricing-mount="offer"></div>
				<?php $this->render_full_catalogue_settings( $settings ); ?>
				<h3><?php esc_html_e( 'Presentation', 'mark-pacura-album-builder' ); ?></h3>
				<p><?php esc_html_e( 'Select presentation box options to offer clients. Leave unselected to hide this step in the client journey.', 'mark-pacura-album-builder' ); ?></p>
				<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_boxes][]" value="">
				<div class="mpab-admin-grid">
					<?php $this->option_row( 'box_wooden', __( 'Presentation option 1', 'mark-pacura-album-builder' ), $settings ); ?>
					<?php $this->option_row( 'box_fabric', __( 'Presentation option 2', 'mark-pacura-album-builder' ), $settings ); ?>
				</div>
				<div class="mpab-setup-panel__actions"><button class="button button-primary" type="button" data-mpab-setup-next="3"><?php esc_html_e( 'Continue to pricing', 'mark-pacura-album-builder' ); ?></button></div>
				</section>

				<section class="mpab-setup-panel" data-mpab-setup-panel="3" hidden>
				<header class="mpab-setup-panel__header"><span class="mpab-section-kicker"><?php esc_html_e( 'Step 2', 'mark-pacura-album-builder' ); ?></span><h2><?php esc_html_e( 'Set the prices your clients will pay', 'mark-pacura-album-builder' ); ?></h2><p><?php esc_html_e( 'Your selected albums and sizes are listed below. Wooden Banana base costs are shown for reference; enter your client retail prices.', 'mark-pacura-album-builder' ); ?></p></header>
				<div data-mpab-pricing-mount="pricing"></div>
				<div class="mpab-setup-panel__actions"><button class="button" type="button" data-mpab-setup-next="2"><?php esc_html_e( 'Back to albums', 'mark-pacura-album-builder' ); ?></button><button class="button button-primary" type="button" data-mpab-setup-next="4"><?php esc_html_e( 'Review my offer', 'mark-pacura-album-builder' ); ?></button></div>
				</section>

				<section class="mpab-setup-panel" data-mpab-setup-panel="4" hidden>
				<header class="mpab-setup-panel__header"><span class="mpab-section-kicker"><?php esc_html_e( 'Step 3', 'mark-pacura-album-builder' ); ?></span><h2><?php esc_html_e( 'Take one last look', 'mark-pacura-album-builder' ); ?></h2><p><?php esc_html_e( 'Review your configured offer below. Go back to edit settings or click save when ready.', 'mark-pacura-album-builder' ); ?></p></header>
				<div class="mpab-offer-check" data-mpab-offer-check aria-live="polite"></div>
				<div class="mpab-setup-panel__actions"><button class="button" type="button" data-mpab-setup-next="3"><?php esc_html_e( 'Back to pricing', 'mark-pacura-album-builder' ); ?></button><?php submit_button( __( 'Save my offer', 'mark-pacura-album-builder' ), 'primary', 'submit', false ); ?></div>
				<p class="description"><?php esc_html_e( 'Saving your offer updates your website configuration and returns you to the Studio overview.', 'mark-pacura-album-builder' ); ?></p>
				</section>
			</form>
			<?php endif; ?>
			<?php if ( ! $email_only ) { $this->render_recent_orders(); } ?>
		</div>
		<?php
	}

	/**
	 * Show the latest saved album orders directly on the plugin dashboard.
	 *
	 * @return void
	 */
	private function render_recent_orders() {
		$orders = get_posts(
			array(
				'post_type'      => self::POST_TYPE,
				'post_status'    => 'private',
				'posts_per_page' => 8,
				'orderby'        => 'date',
				'order'          => 'DESC',
			)
		);
		?>
		<details class="mpab-orders-drawer" id="mpab-previous-orders">
			<summary><span class="mpab-orders-drawer__icon" aria-hidden="true">▤</span><span><strong><?php esc_html_e( 'Previous orders', 'mark-pacura-album-builder' ); ?></strong><small><?php echo esc_html( empty( $orders ) ? __( 'No orders yet', 'mark-pacura-album-builder' ) : sprintf( _n( '%d recent order', '%d recent orders', count( $orders ), 'mark-pacura-album-builder' ), count( $orders ) ) ); ?></small></span><em><?php esc_html_e( 'Open', 'mark-pacura-album-builder' ); ?></em></summary>
		<section class="mpab-previous-orders">
			<header class="mpab-previous-orders__header">
				<div><span class="mpab-section-kicker"><?php esc_html_e( 'Saved automatically', 'mark-pacura-album-builder' ); ?></span><h2><?php esc_html_e( 'Previous orders', 'mark-pacura-album-builder' ); ?></h2><p><?php esc_html_e( 'All album orders are stored securely here. Select an order to view full details, client selections, and email delivery status.', 'mark-pacura-album-builder' ); ?></p></div>
				<a class="button" href="<?php echo esc_url( admin_url( 'edit.php?post_type=' . self::POST_TYPE ) ); ?>"><?php esc_html_e( 'View all orders', 'mark-pacura-album-builder' ); ?></a>
			</header>
			<?php if ( empty( $orders ) ) : ?>
				<div class="mpab-previous-orders__empty"><strong><?php esc_html_e( 'No album orders yet', 'mark-pacura-album-builder' ); ?></strong><p><?php esc_html_e( 'Completed client order requests will appear in this table.', 'mark-pacura-album-builder' ); ?></p></div>
			<?php else : ?>
				<div class="mpab-order-archive">
				<?php foreach ( $orders as $order ) : ?>
					<?php
					$config = json_decode( (string) get_post_meta( $order->ID, '_mpab_configuration', true ), true );
					$config = is_array( $config ) ? $config : array();
					$reference = (string) get_post_meta( $order->ID, '_mpab_reference', true );
					$name = (string) get_post_meta( $order->ID, '_mpab_client_name', true );
					$email = (string) get_post_meta( $order->ID, '_mpab_client_email', true );
					$status = (string) get_post_meta( $order->ID, '_mpab_status', true );
					$status = $status ? $status : 'new';
					$client_mail_sent = 'yes' === get_post_meta( $order->ID, '_mpab_client_email_sent', true );
					$client_mail_retry = (string) get_post_meta( $order->ID, '_mpab_client_email_retry', true );
					$client_mail_label = $client_mail_sent ? __( 'Sent', 'mark-pacura-album-builder' ) : ( 'scheduled' === $client_mail_retry ? __( 'Waiting to retry', 'mark-pacura-album-builder' ) : __( 'Failed', 'mark-pacura-album-builder' ) );
					$total = isset( $config['total'] ) ? $this->format_price( $config['total'], isset( $config['currency_symbol'] ) ? $config['currency_symbol'] : '' ) : '';
					$detail_rows = ! empty( $config['album_type'] ) ? $this->email_summary_rows( $config ) : array();
					?>
					<details class="mpab-order-archive__item">
						<summary>
							<span class="mpab-order-status mpab-order-status--<?php echo esc_attr( sanitize_html_class( $status ) ); ?>"><?php echo esc_html( ucfirst( $status ) ); ?></span>
							<span class="mpab-order-archive__identity"><strong><?php echo esc_html( sprintf( __( 'Order %s', 'mark-pacura-album-builder' ), $reference ) ); ?></strong><span><?php echo esc_html( $name ); ?></span></span>
							<span class="mpab-order-archive__date"><?php echo esc_html( get_the_date( get_option( 'date_format' ), $order ) ); ?></span>
							<strong class="mpab-order-archive__total"><?php echo esc_html( $total ); ?></strong>
						</summary>
						<div class="mpab-order-archive__body">
							<div class="mpab-order-archive__client"><div><span><?php esc_html_e( 'Client', 'mark-pacura-album-builder' ); ?></span><strong><?php echo esc_html( $name ); ?></strong></div><div><span><?php esc_html_e( 'Email', 'mark-pacura-album-builder' ); ?></span><a href="mailto:<?php echo esc_attr( $email ); ?>"><?php echo esc_html( $email ); ?></a></div><div><span><?php esc_html_e( 'Submitted', 'mark-pacura-album-builder' ); ?></span><strong><?php echo esc_html( get_the_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $order ) ); ?></strong></div></div>
							<dl class="mpab-order-archive__details">
							<?php foreach ( $detail_rows as $detail_row ) : ?><div><dt><?php echo esc_html( $detail_row[0] ); ?></dt><dd><?php echo nl2br( esc_html( $detail_row[1] ) ); ?></dd></div><?php endforeach; ?>
								<div><dt><?php esc_html_e( 'Total', 'mark-pacura-album-builder' ); ?></dt><dd><?php echo esc_html( $total ); ?></dd></div>
								<div><dt><?php esc_html_e( 'Email copies', 'mark-pacura-album-builder' ); ?></dt><dd><?php echo esc_html( sprintf( __( 'Photographer Email: %1$s · Client Email: %2$s', 'mark-pacura-album-builder' ), 'yes' === get_post_meta( $order->ID, '_mpab_owner_email_sent', true ) ? __( 'Sent', 'mark-pacura-album-builder' ) : __( 'Failed', 'mark-pacura-album-builder' ), $client_mail_label ) ); ?></dd></div>
							</dl>
							<?php $note = (string) get_post_meta( $order->ID, '_mpab_client_note', true ); ?>
							<?php if ( $note ) : ?><div class="mpab-order-archive__note"><strong><?php esc_html_e( 'Client note', 'mark-pacura-album-builder' ); ?></strong><p><?php echo nl2br( esc_html( $note ) ); ?></p></div><?php endif; ?>
							<div class="mpab-order-archive__actions">
								<a class="button button-primary" href="<?php echo esc_url( admin_url( 'post.php?post=' . absint( $order->ID ) . '&action=edit' ) ); ?>"><?php esc_html_e( 'Open full order', 'mark-pacura-album-builder' ); ?></a>
								<form class="mpab-remove-order" method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" onsubmit="return window.confirm('<?php echo esc_js( __( 'Remove this order? It will move to Trash, where you can restore it if needed.', 'mark-pacura-album-builder' ) ); ?>');">
									<input type="hidden" name="action" value="mpab_remove_order">
									<input type="hidden" name="post_id" value="<?php echo absint( $order->ID ); ?>">
									<?php wp_nonce_field( 'mpab_remove_order_' . $order->ID ); ?>
									<button class="button mpab-remove-order__button" type="submit"><?php esc_html_e( 'Remove order', 'mark-pacura-album-builder' ); ?></button>
								</form>
							</div>
						</div>
					</details>
				<?php endforeach; ?>
				</div>
			<?php endif; ?>
		</section>
		</details>
		<?php
	}

	/**
	 * Render the fixed full-brochure catalogue with offer inclusion controls.
	 *
	 * Dashboard conditional logic keeps dependent choices tidy without deleting
	 * saved selections. Client-side compatibility remains the final authority.
	 *
	 * @param array $settings Current settings.
	 * @return void
	 */
	private function render_full_catalogue_settings( $settings ) {
		$cover_source = self::cover_source();
		$personal     = self::personalisation_source();
		$family_names = array();
		$family_counts = array();
		$method_names = array();
		foreach ( $cover_source['cover_families'] as $family ) {
			$family_names[ $family['family_code'] ] = $family;
			$family_counts[ $family['family_code'] ] = 0;
		}
		foreach ( $personal['methods'] as $method ) {
			$method_names[ $method['id'] ] = $method['label'];
		}
		foreach ( $cover_source['active_colours'] as $colour ) {
			if ( isset( $family_counts[ $colour['family_code'] ] ) ) {
				$family_counts[ $colour['family_code'] ]++;
			}
		}
		$collection_names = array(
			'collection.illustrations' => __( 'Illustrations', 'mark-pacura-album-builder' ),
			'collection.calligraphy'  => __( 'Calligraphy', 'mark-pacura-album-builder' ),
			'collection.family'       => __( 'Family', 'mark-pacura-album-builder' ),
			'collection.boudoir'      => __( 'Boudoir', 'mark-pacura-album-builder' ),
			'collection.full-cover'   => __( 'Full-cover designs', 'mark-pacura-album-builder' ),
		);
		?>
		<section class="mpab-catalogue-section">
			<header class="mpab-catalogue-section__header">
				<div><span class="mpab-section-kicker"><?php esc_html_e( 'Offer catalogue', 'mark-pacura-album-builder' ); ?></span><h2><?php esc_html_e( 'Cover materials and colours', 'mark-pacura-album-builder' ); ?></h2></div>
				<p><?php esc_html_e( 'Choose the Wooden Banana covers you wish to offer. Swatches and titles are pre-configured for your studio.', 'mark-pacura-album-builder' ); ?></p>
			</header>
		<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_cover_families][]" value="">
		<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_cover_colours][]" value="">
		<div class="mpab-family-list">
		<?php foreach ( $family_names as $family_code => $family ) : ?>
			<?php
			$selected_family_colours = 0;
			foreach ( $cover_source['active_colours'] as $colour ) {
				if ( $family_code === $colour['family_code'] && in_array( $colour['colour_id'], $settings['enabled_cover_colours'], true ) ) {
					$selected_family_colours++;
				}
			}
			?>
			<details class="mpab-family" open>
				<summary>
					<span class="mpab-family__identity"><strong><?php echo esc_html( $family['family_name'] ); ?></strong><span><?php echo esc_html( sprintf( _n( '%1$s · %2$d colour', '%1$s · %2$d colours', $family_counts[ $family_code ], 'mark-pacura-album-builder' ), $this->catalogue_translation( $family['material'], 'Cover material type' ), $family_counts[ $family_code ] ) ); ?></span></span>
					<span class="mpab-selection-count" data-mpab-summary-count><?php echo esc_html( sprintf( __( '%1$d of %2$d selected', 'mark-pacura-album-builder' ), $selected_family_colours, $family_counts[ $family_code ] ) ); ?></span>
				</summary>
				<div class="mpab-selection-group" data-mpab-selection-group>
					<div class="mpab-selection-toolbar">
						<label class="mpab-family-switch"><input type="checkbox" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_cover_families][]" value="<?php echo esc_attr( $family_code ); ?>" <?php checked( in_array( $family_code, $settings['enabled_cover_families'], true ) ); ?>><span><?php esc_html_e( 'Include this cover family', 'mark-pacura-album-builder' ); ?></span></label>
						<div class="mpab-selection-toolbar__actions"><span data-mpab-selection-count><?php echo esc_html( sprintf( __( '%1$d of %2$d selected', 'mark-pacura-album-builder' ), $selected_family_colours, $family_counts[ $family_code ] ) ); ?></span><button class="button button-small" type="button" data-mpab-select="all"><?php esc_html_e( 'Select all', 'mark-pacura-album-builder' ); ?></button><button class="button button-small" type="button" data-mpab-select="none"><?php esc_html_e( 'Clear', 'mark-pacura-album-builder' ); ?></button></div>
					</div>
				<div class="mpab-admin-grid mpab-admin-grid--swatches">
				<?php foreach ( $cover_source['active_colours'] as $colour ) : ?>
					<?php if ( $family_code !== $colour['family_code'] ) { continue; } ?>
					<?php $is_selected = in_array( $colour['colour_id'], $settings['enabled_cover_colours'], true ); ?>
					<label class="mpab-admin-card mpab-select-card mpab-swatch-card<?php echo $is_selected ? ' is-selected' : ''; ?>">
						<input class="mpab-select-card__control" type="checkbox" data-mpab-selection-input name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_cover_colours][]" value="<?php echo esc_attr( $colour['colour_id'] ); ?>" <?php checked( $is_selected ); ?>>
						<span class="mpab-select-card__check" aria-hidden="true">✓</span>
						<span class="mpab-admin-card__thumb">
						<?php $image = $this->cover_image( $colour['official_code'] ); ?>
						<?php if ( $image ) : ?>
							<img src="<?php echo esc_url( $image ); ?>" alt="" loading="lazy" decoding="async">
						<?php else : ?>
							<span class="mpab-image-pending"><?php esc_html_e( 'Official image pending', 'mark-pacura-album-builder' ); ?></span>
						<?php endif; ?>
						</span>
						<span class="mpab-select-card__label"><strong><?php echo esc_html( $this->catalogue_translation( $colour['friendly_name'], 'Cover colour' ) ); ?></strong><span><?php echo esc_html( $colour['official_code'] ); ?></span></span>
					</label>
				<?php endforeach; ?>
				</div>
				</div>
			</details>
		<?php endforeach; ?>
		</div>
		</section>

		<section class="mpab-catalogue-section mpab-personalisation-settings">
			<header class="mpab-catalogue-section__header">
				<div><span class="mpab-section-kicker"><?php esc_html_e( 'Client choices', 'mark-pacura-album-builder' ); ?></span><h2><?php esc_html_e( 'Personalisation offer', 'mark-pacura-album-builder' ); ?></h2></div>
				<p><?php esc_html_e( 'Select the personalisation choices to offer, including text, fonts, and designs. Clients only see options compatible with their selected cover.', 'mark-pacura-album-builder' ); ?></p>
			</header>
		<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_personalisation_methods][]" value="">
		<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_foil_colours][]" value="">
		<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_personalisation_fonts][]" value="">
		<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_design_collections][]" value="">
		<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_personalisation_designs][]" value="">

		<div class="mpab-text-line-setting">
			<div>
				<h3><?php esc_html_e( 'Text personalisation', 'mark-pacura-album-builder' ); ?></h3>
				<p><?php esc_html_e( 'Set the maximum lines of text a client can add. Each line allows up to 25 characters.', 'mark-pacura-album-builder' ); ?></p>
			</div>
			<label for="mpab-personalisation-text-lines"><span><?php esc_html_e( 'Maximum lines', 'mark-pacura-album-builder' ); ?></span>
				<select id="mpab-personalisation-text-lines" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[personalisation_text_lines]">
				<?php for ( $line_count = 1; $line_count <= 5; $line_count++ ) : ?>
					<option value="<?php echo esc_attr( $line_count ); ?>" <?php selected( (int) $settings['personalisation_text_lines'], $line_count ); ?>><?php echo esc_html( sprintf( _n( '%d line', '%d lines', $line_count, 'mark-pacura-album-builder' ), $line_count ) ); ?></option>
				<?php endfor; ?>
				</select>
			</label>
		</div>

		<div class="mpab-option-group" data-mpab-selection-group>
			<div class="mpab-option-group__heading"><div><h3><?php esc_html_e( 'Personalisation methods', 'mark-pacura-album-builder' ); ?></h3><p><?php esc_html_e( 'Clients will only see personalisation options compatible with their chosen cover.', 'mark-pacura-album-builder' ); ?></p></div><div class="mpab-selection-toolbar__actions"><span data-mpab-selection-count></span><button class="button button-small" type="button" data-mpab-select="all"><?php esc_html_e( 'Select all', 'mark-pacura-album-builder' ); ?></button><button class="button button-small" type="button" data-mpab-select="none"><?php esc_html_e( 'Clear', 'mark-pacura-album-builder' ); ?></button></div></div>
		<div class="mpab-admin-grid mpab-admin-grid--methods">
		<?php foreach ( $personal['methods'] as $method ) : ?>
			<?php
			$method_selected = in_array( $method['id'], $settings['enabled_personalisation_methods'], true );
			$compatible_names = array();
			foreach ( $method['cover_family_ids'] as $code ) {
				if ( isset( $family_names[ $code ] ) ) {
					$compatible_names[] = $family_names[ $code ]['family_name'];
				}
			}
			?>
			<label class="mpab-admin-card mpab-select-card mpab-method-card<?php echo $method_selected ? ' is-selected' : ''; ?>">
				<input class="mpab-select-card__control" type="checkbox" data-mpab-selection-input name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_personalisation_methods][]" value="<?php echo esc_attr( $method['id'] ); ?>" <?php checked( $method_selected ); ?>>
				<span class="mpab-select-card__check" aria-hidden="true">✓</span>
				<span class="mpab-method-card__icon" aria-hidden="true"><?php echo esc_html( 'foil' === $method['id'] ? __( 'Foil', 'mark-pacura-album-builder' ) : __( 'Laser', 'mark-pacura-album-builder' ) ); ?></span>
				<strong><?php echo esc_html( $this->catalogue_translation( $method['label'], 'Personalisation method' ) ); ?></strong>
				<span><?php echo esc_html( implode( ', ', $compatible_names ) ); ?></span>
			</label>
		<?php endforeach; ?>
		</div>
		</div>

		<div class="mpab-option-group" data-mpab-selection-group data-mpab-requires-method="foil">
			<div class="mpab-option-group__heading"><div><h3><?php esc_html_e( 'Foil colours', 'mark-pacura-album-builder' ); ?></h3></div><div class="mpab-selection-toolbar__actions"><span data-mpab-selection-count></span><button class="button button-small" type="button" data-mpab-select="all"><?php esc_html_e( 'Select all', 'mark-pacura-album-builder' ); ?></button><button class="button button-small" type="button" data-mpab-select="none"><?php esc_html_e( 'Clear', 'mark-pacura-album-builder' ); ?></button></div></div>
			<div class="mpab-admin-grid mpab-admin-grid--foil-colours">
			<?php foreach ( $personal['foil_colours'] as $colour ) : ?>
				<?php
				$foil_selected = in_array( $colour['id'], $settings['enabled_foil_colours'], true );
				$foil_image = $this->personalisation_image( isset( $colour['thumbnail']['public_path'] ) ? $colour['thumbnail']['public_path'] : '', 'foil-colours' );
				?>
				<label class="mpab-admin-card mpab-select-card mpab-foil-card<?php echo $foil_selected ? ' is-selected' : ''; ?>"><input class="mpab-select-card__control" type="checkbox" data-mpab-selection-input name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_foil_colours][]" value="<?php echo esc_attr( $colour['id'] ); ?>" <?php checked( $foil_selected ); ?>><span class="mpab-foil-card__swatch" aria-hidden="true"><?php if ( $foil_image ) : ?><img src="<?php echo esc_url( $foil_image ); ?>" alt="" loading="lazy" decoding="async"><?php endif; ?></span><strong><?php echo esc_html( $this->catalogue_translation( $colour['label'], 'Foil colour' ) ); ?></strong></label>
			<?php endforeach; ?>
			</div>
		</div>

		<details class="mpab-option-group mpab-library" data-mpab-conditional-library open>
			<summary><span><strong><?php esc_html_e( 'Font previews', 'mark-pacura-album-builder' ); ?></strong><small><?php esc_html_e( 'Click any thumbnail to add or remove it from your studio offer.', 'mark-pacura-album-builder' ); ?></small></span><span class="mpab-selection-count"><?php echo esc_html( sprintf( __( '%1$d of %2$d selected', 'mark-pacura-album-builder' ), count( $settings['enabled_personalisation_fonts'] ), count( $personal['fonts'] ) ) ); ?></span></summary>
			<div data-mpab-selection-group>
			<div class="mpab-selection-toolbar"><p><?php esc_html_e( 'Available fonts for your selected personalisation methods.', 'mark-pacura-album-builder' ); ?></p><div class="mpab-selection-toolbar__actions"><span data-mpab-selection-count></span><button class="button button-small" type="button" data-mpab-select="all"><?php esc_html_e( 'Select all', 'mark-pacura-album-builder' ); ?></button><button class="button button-small" type="button" data-mpab-select="none"><?php esc_html_e( 'Clear', 'mark-pacura-album-builder' ); ?></button></div></div>
			<div class="mpab-admin-grid mpab-admin-grid--library">
			<?php foreach ( $personal['fonts'] as $font ) : ?>
				<?php
				$font_selected = in_array( $font['id'], $settings['enabled_personalisation_fonts'], true );
				$font_methods = array();
				foreach ( $font['method_ids'] as $method_id ) {
					if ( isset( $method_names[ $method_id ] ) ) {
						$font_methods[] = $method_names[ $method_id ];
					}
				}
				?>
				<label class="mpab-admin-card mpab-library-card mpab-select-card<?php echo $font_selected ? ' is-selected' : ''; ?>" data-mpab-methods="<?php echo esc_attr( implode( ' ', $font['method_ids'] ) ); ?>">
					<input class="mpab-select-card__control" type="checkbox" data-mpab-selection-input name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_personalisation_fonts][]" value="<?php echo esc_attr( $font['id'] ); ?>" <?php checked( $font_selected ); ?>><span class="mpab-select-card__check" aria-hidden="true">✓</span>
					<?php $font_image = $this->personalisation_image( isset( $font['thumbnail']['public_path'] ) ? $font['thumbnail']['public_path'] : '', 'fonts' ); ?>
					<?php if ( $font_image ) : ?><span class="mpab-admin-card__thumb"><img src="<?php echo esc_url( $font_image ); ?>" alt="" loading="lazy" decoding="async"></span><?php endif; ?>
					<span class="mpab-select-card__label"><strong><?php echo esc_html( $font['label'] ); ?></strong><span><?php echo esc_html( implode( ' + ', $font_methods ) ); ?></span></span>
				</label>
			<?php endforeach; ?>
			</div>
			</div>
		</details>

		<div class="mpab-design-groups">
		<?php foreach ( $collection_names as $collection_id => $collection_name ) : ?>
			<?php
			$matching_designs = array_filter(
				$personal['design_items'],
				function ( $item ) use ( $collection_id ) {
					return ! empty( $item['selectable'] ) && $collection_id === $item['collection_id'];
				}
			);
			if ( empty( $matching_designs ) ) { continue; }
			$selected_designs = 0;
			foreach ( $matching_designs as $design ) {
				if ( in_array( $design['source_reference'], $settings['enabled_personalisation_designs'], true ) ) {
					$selected_designs++;
				}
			}
			$collection_enabled = in_array( $collection_id, $settings['enabled_design_collections'], true );
			?>
			<details class="mpab-family mpab-library<?php echo $collection_enabled ? '' : ' is-disabled'; ?>" data-mpab-conditional-library open>
				<summary><span class="mpab-family__identity"><strong><?php echo esc_html( $collection_name ); ?></strong><span><?php echo esc_html( sprintf( _n( '%d design thumbnail', '%d design thumbnails', count( $matching_designs ), 'mark-pacura-album-builder' ), count( $matching_designs ) ) ); ?></span></span><span class="mpab-selection-count" data-mpab-summary-count><?php echo esc_html( sprintf( __( '%1$d of %2$d selected', 'mark-pacura-album-builder' ), $selected_designs, count( $matching_designs ) ) ); ?></span></summary>
				<div data-mpab-selection-group>
				<div class="mpab-selection-toolbar"><label class="mpab-family-switch"><input type="checkbox" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_design_collections][]" value="<?php echo esc_attr( $collection_id ); ?>" <?php checked( $collection_enabled ); ?>><span><?php esc_html_e( 'Include this design collection', 'mark-pacura-album-builder' ); ?></span></label><div class="mpab-selection-toolbar__actions"><span data-mpab-selection-count></span><button class="button button-small" type="button" data-mpab-select="all"><?php esc_html_e( 'Select all', 'mark-pacura-album-builder' ); ?></button><button class="button button-small" type="button" data-mpab-select="none"><?php esc_html_e( 'Clear', 'mark-pacura-album-builder' ); ?></button></div></div>
				<div class="mpab-admin-grid mpab-admin-grid--library">
				<?php foreach ( $matching_designs as $design ) : ?>
					<?php $design_selected = in_array( $design['source_reference'], $settings['enabled_personalisation_designs'], true ); ?>
					<label class="mpab-admin-card mpab-library-card mpab-select-card<?php echo $design_selected ? ' is-selected' : ''; ?>" data-mpab-methods="<?php echo esc_attr( implode( ' ', $design['method_ids'] ) ); ?>">
						<input class="mpab-select-card__control" type="checkbox" data-mpab-selection-input name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_personalisation_designs][]" value="<?php echo esc_attr( $design['source_reference'] ); ?>" <?php checked( $design_selected ); ?>><span class="mpab-select-card__check" aria-hidden="true">✓</span>
						<?php $design_image = $this->personalisation_image( isset( $design['thumbnail']['public_path'] ) ? $design['thumbnail']['public_path'] : '', 'designs' ); ?>
						<?php if ( $design_image ) : ?><span class="mpab-admin-card__thumb"><img src="<?php echo esc_url( $design_image ); ?>" alt="" loading="lazy" decoding="async"></span><?php endif; ?>
						<span class="mpab-select-card__label"><strong><?php echo esc_html( $this->catalogue_translation( $design['display_name'], 'Personalisation design name' ) ); ?></strong></span>
					</label>
				<?php endforeach; ?>
				</div>
				</div>
			</details>
		<?php endforeach; ?>
		</div>
		</section>
		<?php
	}

	/**
	 * Render the Studio-style comparison table for album and spread prices.
	 *
	 * @param array $settings Current settings.
	 * @return void
	 */
	private function render_pricing_table( $settings ) {
		$symbol = $this->currency_symbol( $settings['currency'] );
		?>
		<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_album_types][]" value="">
		<input type="hidden" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_sizes][]" value="">
		<div class="mpab-pricing-groups">
		<?php foreach ( self::size_definitions() as $album_id => $album ) : ?>
			<?php
			$album_enabled = in_array( $album_id, $settings['enabled_album_types'], true );
			$album_label_key = $album['label_setting'];
			?>
			<section class="mpab-pricing-card<?php echo $album_enabled ? '' : ' is-disabled'; ?>" data-mpab-album-offer data-mpab-album-size-group>
				<header class="mpab-pricing-card__header">
					<label class="mpab-offer-toggle"><input type="checkbox" data-mpab-album-toggle name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_album_types][]" value="<?php echo esc_attr( $album_id ); ?>" <?php checked( $album_enabled ); ?>><span><strong><?php echo esc_html( $settings[ $album_label_key ] ); ?></strong><small><?php echo esc_html( count( $album['sizes'] ) . ' available sizes' ); ?></small></span></label>
					<label class="mpab-album-name"><span><?php esc_html_e( 'Client-facing album name', 'mark-pacura-album-builder' ); ?></span><input id="mpab-<?php echo esc_attr( $album_label_key ); ?>" type="text" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[<?php echo esc_attr( $album_label_key ); ?>]" value="<?php echo esc_attr( $settings[ $album_label_key ] ); ?>"></label>
				</header>
				<div class="mpab-pricing-card__body" data-mpab-album-body>
				<div class="mpab-size-actions"><span><?php esc_html_e( 'Choose the sizes you want to offer.', 'mark-pacura-album-builder' ); ?></span><button class="button button-small" type="button" data-mpab-select-album-sizes><?php esc_html_e( 'Select all sizes', 'mark-pacura-album-builder' ); ?></button></div>
				<div class="mpab-pricing-table-wrap">
				<table class="widefat striped mpab-pricing-table">
					<thead><tr>
						<th><span><?php esc_html_e( 'Include', 'mark-pacura-album-builder' ); ?></span></th>
						<th><span><?php esc_html_e( 'Album size', 'mark-pacura-album-builder' ); ?></span><small><?php esc_html_e( 'shown to client', 'mark-pacura-album-builder' ); ?></small></th>
						<th><span><?php esc_html_e( 'Your cost from Wooden Banana.', 'mark-pacura-album-builder' ); ?></span><small><?php esc_html_e( '10 spreads', 'mark-pacura-album-builder' ); ?></small></th>
						<th><span><?php esc_html_e( 'The price your client pays.', 'mark-pacura-album-builder' ); ?></span><small><?php esc_html_e( '10 spreads', 'mark-pacura-album-builder' ); ?></small></th>
						<th><span><?php esc_html_e( 'Your cost from Wooden Banana.', 'mark-pacura-album-builder' ); ?></span><small><?php esc_html_e( 'extra spread', 'mark-pacura-album-builder' ); ?></small></th>
						<th><span><?php esc_html_e( 'The price your client pays.', 'mark-pacura-album-builder' ); ?></span><small><?php esc_html_e( 'extra spread', 'mark-pacura-album-builder' ); ?></small></th>
					</tr></thead>
					<tbody>
					<?php foreach ( $album['sizes'] as $size ) : ?>
						<?php $size_enabled = in_array( $size['id'], $settings['enabled_sizes'], true ); ?>
						<tr class="<?php echo $size_enabled ? '' : 'is-disabled'; ?>" data-mpab-pricing-row>
							<td><label class="mpab-size-toggle"><input type="checkbox" data-mpab-size-toggle name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_sizes][]" value="<?php echo esc_attr( $size['id'] ); ?>" <?php checked( $size_enabled ); ?>><span><?php esc_html_e( 'Include', 'mark-pacura-album-builder' ); ?></span></label></td>
							<td><label class="screen-reader-text" for="mpab-<?php echo esc_attr( $size['prefix'] ); ?>"><?php esc_html_e( 'Client-facing size label', 'mark-pacura-album-builder' ); ?></label><input class="widefat" id="mpab-<?php echo esc_attr( $size['prefix'] ); ?>" type="text" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[<?php echo esc_attr( $size['prefix'] . '_label' ); ?>]" value="<?php echo esc_attr( $settings[ $size['prefix'] . '_label' ] ); ?>"></td>
							<td><?php $this->render_wb_price( $size['prefix'], 'album', $settings['currency'] ); ?></td>
							<td><label class="mpab-money-input"><span data-mpab-currency-symbol><?php echo esc_html( $symbol ); ?></span><input min="0" step="0.01" type="number" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[<?php echo esc_attr( $size['prefix'] . '_price' ); ?>]" value="<?php echo esc_attr( $settings[ $size['prefix'] . '_price' ] ); ?>"></label></td>
							<td><?php $this->render_wb_price( $size['prefix'], 'spread', $settings['currency'] ); ?></td>
							<td><label class="mpab-money-input mpab-money-input--extra"><span data-mpab-currency-symbol><?php echo esc_html( $symbol ); ?></span><input min="0" step="0.01" type="number" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[<?php echo esc_attr( $size['prefix'] . '_spread_price' ); ?>]" value="<?php echo esc_attr( $settings[ $size['prefix'] . '_spread_price' ] ); ?>"></label></td>
						</tr>
					<?php endforeach; ?>
					</tbody>
				</table>
				</div>
				</div>
			</section>
		<?php endforeach; ?>
		</div>
		<p class="description"><?php esc_html_e( 'Note: 1 spread equals 2 pages. Enter your total price for a 10-spread album, followed by the cost per additional spread.', 'mark-pacura-album-builder' ); ?></p>
		<?php
	}

	/**
	 * Render one currency-aware, read-only WB reference price.
	 *
	 * @param string $prefix Size settings prefix.
	 * @param string $kind Album or spread.
	 * @param string $currency Selected currency.
	 * @return void
	 */
	private function render_wb_price( $prefix, $kind, $currency ) {
		$prices  = self::wb_prices();
		$current = $prices[ $prefix ][ $currency ][ $kind ];
		?>
		<span class="mpab-wb-price" data-mpab-wb-price data-gbp="<?php echo esc_attr( $prices[ $prefix ]['GBP'][ $kind ] ); ?>" data-eur="<?php echo esc_attr( $prices[ $prefix ]['EUR'][ $kind ] ); ?>" data-usd="<?php echo esc_attr( $prices[ $prefix ]['USD'][ $kind ] ); ?>"><?php echo esc_html( $this->currency_symbol( $currency ) . number_format( $current, 2, '.', '' ) ); ?></span>
		<?php
	}

	/**
	 * Render label and price controls for one deliberately fixed option.
	 *
	 * @param string $prefix Option prefix.
	 * @param string $label Row label.
	 * @param array  $settings Current settings.
	 * @return void
	 */
	private function option_row( $prefix, $label, $settings ) {
		$box_id = 'box_wooden' === $prefix ? 'wooden' : 'fabric';
		$this->admin_option_card( $prefix, $label, $settings, true, $box_id );
	}

	/**
	 * Render one unrestricted offer card in the WordPress backend.
	 *
	 * @param string $prefix Option prefix.
	 * @param string $label Card eyebrow.
	 * @param array  $settings Current settings.
	 * @param bool   $has_price Whether this option adds a price field.
	 * @param string $box_id Optional presentation-box identifier.
	 * @return void
	 */
	private function admin_option_card( $prefix, $label, $settings, $has_price, $box_id = '' ) {
		$label_key = $prefix . '_label';
		$price_key = $prefix . '_price';
		$is_enabled = '' === $box_id || in_array( $box_id, $settings['enabled_boxes'], true );
		?>
		<div class="mpab-admin-card mpab-presentation-card<?php echo $is_enabled ? '' : ' is-disabled'; ?>"<?php echo $box_id ? ' data-mpab-box-offer' : ''; ?>>
			<?php if ( $box_id ) : ?>
			<label class="mpab-offer-toggle mpab-box-toggle"><input type="checkbox" data-mpab-box-toggle name="<?php echo esc_attr( self::OPTION_KEY ); ?>[enabled_boxes][]" value="<?php echo esc_attr( $box_id ); ?>" <?php checked( $is_enabled ); ?>><span><strong><?php esc_html_e( 'Offer this box', 'mark-pacura-album-builder' ); ?></strong></span></label>
			<?php endif; ?>
			<div class="mpab-presentation-card__body" data-mpab-box-body>
			<p class="mpab-admin-card__eyebrow"><?php echo esc_html( $label ); ?></p>
			<label for="mpab-<?php echo esc_attr( $prefix ); ?>"><?php esc_html_e( 'Client-facing label', 'mark-pacura-album-builder' ); ?></label>
			<input class="widefat" id="mpab-<?php echo esc_attr( $prefix ); ?>" type="text" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[<?php echo esc_attr( $label_key ); ?>]" value="<?php echo esc_attr( $settings[ $label_key ] ); ?>">
			<?php if ( $has_price ) : ?>
			<label><?php esc_html_e( 'WB reference price', 'mark-pacura-album-builder' ); ?></label>
			<span class="mpab-wb-price" data-mpab-wb-price data-gbp="40" data-eur="48" data-usd="56"><?php echo esc_html( $this->currency_symbol( $settings['currency'] ) . number_format( self::wb_box_price( $settings['currency'] ), 2, '.', '' ) ); ?></span>
			<label for="mpab-<?php echo esc_attr( $prefix ); ?>-price"><?php esc_html_e( 'Client price', 'mark-pacura-album-builder' ); ?></label>
			<label class="mpab-money-input"><span data-mpab-currency-symbol><?php echo esc_html( $this->currency_symbol( $settings['currency'] ) ); ?></span><input class="small-text" id="mpab-<?php echo esc_attr( $prefix ); ?>-price" min="0" step="0.01" type="number" name="<?php echo esc_attr( self::OPTION_KEY ); ?>[<?php echo esc_attr( $price_key ); ?>]" value="<?php echo esc_attr( $settings[ $price_key ] ); ?>"></label>
			<?php endif; ?>
			</div>
		</div>
		<?php
	}

	/**
	 * Return the Studio-style proportional size diagram for size options.
	 *
	 * @param string $prefix Option prefix.
	 * @return string
	 */
	private function thumbnail_variant( $prefix ) {
		foreach ( self::size_definitions() as $album ) {
			foreach ( $album['sizes'] as $size ) {
				if ( $prefix === $size['prefix'] ) {
					return $size['thumb'];
				}
			}
		}
		return '';
	}

	/**
	 * Send one of the two guided test messages to the photographer.
	 *
	 * @return void
	 */
	public function send_test_email() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have permission to send this test email.', 'mark-pacura-album-builder' ) );
		}
		check_admin_referer( 'mpab_send_test_email' );
		$step = isset( $_POST['step'] ) && 'client' === sanitize_key( wp_unslash( $_POST['step'] ) ) ? 'client' : 'owner';
		$settings = $this->settings();

		if ( isset( $_POST['photographer_email'], $_POST['sender_name'] ) ) {
			$email = sanitize_email( wp_unslash( $_POST['photographer_email'] ) );
			$name  = $this->truncate( sanitize_text_field( wp_unslash( $_POST['sender_name'] ) ), 120 );
			if ( ! is_email( $email ) || '' === $name ) {
				wp_die( esc_html__( 'Please enter your business name and a valid order email address.', 'mark-pacura-album-builder' ) );
			}
			$address_changed = $email !== sanitize_email( $settings['notification_email'] );
			$saved_settings = get_option( self::OPTION_KEY, array() );
			$saved_settings = is_array( $saved_settings ) ? $saved_settings : array();
			$saved_settings['notification_email'] = $email;
			$saved_settings['email_sender_name'] = $name;
			update_option( self::OPTION_KEY, $saved_settings, false );
			$settings['notification_email'] = $email;
			$settings['email_sender_name'] = $name;
			if ( $address_changed ) {
				delete_option( self::EMAIL_TEST_STATE );
			}
		}

		$to = sanitize_email( $settings['notification_email'] );
		$state = $this->email_test_state( $settings );
		if ( 'client' === $step && empty( $state['owner_confirmed'] ) ) {
			wp_die( esc_html__( 'Please confirm receipt of the test order before sending the client confirmation test.', 'mark-pacura-album-builder' ) );
		}
		$sample = $this->sample_email_order( $settings );
		$headers = $this->email_headers( $to, $settings['email_sender_name'], $to, $settings['email_sender_name'] );
		if ( 'client' === $step ) {
			$body = $this->client_email_html( $sample['reference'], 'Alex & Sam', $sample['configuration'], $settings );
			$subject = '[TEST] ' . sprintf( __( 'Client order confirmation — %s', 'mark-pacura-album-builder' ), $sample['reference'] );
		} else {
			$body = $this->owner_email_html( $sample['reference'], 'Alex & Sam', 'alex@example.com', 'Please call before sending the album to production.', $sample['configuration'], $settings, admin_url( 'edit.php?post_type=' . self::POST_TYPE ) );
			$subject = '[TEST] ' . sprintf( __( 'New Studio order: %s', 'mark-pacura-album-builder' ), $sample['reference'] );
		}
		$sent = is_email( $to ) && $this->deliver_mail( $to, $subject, $body, $headers );
		$state['email'] = $to;
		$state['last_step'] = $step;
		$state[ $step . '_sent' ] = (bool) $sent;
		$state['failed'] = ! $sent;
		$state['error_code'] = $sent ? '' : 'send_failed';
		update_option( self::EMAIL_TEST_STATE, $state, false );
		$this->redirect_to_email_setup();
	}

	/**
	 * Record the photographer's real inbox result for one test message.
	 *
	 * @return void
	 */
	public function confirm_test_email() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have permission to confirm this test email.', 'mark-pacura-album-builder' ) );
		}
		check_admin_referer( 'mpab_confirm_test_email' );
		$step = isset( $_POST['step'] ) && 'client' === sanitize_key( wp_unslash( $_POST['step'] ) ) ? 'client' : 'owner';
		$result = isset( $_POST['result'] ) ? sanitize_key( wp_unslash( $_POST['result'] ) ) : 'missing';
		$settings = $this->settings();
		$state = $this->email_test_state( $settings );
		if ( empty( $state[ $step . '_sent' ] ) ) {
			wp_die( esc_html__( 'Please send the test email before attempting confirmation.', 'mark-pacura-album-builder' ) );
		}
		$state['last_step'] = $step;
		if ( 'received' === $result ) {
			$state[ $step . '_confirmed' ] = true;
			$state['failed'] = false;
			$state['error_code'] = '';
		} else {
			$state[ $step . '_sent' ] = false;
			$state['failed'] = true;
			$state['error_code'] = 'missing';
		}
		update_option( self::EMAIL_TEST_STATE, $state, false );
		$this->redirect_to_email_setup();
	}

	/** Redirect back to the email setup card after a test action. */
	private function redirect_to_email_setup() {
		wp_safe_redirect(
			add_query_arg(
				array(
					'page'             => 'mpab-albums',
					'mpab_email_setup' => 1,
				),
				admin_url( 'admin.php' )
			) . '#mpab-email-setup'
		);
		exit;
	}

	/**
	 * Register public submission endpoint. The callback performs its own nonce,
	 * spam and allow-list checks because clients are normally logged out.
	 *
	 * @return void
	 */
	public function register_routes() {
		register_rest_route(
			'mpab/v1',
			'/submissions',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'receive_submission' ),
				'permission_callback' => '__return_true',
			)
		);
	}

	/**
	 * Return a public form token that remains valid when a WordPress page is cached.
	 *
	 * Anonymous WordPress nonces can expire while the surrounding page remains in a
	 * full-page cache. The form is public, so use a site-bound HMAC for request
	 * integrity and retain the honeypot and elapsed-time checks for spam resistance.
	 *
	 * @return string
	 */
	private function submission_token() {
		return hash_hmac( 'sha256', home_url( '/' ) . '|' . self::NONCE_KEY, wp_salt( 'nonce' ) );
	}

	/**
	 * Return the address used for anonymous submission admission control.
	 *
	 * Forwarded-address headers are deliberately ignored because a public visitor
	 * can forge them unless the site's proxy configuration is independently trusted.
	 * Hosts with a trusted proxy may supply the verified client address through the
	 * `mpab_submission_client_ip` filter; Album Studio never trusts forwarded headers
	 * by itself.
	 * The address is only used to build a one-way rate-limit key and is never stored
	 * in readable form by Album Studio.
	 *
	 * @return string
	 */
	private function submission_client_ip() {
		$address = isset( $_SERVER['REMOTE_ADDR'] ) ? trim( sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) : '';
		$address = apply_filters( 'mpab_submission_client_ip', $address );
		return filter_var( $address, FILTER_VALIDATE_IP ) ? $address : 'unknown';
	}

	/**
	 * Build a site-specific, non-reversible transient key for a submission limit.
	 *
	 * @param string $scope Bucket scope.
	 * @param string $email Validated client email, when the rule uses it.
	 * @return string
	 */
	private function submission_rate_key( $scope, $email = '' ) {
		$identity = 'site' === $scope ? home_url( '/' ) : $this->submission_client_ip();
		if ( '' !== $email ) {
			$identity .= '|' . strtolower( $email );
		}
		$digest = hash_hmac( 'sha256', $scope . '|' . $identity, wp_salt( 'auth' ) );
		return 'mpab_rl_' . substr( $digest, 0, 40 );
	}

	/**
	 * Atomically increment one fixed-window bucket in the WordPress options table.
	 *
	 * A single database statement prevents concurrent requests from reading the same
	 * old transient value and both being admitted. Values contain only expiry/count
	 * integers and rate keys contain only one-way HMAC digests.
	 *
	 * @param string $key Bucket key.
	 * @param int    $now Current Unix timestamp.
	 * @return array{count:int,reset:int}
	 */
	private function increment_submission_bucket( $key, $now ) {
		global $wpdb;
		$option_name = '_mpab_rate_' . substr( sanitize_key( $key ), 0, 64 );
		$reset       = $now + self::SUBMISSION_RATE_WINDOW;
		$initial     = $reset . ':1';
		$sql = $wpdb->prepare(
			"INSERT INTO {$wpdb->options} (option_name, option_value, autoload)
			 VALUES (%s, %s, 'no')
			 ON DUPLICATE KEY UPDATE option_value = IF(
				CAST(SUBSTRING_INDEX(option_value, ':', 1) AS UNSIGNED) <= %d,
				VALUES(option_value),
				CONCAT(SUBSTRING_INDEX(option_value, ':', 1), ':', CAST(SUBSTRING_INDEX(option_value, ':', -1) AS UNSIGNED) + 1)
			 )",
			$option_name,
			$initial,
			$now
		);
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared immediately above.
		$value = (string) $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", $option_name ) );
		if ( 1 === wp_rand( 1, 100 ) ) {
			$wpdb->query(
				$wpdb->prepare(
					"DELETE FROM {$wpdb->options}
					 WHERE option_name LIKE %s
					 AND CAST(SUBSTRING_INDEX(option_value, ':', 1) AS UNSIGNED) <= %d
					 LIMIT 100",
					$wpdb->esc_like( '_mpab_rate_' ) . '%',
					$now
				)
			); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared immediately above.
		}
		$parts = array_map( 'absint', explode( ':', $value, 2 ) );
		return array(
			'reset' => isset( $parts[0] ) && $parts[0] > $now ? $parts[0] : $reset,
			'count' => isset( $parts[1] ) ? $parts[1] : 1,
		);
	}

	/**
	 * Atomically reserve an idempotency key, recovering abandoned attempts after an hour.
	 *
	 * @param string $idempotency_key Valid 32-character hexadecimal key.
	 * @return bool
	 */
	private function reserve_submission_attempt( $idempotency_key ) {
		global $wpdb;
		$option_name = '_mpab_idem_' . $idempotency_key;
		$pending = 'pending:' . time();
		if ( add_option( $option_name, $pending, '', false ) ) {
			return true;
		}
		$current = (string) get_option( $option_name, '' );
		if ( 0 !== strpos( $current, 'pending:' ) || absint( substr( $current, 8 ) ) > time() - HOUR_IN_SECONDS ) {
			return false;
		}
		$updated = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND option_value = %s",
				$pending,
				$option_name,
				$current
			)
		); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared immediately above.
		return 1 === (int) $updated;
	}

	/**
	 * Limit repeated valid order attempts before an order is written or mailed.
	 *
	 * A client/email combination may try five times in fifteen minutes. Higher
	 * address and site-wide ceilings stop simple email/IP rotation. The counters use
	 * an atomic database increment so parallel requests cannot bypass the limit.
	 *
	 * @param string $email Validated client email.
	 * @return true|WP_Error
	 */
	private function enforce_submission_rate_limit( $email ) {
		$now = time();
		$rules = array(
			array(
				'key'   => $this->submission_rate_key( 'identity', $email ),
				'limit' => self::SUBMISSION_RATE_LIMIT,
			),
			array(
				'key'   => $this->submission_rate_key( 'address' ),
				'limit' => self::SUBMISSION_IP_LIMIT,
			),
			array(
				'key'   => $this->submission_rate_key( 'site' ),
				'limit' => max( 1, absint( apply_filters( 'mpab_submission_site_limit', self::SUBMISSION_SITE_LIMIT ) ) ),
			),
		);

		foreach ( $rules as $rule ) {
			$bucket = $this->increment_submission_bucket( $rule['key'], $now );
			if ( (int) $bucket['count'] > (int) $rule['limit'] ) {
				$retry_after = max( 1, (int) $bucket['reset'] - $now );
				$minutes = max( 1, (int) ceil( $retry_after / MINUTE_IN_SECONDS ) );
				return new WP_Error(
					'mpab_rate_limited',
					sprintf(
						_n(
							'Too many requests detected. Your settings are preserved—please wait %d minute before trying again.',
							'Too many requests detected. Your settings are preserved—please wait %d minutes before trying again.',
							$minutes,
							'mark-pacura-album-builder'
						),
						$minutes
					),
					array(
						'status'      => 429,
						'retry_after' => $retry_after,
					)
				);
			}
		}

		return true;
	}

	/**
	 * Render the form only; all surrounding page content remains theme-owned.
	 *
	 * @return string
	 */
	private function builder_script_messages() {
		return array(
			'chooseAlbum'             => __( 'Choose an album', 'mark-pacura-album-builder' ),
			'notChosen'               => __( 'Not chosen yet', 'mark-pacura-album-builder' ),
			'pageSingular'            => __( '%d page', 'mark-pacura-album-builder' ),
			'pagePlural'              => __( '%d pages', 'mark-pacura-album-builder' ),
			'yourAlbum'               => __( 'Your album', 'mark-pacura-album-builder' ),
			'yourChosenCover'         => __( 'Your chosen cover', 'mark-pacura-album-builder' ),
			'albumType'               => __( 'Album type', 'mark-pacura-album-builder' ),
			'notSelected'             => __( 'Not selected', 'mark-pacura-album-builder' ),
			'size'                    => __( 'Size', 'mark-pacura-album-builder' ),
			'spreads'                 => __( 'Spreads', 'mark-pacura-album-builder' ),
			'spreadsWithPages'        => __( '%1$d (%2$d pages)', 'mark-pacura-album-builder' ),
			'coverMaterial'           => __( 'Cover material', 'mark-pacura-album-builder' ),
			'cover'                   => __( 'Cover', 'mark-pacura-album-builder' ),
			'personalisation'         => __( 'Personalisation', 'mark-pacura-album-builder' ),
			'personalisationStyle'    => __( 'Personalisation style', 'mark-pacura-album-builder' ),
			'wording'                 => __( 'Wording', 'mark-pacura-album-builder' ),
			'notEntered'              => __( 'Not entered', 'mark-pacura-album-builder' ),
			'font'                    => __( 'Font', 'mark-pacura-album-builder' ),
			'design'                  => __( 'Design', 'mark-pacura-album-builder' ),
			'foilColour'              => __( 'Foil colour', 'mark-pacura-album-builder' ),
			'presentation'            => __( 'Presentation', 'mark-pacura-album-builder' ),
			'woodenBoxColour'         => __( 'Wooden box colour', 'mark-pacura-album-builder' ),
			'albumAndSpreads'         => __( 'Album · %d spreads', 'mark-pacura-album-builder' ),
			'presentationBox'         => __( 'Presentation box', 'mark-pacura-album-builder' ),
			'saving'                  => __( '%d%% saving', 'mark-pacura-album-builder' ),
			'total'                   => __( 'Total', 'mark-pacura-album-builder' ),
			'youSave'                 => __( 'You save', 'mark-pacura-album-builder' ),
			'offerEnds'               => __( 'Offer ends', 'mark-pacura-album-builder' ),
			'priceBreakdown'          => __( 'Price breakdown', 'mark-pacura-album-builder' ),
			'edit'                    => __( 'Edit', 'mark-pacura-album-builder' ),
			'stepProgress'            => __( 'Step %1$d of %2$d', 'mark-pacura-album-builder' ),
			'chooseBeforeContinuing'  => __( 'Please select an option before continuing.', 'mark-pacura-album-builder' ),
			'confirmBeforeSending'    => __( 'Please confirm that I may process your album choices.', 'mark-pacura-album-builder' ),
			'chooseSpreads'           => __( 'Choose the number of spreads.', 'mark-pacura-album-builder' ),
			'validEmail'              => __( 'Please enter a valid email address.', 'mark-pacura-album-builder' ),
			'completeDetails'         => __( 'Please complete all required fields.', 'mark-pacura-album-builder' ),
			'sending'                 => __( 'Sending your choices…', 'mark-pacura-album-builder' ),
			'demoSaved'               => __( 'Your album choices have been saved.', 'mark-pacura-album-builder' ),
			'requestFailed'           => __( 'Your request could not be sent. Please check your connection and try again.', 'mark-pacura-album-builder' ),
			'submitted'               => __( 'Album choices submitted', 'mark-pacura-album-builder' ),
			'thankYou'                => __( 'Thank you. Your album choices are safely saved.', 'mark-pacura-album-builder' ),
			'orderNumber'             => __( 'Order number: %s', 'mark-pacura-album-builder' ),
			'confirmationSent'        => __( 'A confirmation has also been sent to your email address.', 'mark-pacura-album-builder' ),
			'confirmationRetrying'    => __( 'Your choices are saved. The email confirmation is being retried.', 'mark-pacura-album-builder' ),
			'tryAgain'                => __( 'Please refresh the page and try again.', 'mark-pacura-album-builder' ),
			'sendChoices'             => __( 'Send my album choices', 'mark-pacura-album-builder' ),
			'addBasket'               => __( 'Add to basket and order more', 'mark-pacura-album-builder' ),
			'updateItem'              => __( 'Update item', 'mark-pacura-album-builder' ),
		);
	}

	/** Shared basket copy used by both the Album and Wall Art journeys. */
	public function studio_basket_script_messages() {
		return array(
			'orderSoFar' => __( 'Your order so far', 'mark-pacura-album-builder' ),
			'reviewItems' => __( 'Review every item before confirming the complete order.', 'mark-pacura-album-builder' ),
			'copies' => __( 'Copies: %d', 'mark-pacura-album-builder' ),
			'orderTotal' => __( 'Order total', 'mark-pacura-album-builder' ),
			'orderEmpty' => __( 'Your order is empty.', 'mark-pacura-album-builder' ),
			'editItem' => __( 'Edit', 'mark-pacura-album-builder' ),
			'removeItem' => __( 'Remove', 'mark-pacura-album-builder' ),
		);
	}

	public function render_builder() {
		static $render_count = 0;
		$render_count++;
		$instance = 'mpab-builder-' . $render_count;
		$settings = $this->settings();
		$promotion = $this->promotion( $settings );
		$studio_voice_is_we = 'we' === $this->site_client_voice();
		if ( ! $this->email_setup_complete( $settings ) ) {
			return '<p class="mpab-builder-unavailable">' . esc_html__( 'Finishing touches are being added to these album options. Please check back shortly.', 'mark-pacura-album-builder' ) . '</p>';
		}

		wp_enqueue_style( 'mpab-thumbnails', MPAB_URL . 'assets/css/thumbnails.css', array(), MPAB_VERSION );
		wp_enqueue_style( 'mpab-builder', MPAB_URL . 'assets/css/builder.css', array( 'mpab-thumbnails' ), MPAB_VERSION );
		wp_enqueue_script( 'wb-studio-basket', MPAB_URL . 'assets/js/studio-basket.js', array(), MPAB_VERSION, true );
		wp_enqueue_script( 'mpab-builder', MPAB_URL . 'assets/js/builder.js', array( 'wb-studio-basket' ), MPAB_VERSION, true );
		wp_localize_script( 'wb-studio-basket', 'WB_STUDIO_BASKET_I18N', $this->studio_basket_script_messages() );
		wp_localize_script(
			'mpab-builder',
			'MPAB_DATA',
			array(
				'endpoint' => esc_url_raw( rest_url( 'mpab/v1/submissions' ) ),
				'token'    => $this->submission_token(),
				'locale'   => $this->normalise_locale( function_exists( 'determine_locale' ) ? determine_locale() : get_locale() ),
				'messages' => $this->builder_script_messages(),
				'successContact' => $studio_voice_is_we
					? __( 'We will contact you directly to confirm production, delivery, and payment details.', 'mark-pacura-album-builder' )
					: __( 'I will contact you directly to confirm production, delivery, and payment details.', 'mark-pacura-album-builder' ),
				'successFollowUp' => $studio_voice_is_we
					? sprintf( __( 'If we have not been in touch within 48 hours, please email us at %s.', 'mark-pacura-album-builder' ), $settings['notification_email'] )
					: sprintf( __( 'If I have not been in touch within 48 hours, please email me at %s.', 'mark-pacura-album-builder' ), $settings['notification_email'] ),
				'demo'     => false,
			)
		);

		$options = $this->catalogue( $settings );
		if ( ! $this->offer_is_viable( $options ) ) {
			return '<p class="mpab-builder-unavailable">' . esc_html__( 'This album collection is currently being prepared. Please check back shortly.', 'mark-pacura-album-builder' ) . '</p>';
		}
		$step_total = empty( $options['boxes'] ) ? 4 : 5;
		ob_start();
		?>
		<section
			class="mpab-builder"
			data-currency="<?php echo esc_attr( $this->currency_symbol( $settings['currency'] ) ); ?>"
			data-show-prices="<?php echo $settings['show_prices'] ? '1' : '0'; ?>"
			data-promotion-active="<?php echo $promotion['active'] ? '1' : '0'; ?>"
			data-promotion-percent="<?php echo esc_attr( $promotion['active'] ? $promotion['percent'] : 0 ); ?>"
			data-promotion-end-date="<?php echo esc_attr( $promotion['active'] ? $promotion['end_date'] : '' ); ?>"
			data-promotion-end-label="<?php echo esc_attr( $promotion['active'] ? $promotion['end_label'] : '' ); ?>"
			data-site-date="<?php echo esc_attr( current_datetime()->format( 'Y-m-d' ) ); ?>"
			style="--mpab-accent:<?php echo esc_attr( $settings['accent_colour'] ); ?>"
		>
			<div class="mpab-builder__progress" aria-label="<?php esc_attr_e( 'Your progress', 'mark-pacura-album-builder' ); ?>">
				<span class="mpab-builder__progress-text" aria-live="polite"><?php echo esc_html( sprintf( __( 'Step 1 of %d', 'mark-pacura-album-builder' ), $step_total ) ); ?></span>
				<span class="mpab-builder__progress-track"><span></span></span>
			</div>
			<?php if ( $promotion['active'] ) : ?>
			<aside class="mpab-promotion" aria-label="<?php esc_attr_e( 'Special album offer', 'mark-pacura-album-builder' ); ?>">
				<strong><?php echo esc_html( sprintf( __( 'Enjoy %d%% off your album', 'mark-pacura-album-builder' ), $promotion['percent'] ) ); ?></strong>
				<p><?php echo esc_html( $promotion['end_label'] ? sprintf( __( 'Confirm your choices by %s to apply your promotional saving automatically at the final review.', 'mark-pacura-album-builder' ), $promotion['end_label'] ) : __( 'Your promotional saving will be applied automatically at the final review.', 'mark-pacura-album-builder' ) ); ?></p>
			</aside>
			<?php endif; ?>
			<form class="mpab-form" novalidate>
				<div class="mpab-step" data-step="0">
					<fieldset>
						<legend><?php esc_html_e( 'Choose your album style', 'mark-pacura-album-builder' ); ?></legend>
						<p class="mpab-step__intro"><?php esc_html_e( 'Choose the album style you love most. You can select your ideal size in the next step.', 'mark-pacura-album-builder' ); ?></p>
						<div class="mpab-options mpab-options--album-types">
						<?php foreach ( $options['album_types'] as $album_type ) : ?>
							<?php $this->render_option( 'album_type', $album_type, false, $settings, false ); ?>
						<?php endforeach; ?>
						</div>
						<?php foreach ( $options['album_types'] as $album_id => $album_type ) : ?>
						<div class="mpab-subchoice" data-mpab-branch="album_type" data-mpab-value="<?php echo esc_attr( $album_id ); ?>" hidden>
							<h3><?php echo esc_html( sprintf( __( '%s sizes', 'mark-pacura-album-builder' ), $album_type['label'] ) ); ?></h3>
							<div class="mpab-options mpab-options--sizes">
							<?php foreach ( $options['sizes'][ $album_id ] as $size ) : ?>
								<?php $this->render_option( 'size', $size, false, $settings ); ?>
							<?php endforeach; ?>
							</div>
						</div>
						<?php endforeach; ?>
						<div class="mpab-spreads">
							<label for="mpab-spreads-<?php echo esc_attr( $instance ); ?>"><?php esc_html_e( 'How much space do you need for your story?', 'mark-pacura-album-builder' ); ?></label>
							<p><?php esc_html_e( 'Note: 1 spread equals 2 pages. Every album starts with a classic foundation of 10 spreads (20 pages).', 'mark-pacura-album-builder' ); ?></p>
							<select id="mpab-spreads-<?php echo esc_attr( $instance ); ?>" name="spreads" required>
								<option value=""><?php esc_html_e( 'Choose the number of spreads', 'mark-pacura-album-builder' ); ?></option>
								<?php for ( $spread_count = 10; $spread_count <= 40; $spread_count++ ) : ?>
									<option value="<?php echo esc_attr( $spread_count ); ?>"><?php echo esc_html( sprintf( __( '%1$d spreads (%2$d pages)', 'mark-pacura-album-builder' ), $spread_count, $spread_count * 2 ) ); ?></option>
								<?php endfor; ?>
							</select>
						</div>
					</fieldset>
				</div>

				<div class="mpab-step" data-step="1" hidden>
					<fieldset>
						<legend><?php esc_html_e( 'Choose a beautiful cover', 'mark-pacura-album-builder' ); ?></legend>
						<p class="mpab-step__intro"><?php esc_html_e( 'Select a cover material you love. All available colour options will display below.', 'mark-pacura-album-builder' ); ?></p>
						<div class="mpab-options mpab-options--four mpab-options--cover-materials">
						<?php foreach ( $options['cover_materials'] as $material ) : ?>
							<?php $this->render_option( 'cover_material', $material, false, $settings, false ); ?>
						<?php endforeach; ?>
						</div>
						<?php foreach ( $options['cover_materials'] as $material_id => $material ) : ?>
						<div class="mpab-subchoice" data-mpab-branch="cover_material" data-mpab-value="<?php echo esc_attr( $material_id ); ?>" hidden>
							<h3><?php echo esc_html( sprintf( __( '%s colours', 'mark-pacura-album-builder' ), $material['label'] ) ); ?></h3>
							<div class="mpab-options mpab-options--swatches">
							<?php foreach ( $options['covers'][ $material_id ] as $cover ) : ?>
								<?php $this->render_option( 'cover', $cover, false, $settings ); ?>
							<?php endforeach; ?>
							</div>
						</div>
						<?php endforeach; ?>
					</fieldset>
				</div>

				<div class="mpab-step" data-step="2" hidden>
					<fieldset>
						<legend><?php esc_html_e( 'Would you like to add a personal touch?', 'mark-pacura-album-builder' ); ?></legend>
						<p class="mpab-step__intro"><?php esc_html_e( 'Personalize your album. The choices below automatically adapt to your selected cover.', 'mark-pacura-album-builder' ); ?></p>
						<?php foreach ( $options['cover_materials'] as $material_id => $material ) : ?>
						<div data-mpab-branch="cover_material" data-mpab-value="<?php echo esc_attr( $material_id ); ?>" hidden>
							<div class="mpab-options mpab-options--personalisation-methods">
								<?php $this->render_option( 'personalisation', $options['personalisation']['none'], false, $settings, false ); ?>
								<?php foreach ( $options['personalisation'] as $method_id => $method ) : ?>
									<?php if ( 'none' !== $method_id && in_array( $material_id, $method['families'], true ) ) { $this->render_option( 'personalisation', $method, false, $settings, false ); } ?>
								<?php endforeach; ?>
							</div>
						</div>
						<?php endforeach; ?>
						<?php if ( isset( $options['personalisation']['foil'] ) ) { $this->render_personalisation_branch( 'foil', $options, $settings ); } ?>
						<?php if ( isset( $options['personalisation']['engraving'] ) ) { $this->render_personalisation_branch( 'engraving', $options, $settings ); } ?>
					</fieldset>
				</div>

				<?php if ( ! empty( $options['boxes'] ) ) : ?>
				<div class="mpab-step" data-step="3" hidden>
					<fieldset>
						<legend><?php esc_html_e( 'Would you like a matching presentation box?', 'mark-pacura-album-builder' ); ?></legend>
						<p class="mpab-step__intro"><?php esc_html_e( 'Protect your album with a handcrafted presentation box, designed to keep your memories safe for generations.', 'mark-pacura-album-builder' ); ?></p>
						<div class="mpab-options mpab-options--presentation">
				<?php $this->render_option( 'box', array( 'id' => 'none', 'label' => __( 'Just the album, please', 'mark-pacura-album-builder' ), 'price' => 0, 'image' => '', 'thumb' => '' ), false, $settings ); ?>
							<?php foreach ( $options['boxes'] as $box_option ) : ?>
								<?php $this->render_option( 'box', $box_option, false, $settings ); ?>
							<?php endforeach; ?>
						</div>
						<?php if ( isset( $options['boxes']['wooden'] ) ) : ?>
						<div class="mpab-subchoice" data-mpab-branch="box" data-mpab-value="wooden" hidden>
							<h3><?php esc_html_e( 'Choose a lovely finish for your wooden box', 'mark-pacura-album-builder' ); ?></h3>
							<div class="mpab-options mpab-options--three mpab-options--box-colours">
							<?php foreach ( $options['box_colours'] as $box_colour ) : ?>
								<?php $this->render_option( 'box_colour', $box_colour, false, $settings, false ); ?>
							<?php endforeach; ?>
							</div>
						</div>
						<?php endif; ?>
					</fieldset>
				</div>
				<?php endif; ?>

				<div class="mpab-step" data-step="4" hidden>
					<header class="mpab-review-intro">
						<span class="mpab-review-intro__eyebrow"><?php esc_html_e( 'Almost there! Let’s review your choices', 'mark-pacura-album-builder' ); ?></span>
						<h2><?php esc_html_e( 'Review your album and send your choices', 'mark-pacura-album-builder' ); ?></h2>
						<p><?php esc_html_e( 'Review your choices and final summary below. When you are ready, click Confirm order to complete your request.', 'mark-pacura-album-builder' ); ?></p>
					</header>
					<div class="wb-studio-basket" data-mpab-basket-summary hidden></div>
					<div class="mpab-review-grid">
						<section class="mpab-review-card" aria-labelledby="mpab-review-choices-<?php echo esc_attr( $instance ); ?>">
							<div class="mpab-review-product">
								<div class="mpab-review-product__shape" data-mpab-review-shape></div>
								<div><span data-mpab-review-album><?php esc_html_e( 'Your album', 'mark-pacura-album-builder' ); ?></span><strong data-mpab-review-cover><?php esc_html_e( 'Your chosen cover', 'mark-pacura-album-builder' ); ?></strong></div>
							</div>
							<div class="mpab-review-card__details">
							<div class="mpab-review-price">
								<?php if ( $promotion['active'] ) : ?>
								<del data-mpab-normal-total>—</del>
								<?php endif; ?>
								<strong data-mpab-final-total>—</strong>
								<?php if ( $promotion['active'] ) : ?>
								<small class="mpab-review-price__saving" data-mpab-promotion-saving><?php echo esc_html( $promotion['end_label'] ? sprintf( __( 'Your saving will be shown here. Offer ends %s.', 'mark-pacura-album-builder' ), $promotion['end_label'] ) : __( 'Your saving will be shown here.', 'mark-pacura-album-builder' ) ); ?></small>
								<?php endif; ?>
							</div>
							<h3 id="mpab-review-choices-<?php echo esc_attr( $instance ); ?>"><?php esc_html_e( 'Your album choices', 'mark-pacura-album-builder' ); ?></h3>
							<div class="mpab-summary" aria-live="polite"></div>
							<label class="mpab-order-quantity"><span><?php esc_html_e( 'Number of copies', 'mark-pacura-album-builder' ); ?></span><input type="number" name="quantity" min="1" max="20" value="1" required></label>
							</div>
						</section>
						<section class="mpab-contact" data-mpab-contact hidden aria-labelledby="mpab-review-details-<?php echo esc_attr( $instance ); ?>">
							<header class="mpab-contact__header">
								<span><?php esc_html_e( 'Your details', 'mark-pacura-album-builder' ); ?></span>
								<h3 id="mpab-review-details-<?php echo esc_attr( $instance ); ?>"><?php esc_html_e( 'Where should we send your confirmation?', 'mark-pacura-album-builder' ); ?></h3>
								<p><?php echo esc_html( $studio_voice_is_we ? __( 'We’ll email you a summary of everything you’ve chosen and keep a copy too.', 'mark-pacura-album-builder' ) : __( 'I’ll email you a summary of everything you’ve chosen and keep a copy too.', 'mark-pacura-album-builder' ) ); ?></p>
							</header>
							<label><?php esc_html_e( 'Your names', 'mark-pacura-album-builder' ); ?><input type="text" name="client_name" autocomplete="name" maxlength="120" required></label>
							<label><?php esc_html_e( 'Email address', 'mark-pacura-album-builder' ); ?><input type="email" name="client_email" autocomplete="email" maxlength="190" required></label>
							<div class="mpab-order-confirmation">
								<strong><?php esc_html_e( 'Ready to send your choices?', 'mark-pacura-album-builder' ); ?></strong>
								<label class="mpab-consent"><input type="checkbox" name="consent" value="1" required> <span><?php esc_html_e( 'I’m happy with my choices and the total shown! I understand the photographer will be in touch shortly to confirm payment, production, and delivery.', 'mark-pacura-album-builder' ); ?></span></label>
							</div>
							<label class="mpab-trap" aria-hidden="true">Website<input type="text" name="website" tabindex="-1" autocomplete="off"></label>
							<input type="hidden" name="started_at" value="">
						</section>
					</div>
				</div>

				<div class="mpab-error" role="alert" tabindex="-1" hidden></div>
				<div class="mpab-actions">
					<button class="mpab-button mpab-button--back" type="button" hidden><?php esc_html_e( 'Go back', 'mark-pacura-album-builder' ); ?></button>
					<button class="mpab-button mpab-button--next" type="button"><?php esc_html_e( 'Continue', 'mark-pacura-album-builder' ); ?></button>
					<button class="mpab-button mpab-button--add-basket" type="button" hidden><?php esc_html_e( 'Add to basket and order more', 'mark-pacura-album-builder' ); ?></button>
					<button class="mpab-button mpab-button--show-details" type="button" hidden><?php esc_html_e( 'Confirm order', 'mark-pacura-album-builder' ); ?></button>
					<button class="mpab-button mpab-button--submit" type="submit" disabled hidden><?php esc_html_e( 'Send my album choices', 'mark-pacura-album-builder' ); ?></button>
				</div>
				<aside class="mpab-album-so-far" data-mpab-album-so-far hidden aria-label="<?php esc_attr_e( 'Your album so far', 'mark-pacura-album-builder' ); ?>" aria-live="polite">
					<div class="mpab-album-so-far__body">
						<div class="mpab-album-miniature"><span data-mpab-progress-shape></span><small data-mpab-progress-album><?php esc_html_e( 'Choose an album', 'mark-pacura-album-builder' ); ?></small></div>
						<dl>
							<div><dt><?php esc_html_e( 'Material', 'mark-pacura-album-builder' ); ?></dt><dd data-mpab-progress-material>—</dd></div>
							<div><dt><?php esc_html_e( 'Colour', 'mark-pacura-album-builder' ); ?></dt><dd><span class="mpab-progress-swatch" data-mpab-progress-swatch hidden></span><span data-mpab-progress-colour>—</span></dd></div>
							<div><dt><?php esc_html_e( 'Spreads', 'mark-pacura-album-builder' ); ?></dt><dd data-mpab-progress-spreads>—</dd></div>
							<div><dt><?php esc_html_e( 'Personalisation', 'mark-pacura-album-builder' ); ?></dt><dd data-mpab-progress-personalisation>—</dd></div>
						</dl>
					</div>
				</aside>
			</form>
			<div class="mpab-success" role="status" tabindex="-1" hidden></div>
		</section>
		<?php
		return ob_get_clean();
	}

	/**
	 * Return the limited allow-listed catalogue.
	 *
	 * @param array $settings Plugin settings.
	 * @return array<string,mixed>
	 */
	private function catalogue_translation( $text, $context ) {
		return translate_with_gettext_context( (string) $text, (string) $context, 'mark-pacura-album-builder' );
	}

	private function catalogue( $settings ) {
		$cover_source = self::cover_source();
		$personal     = self::personalisation_source();
		$materials    = array();
		$covers       = array();
		$methods      = array(
			'none' => array( 'id' => 'none', 'label' => __( 'Keep it plain', 'mark-pacura-album-builder' ), 'price' => 0, 'families' => array( 'ds', 'sv', 'cl', 'cn' ) ),
		);
		$fonts        = array();
		$designs      = array();
		$foil_colours = array();
		$album_types  = array();
		$sizes        = array();
		$boxes        = array();
		$material_images = array(
			'DS' => 'dolomiti-suede.jpg',
			'SV' => 'selfoss-velvet.jpg',
			'CL' => 'canary-leather.jpg',
			'CN' => 'cali-natural.jpg',
		);
		$material_descriptions = array(
			'DS' => __( 'Soft to the touch with a warm, relaxed finish. Perfect for bringing portrait and wedding stories to life.', 'mark-pacura-album-builder' ),
			'SV' => __( 'A rich, exceptionally soft velvet that catches the light beautifully. A truly luxurious statement piece.', 'mark-pacura-album-builder' ),
			'CL' => __( 'Characterful and timeless with a softly textured feel. An elegant, enduring choice for your story.', 'mark-pacura-album-builder' ),
			'CN' => __( 'Clean and understated with a natural woven feel. A wonderful choice if you love a modern, organic aesthetic.', 'mark-pacura-album-builder' ),
		);

		foreach ( self::size_definitions() as $album_id => $album ) {
			if ( ! in_array( $album_id, $settings['enabled_album_types'], true ) ) {
				continue;
			}
			$album_types[ $album_id ] = $this->catalogue_option( $album_id, $settings[ $album['label_setting'] ], 0, $settings, $album['label_setting'] );
			$album_types[ $album_id ]['image'] = MPAB_URL . 'assets/images/albums/' . ( 'fine_art' === $album_id ? 'fine-art-closed.webp' : 'photo-album-closed.webp' );
			$album_types[ $album_id ]['thumb'] = '';
			$sizes[ $album_id ] = array();
			foreach ( $album['sizes'] as $size ) {
				if ( ! in_array( $size['id'], $settings['enabled_sizes'], true ) ) {
					continue;
				}
				$sizes[ $album_id ][ $size['code'] ] = $this->catalogue_option( $size['id'], $settings[ $size['prefix'] . '_label' ], $settings[ $size['prefix'] . '_price' ], $settings, $size['prefix'] );
			}
			if ( empty( $sizes[ $album_id ] ) ) {
				unset( $album_types[ $album_id ], $sizes[ $album_id ] );
			}
		}

		foreach ( $cover_source['cover_families'] as $family ) {
			$family_id = strtolower( $family['family_code'] );
			if ( ! in_array( $family['family_code'], $settings['enabled_cover_families'], true ) ) {
				continue;
			}
			$materials[ $family_id ] = array(
				'id'       => $family_id,
				'code'     => $family['family_code'],
				'label'    => $family['family_name'],
				'material' => $this->catalogue_translation( $family['material'], 'Cover material type' ),
				'description' => isset( $material_descriptions[ $family['family_code'] ] ) ? $material_descriptions[ $family['family_code'] ] : '',
				'price'    => 0,
				'image'    => isset( $material_images[ $family['family_code'] ] ) ? MPAB_URL . 'assets/images/materials/' . $material_images[ $family['family_code'] ] : '',
				'thumb'    => '',
			);
			$covers[ $family_id ] = array();
		}
		foreach ( $cover_source['active_colours'] as $colour ) {
			$family_id = strtolower( $colour['family_code'] );
			if ( ! isset( $materials[ $family_id ] ) || ! in_array( $colour['colour_id'], $settings['enabled_cover_colours'], true ) ) {
				continue;
			}
			$covers[ $family_id ][] = array(
				'id'       => strtolower( $colour['colour_id'] ),
				'label'    => $colour['official_code'] . ' · ' . $this->catalogue_translation( $colour['friendly_name'], 'Cover colour' ),
				'code'     => $colour['official_code'],
				'price'    => 0,
				'image'    => $this->cover_image( $colour['official_code'] ),
				'thumb'    => '',
				'pending_image' => '' === $this->cover_image( $colour['official_code'] ),
			);
		}
		foreach ( array_keys( $materials ) as $family_id ) {
			if ( empty( $covers[ $family_id ] ) ) {
				unset( $materials[ $family_id ], $covers[ $family_id ] );
			}
		}

		foreach ( $personal['foil_colours'] as $colour ) {
			if ( ! in_array( $colour['id'], $settings['enabled_foil_colours'], true ) ) {
				continue;
			}
			$id = strtolower( $colour['id'] );
			$foil_colours[] = array( 'id' => $id, 'label' => $this->catalogue_translation( $colour['label'], 'Foil colour' ), 'price' => 0, 'image' => $this->personalisation_image( isset( $colour['thumbnail']['public_path'] ) ? $colour['thumbnail']['public_path'] : '', 'foil-colours' ), 'thumb' => '' );
		}
		foreach ( $personal['fonts'] as $font ) {
			if ( ! in_array( $font['id'], $settings['enabled_personalisation_fonts'], true ) ) {
				continue;
			}
			$id = str_replace( '.', '-', strtolower( $font['id'] ) );
			$fonts[] = array(
				'id'      => $id,
				'label'   => $font['label'],
				'price'   => 0,
				'methods' => $font['method_ids'],
				'image'   => $this->personalisation_image( isset( $font['thumbnail']['public_path'] ) ? $font['thumbnail']['public_path'] : '', 'fonts' ),
				'thumb'   => '',
			);
		}
		foreach ( $personal['design_items'] as $design ) {
			if ( empty( $design['selectable'] ) || ! in_array( $design['source_reference'], $settings['enabled_personalisation_designs'], true ) ) {
				continue;
			}
			$designs[] = array(
				'id'         => 'design-' . strtolower( $design['source_reference'] ),
				'label'      => $this->catalogue_translation( $design['display_name'], 'Personalisation design name' ),
				'price'      => 0,
				'methods'    => $design['method_ids'],
				'collection' => $design['collection_id'],
				'image'      => $this->personalisation_image( isset( $design['thumbnail']['public_path'] ) ? $design['thumbnail']['public_path'] : '', 'designs' ),
				'thumb'      => '',
			);
		}
		foreach ( $personal['methods'] as $method ) {
			if ( ! in_array( $method['id'], $settings['enabled_personalisation_methods'], true ) ) {
				continue;
			}
			$has_font = ! empty( array_filter( $fonts, function ( $font ) use ( $method ) { return in_array( $method['id'], $font['methods'], true ); } ) );
			$has_design = ! empty( array_filter( $designs, function ( $design ) use ( $method ) { return 'collection.full-cover' !== $design['collection'] && in_array( $method['id'], $design['methods'], true ); } ) );
			if ( 'foil' === $method['id'] && ( empty( $foil_colours ) || ( ! $has_font && ! $has_design ) ) ) {
				continue;
			}
			$methods[ $method['id'] ] = array(
				'id'       => $method['id'],
				'label'    => $this->catalogue_translation( $method['label'], 'Personalisation method' ),
				'price'    => 0,
				'families' => array_map( 'strtolower', $method['cover_family_ids'] ),
				'image'    => MPAB_URL . 'assets/images/personalisation/methods/' . ( 'foil' === $method['id'] ? 'foil-embossing.jpg' : 'laser-engraving.jpg' ),
				'thumb'    => '',
			);
		}
		if ( in_array( 'wooden', $settings['enabled_boxes'], true ) ) {
			$boxes['wooden'] = array_merge( $this->catalogue_option( 'wooden', $settings['box_wooden_label'], $settings['box_wooden_price'], $settings, 'box_wooden' ), array( 'image' => MPAB_URL . 'assets/images/presentation/wooden-box.jpg' ) );
		}
		if ( in_array( 'fabric', $settings['enabled_boxes'], true ) ) {
			$boxes['fabric'] = array_merge( $this->catalogue_option( 'fabric', $settings['box_fabric_label'], $settings['box_fabric_price'], $settings, 'box_fabric' ), array( 'image' => MPAB_URL . 'assets/images/presentation/fabric-box.jpg' ) );
		}

		return array(
			'album_types' => $album_types,
			'sizes' => $sizes,
			'cover_materials' => $materials,
			'covers' => $covers,
			'personalisation' => $methods,
			'personalisation_categories' => array(
				'text'       => array( 'id' => 'text', 'label' => __( 'Add your own words', 'mark-pacura-album-builder' ), 'price' => 0 ),
				'wb_design'  => array( 'id' => 'wb-design', 'label' => __( 'Choose a ready-made design', 'mark-pacura-album-builder' ), 'price' => 0 ),
				'own_design' => array( 'id' => 'own-design', 'label' => __( 'Provide your own custom artwork', 'mark-pacura-album-builder' ), 'price' => 0 ),
				'full_cover' => array( 'id' => 'full-cover', 'label' => __( 'Choose a striking full-cover design', 'mark-pacura-album-builder' ), 'price' => 0 ),
			),
			'foil_colours' => $foil_colours,
			'fonts' => $fonts,
			'designs' => $designs,
			'boxes' => $boxes,
			'box_colours' => array(
				'natural'   => array( 'id' => 'natural', 'label' => __( 'Natural', 'mark-pacura-album-builder' ), 'price' => 0, 'image' => '', 'thumb' => '' ),
				'light_oak' => array( 'id' => 'light-oak', 'label' => __( 'Light Oak', 'mark-pacura-album-builder' ), 'price' => 0, 'image' => '', 'thumb' => '' ),
				'ebony'     => array( 'id' => 'ebony', 'label' => __( 'Ebony', 'mark-pacura-album-builder' ), 'price' => 0, 'image' => '', 'thumb' => '' ),
			),
		);
	}

	/**
	 * Build one catalogue option with its Studio-matched visual when applicable.
	 *
	 * @param string $id Public identifier.
	 * @param string $label Client-facing label.
	 * @param float  $price Option price.
	 * @param array  $settings Current settings.
	 * @param string $prefix Settings prefix.
	 * @return array<string,mixed>
	 */
	private function catalogue_option( $id, $label, $price, $settings, $prefix ) {
		$spread_key   = $prefix . '_spread_price';
		return array(
			'id'    => $id,
			'label' => $label,
			'price' => (float) $price,
			'spread_price' => isset( $settings[ $spread_key ] ) ? (float) $settings[ $spread_key ] : 0,
			'image' => '',
			'thumb' => $this->thumbnail_variant( $prefix ),
		);
	}

	/**
	 * Find an allow-listed option by its public identifier.
	 *
	 * @param array  $options Available option records.
	 * @param string $id Requested identifier.
	 * @return array|null
	 */
	private function option_by_id( $options, $id ) {
		foreach ( $options as $option ) {
			if ( isset( $option['id'] ) && $id === $option['id'] ) {
				return $option;
			}
		}
		return null;
	}

	/**
	 * Render one complete progressive-disclosure personalisation method.
	 *
	 * @param string $method Method id.
	 * @param array  $options Full catalogue.
	 * @param array  $settings Current settings.
	 * @return void
	 */
	private function render_personalisation_branch( $method, $options, $settings ) {
		$studio_voice_is_we = 'we' === $this->site_client_voice();
		$method_fonts = array_filter( $options['fonts'], function ( $font ) use ( $method ) { return in_array( $method, $font['methods'], true ); } );
		$method_designs = array_filter( $options['designs'], function ( $design ) use ( $method ) { return 'collection.full-cover' !== $design['collection'] && in_array( $method, $design['methods'], true ); } );
		$method_full_designs = array_filter( $options['designs'], function ( $design ) use ( $method ) { return 'collection.full-cover' === $design['collection'] && in_array( $method, $design['methods'], true ); } );
		$category_keys = array();
		if ( ! empty( $method_fonts ) ) { $category_keys[] = 'text'; }
		if ( ! empty( $method_designs ) ) { $category_keys[] = 'wb_design'; }
		if ( 'engraving' === $method ) { $category_keys[] = 'own_design'; }
		if ( 'engraving' === $method && ! empty( $method_full_designs ) ) { $category_keys[] = 'full_cover'; }
		?>
		<div class="mpab-subchoice mpab-personalisation-branch" data-mpab-branch="personalisation" data-mpab-value="<?php echo esc_attr( $method ); ?>" hidden>
			<h3><?php esc_html_e( 'How would you like to personalise your cover?', 'mark-pacura-album-builder' ); ?></h3>
			<div class="mpab-options mpab-options--four">
			<?php foreach ( $category_keys as $category_key ) : ?>
				<?php $this->render_option( 'personalisation_category', $options['personalisation_categories'][ $category_key ], false, $settings ); ?>
			<?php endforeach; ?>
			</div>

			<?php if ( in_array( 'text', $category_keys, true ) ) : ?>
			<div class="mpab-subchoice" data-mpab-branch="personalisation_category" data-mpab-value="text" hidden>
				<h3><?php esc_html_e( 'What would you like your album to say?', 'mark-pacura-album-builder' ); ?></h3>
				<p class="mpab-choice-help mpab-personal-lines-help"><?php echo esc_html( sprintf( __( 'You can add up to %d lines of text (up to 25 characters per line). Final positioning will be confirmed before production—', 'mark-pacura-album-builder' ), (int) $settings['personalisation_text_lines'] ) . ( $studio_voice_is_we ? __( 'we will confirm the final positioning with you later.', 'mark-pacura-album-builder' ) : __( 'I will confirm the final positioning with you later.', 'mark-pacura-album-builder' ) ) ); ?></p>
				<div class="mpab-personal-lines">
				<?php for ( $line_number = 1; $line_number <= (int) $settings['personalisation_text_lines']; $line_number++ ) : ?>
					<label class="mpab-personal-text"><span><?php echo esc_html( sprintf( __( 'Line %d', 'mark-pacura-album-builder' ), $line_number ) ); ?></span><input type="text" name="personal_text_lines[]" maxlength="25" <?php echo 1 === $line_number ? 'required' : ''; ?>></label>
				<?php endfor; ?>
				</div>
				<div class="mpab-options mpab-options--font-gallery">
				<?php foreach ( $options['fonts'] as $font ) : ?>
					<?php if ( in_array( $method, $font['methods'], true ) ) { $this->render_option( 'personalisation_font', $font, false, $settings ); } ?>
				<?php endforeach; ?>
				</div>
			</div>
			<?php endif; ?>

			<?php if ( in_array( 'wb_design', $category_keys, true ) ) : ?>
			<div class="mpab-subchoice" data-mpab-branch="personalisation_category" data-mpab-value="wb-design" hidden>
				<h3><?php esc_html_e( 'Select a beautiful ready-made design', 'mark-pacura-album-builder' ); ?></h3>
				<?php $this->render_design_gallery( $method, false, $options, $settings ); ?>
			</div>
			<?php endif; ?>

			<?php if ( 'engraving' === $method ) : ?>
			<div class="mpab-subchoice mpab-own-design" data-mpab-branch="personalisation_category" data-mpab-value="own-design" hidden>
				<h3><?php esc_html_e( 'Use your own custom design', 'mark-pacura-album-builder' ); ?></h3>
				<p><?php echo esc_html( $studio_voice_is_we ? __( 'Select this option to submit custom artwork. Once your choices are sent, we will contact you to collect your design and ensure perfect engraving.', 'mark-pacura-album-builder' ) : __( 'Select this option to submit custom artwork. Once your choices are sent, I will contact you to collect your design and ensure perfect engraving.', 'mark-pacura-album-builder' ) ); ?></p>
			</div>
			<?php if ( in_array( 'full_cover', $category_keys, true ) ) : ?>
			<div class="mpab-subchoice" data-mpab-branch="personalisation_category" data-mpab-value="full-cover" hidden>
				<h3><?php esc_html_e( 'Choose a striking full-cover design', 'mark-pacura-album-builder' ); ?></h3>
				<?php $this->render_design_gallery( $method, true, $options, $settings ); ?>
			</div>
			<?php endif; ?>
			<?php endif; ?>

			<?php if ( 'foil' === $method ) : ?>
			<div class="mpab-personal-section mpab-personal-section--foil">
				<h3><?php esc_html_e( 'Choose your foil colour', 'mark-pacura-album-builder' ); ?></h3>
				<div class="mpab-options mpab-options--foil-colours">
				<?php foreach ( $options['foil_colours'] as $foil_colour ) : ?>
					<?php $this->render_option( 'foil_colour', $foil_colour, false, $settings ); ?>
				<?php endforeach; ?>
				</div>
			</div>
			<?php endif; ?>
		</div>
		<?php
	}

	/**
	 * Render the official fixed design thumbnails grouped like the Studio form.
	 *
	 * @param string $method Personalisation method.
	 * @param bool   $full_cover Whether to render only full-cover designs.
	 * @param array  $options Full catalogue.
	 * @param array  $settings Current settings.
	 * @return void
	 */
	private function render_design_gallery( $method, $full_cover, $options, $settings ) {
		$collections = $full_cover ? array( 'collection.full-cover' => __( 'Full-cover designs', 'mark-pacura-album-builder' ) ) : array(
			'collection.illustrations' => __( 'Illustrations', 'mark-pacura-album-builder' ),
			'collection.calligraphy'  => __( 'Calligraphy', 'mark-pacura-album-builder' ),
			'collection.family'       => __( 'Family', 'mark-pacura-album-builder' ),
			'collection.boudoir'      => __( 'Boudoir', 'mark-pacura-album-builder' ),
		);
		foreach ( $collections as $collection_id => $collection_label ) :
			$matching = array_filter(
				$options['designs'],
				function ( $design ) use ( $method, $collection_id ) {
					return $collection_id === $design['collection'] && in_array( $method, $design['methods'], true );
				}
			);
			if ( empty( $matching ) ) {
				continue;
			}
			?>
			<h4><?php echo esc_html( $collection_label ); ?></h4>
			<div class="mpab-options mpab-options--design-gallery">
			<?php foreach ( $matching as $design ) : ?>
				<?php $this->render_option( 'personalisation_design', $design, false, $settings ); ?>
			<?php endforeach; ?>
			</div>
			<?php
		endforeach;
	}

	/**
	 * Render one radio card.
	 *
	 * @param string $group Radio group.
	 * @param array  $option Option data.
	 * @param bool   $checked Whether checked by default.
	 * @param array  $settings Current settings.
	 * @param bool   $display_price Whether the option card should show a price.
	 * @return void
	 */
	private function render_option( $group, $option, $checked, $settings, $display_price = true ) {
		$image_url    = isset( $option['image'] ) ? (string) $option['image'] : '';
		$thumb_variant = isset( $option['thumb'] ) ? sanitize_html_class( (string) $option['thumb'] ) : '';
		$has_visual   = '' !== $image_url || '' !== $thumb_variant;
		?>
		<label class="mpab-option">
			<input type="radio" name="<?php echo esc_attr( $group ); ?>" value="<?php echo esc_attr( $option['id'] ); ?>" aria-label="<?php echo esc_attr( $option['label'] ); ?>" data-label="<?php echo esc_attr( $option['label'] ); ?>" data-price="<?php echo esc_attr( $option['price'] ); ?>" data-spread-price="<?php echo esc_attr( isset( $option['spread_price'] ) ? $option['spread_price'] : 0 ); ?>" data-image="<?php echo esc_url( $image_url ); ?>" data-thumb="<?php echo esc_attr( $thumb_variant ); ?>" <?php checked( $checked ); ?> required>
			<span class="mpab-option__body<?php echo $has_visual ? ' mpab-option__body--visual' : ''; ?>">
				<span class="mpab-option__mark" aria-hidden="true"></span>
				<?php if ( $has_visual ) : ?>
					<span class="mpab-option__media" aria-hidden="true">
					<?php if ( $image_url ) : ?>
						<img class="mpab-option__image" src="<?php echo esc_url( $image_url ); ?>" alt="" loading="lazy" decoding="sync">
					<?php else : ?>
						<span class="mpab-thumb mpab-thumb--<?php echo esc_attr( $thumb_variant ); ?>"></span>
					<?php endif; ?>
					</span>
				<?php endif; ?>
				<?php if ( ! empty( $option['pending_image'] ) ) : ?><span class="mpab-option__pending"><?php esc_html_e( 'Image coming soon', 'mark-pacura-album-builder' ); ?></span><?php endif; ?>
				<span class="mpab-option__copy">
					<?php if ( 'size' === $group ) : ?>
						<?php $size_parts = preg_split( '/\s*·\s*/', (string) $option['label'], 2 ); ?>
						<strong class="mpab-size-label"><span><?php echo esc_html( isset( $size_parts[0] ) ? $size_parts[0] : $option['label'] ); ?></span><?php if ( ! empty( $size_parts[1] ) ) : ?><span><?php echo esc_html( $size_parts[1] ); ?></span><?php endif; ?></strong>
					<?php elseif ( 'cover' === $group ) : ?>
						<?php $cover_parts = preg_split( '/\s*·\s*/', (string) $option['label'], 2 ); ?>
						<strong class="mpab-swatch-label"><span><?php echo esc_html( isset( $cover_parts[0] ) ? $cover_parts[0] : $option['label'] ); ?></span><?php if ( ! empty( $cover_parts[1] ) ) : ?><span><?php echo esc_html( $cover_parts[1] ); ?></span><?php endif; ?></strong>
					<?php else : ?>
						<strong><?php echo esc_html( $option['label'] ); ?></strong>
					<?php endif; ?>
					<?php if ( ! empty( $option['description'] ) ) : ?><span class="mpab-option__description"><?php echo esc_html( $option['description'] ); ?></span><?php endif; ?>
				</span>
			</span>
		</label>
		<?php
	}

	/**
	 * Format a whole or decimal price for prototype display.
	 *
	 * @param float  $price Price.
	 * @param string $symbol Currency symbol.
	 * @param bool   $prefix_plus Whether this is an add-on.
	 * @return string
	 */
	private function format_price( $price, $symbol, $prefix_plus = false ) {
		if ( 0.0 === (float) $price ) {
			return __( 'Included', 'mark-pacura-album-builder' );
		}
		$number = ( floor( (float) $price ) === (float) $price ) ? number_format_i18n( $price, 0 ) : number_format_i18n( $price, 2 );
		return ( $prefix_plus ? '+' : '' ) . $symbol . $number;
	}

	/**
	 * Validate, price, persist and notify for a public album request.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function receive_submission( WP_REST_Request $request ) {
		$settings = $this->settings();
		if ( ! $this->email_setup_complete( $settings ) ) {
			return new WP_Error( 'mpab_email_setup_incomplete', __( 'Album ordering is not available yet.', 'mark-pacura-album-builder' ), array( 'status' => 503 ) );
		}
		$payload = $request->get_json_params();
		$payload = is_array( $payload ) ? $payload : array();

		$submitted_token = isset( $payload['token'] ) ? sanitize_text_field( $payload['token'] ) : '';
		$valid_token     = '' !== $submitted_token && hash_equals( $this->submission_token(), $submitted_token );
		if ( ! $valid_token ) {
			return new WP_Error( 'mpab_invalid_request', __( 'Your order details could not be verified. Please refresh the page and try again.', 'mark-pacura-album-builder' ), array( 'status' => 403 ) );
		}
		if ( ! empty( $payload['website'] ) ) {
			return new WP_Error( 'mpab_spam', __( 'Your request could not be sent. Please check your connection and try again.', 'mark-pacura-album-builder' ), array( 'status' => 400 ) );
		}
		$elapsed_ms = isset( $payload['elapsed_ms'] ) ? absint( $payload['elapsed_ms'] ) : 0;
		if ( $elapsed_ms < 2000 || $elapsed_ms > ( DAY_IN_SECONDS * 1000 ) ) {
			return new WP_Error( 'mpab_timing', __( 'Please refresh the page and try again.', 'mark-pacura-album-builder' ), array( 'status' => 400 ) );
		}

		$name    = $this->truncate( sanitize_text_field( isset( $payload['client_name'] ) ? $payload['client_name'] : '' ), 120 );
		$email   = sanitize_email( isset( $payload['client_email'] ) ? $payload['client_email'] : '' );
		$note    = $this->truncate( sanitize_textarea_field( isset( $payload['client_note'] ) ? $payload['client_note'] : '' ), 1000 );
		$locale  = $this->normalise_locale( isset( $payload['locale'] ) ? $payload['locale'] : '' );
		$consent = ! empty( $payload['consent'] );
		$idempotency_key = isset( $payload['idempotency_key'] ) ? strtolower( sanitize_text_field( $payload['idempotency_key'] ) ) : '';

		if ( '' === $name || ! is_email( $email ) || ! $consent || 1 !== preg_match( '/^[a-f0-9]{32}$/', $idempotency_key ) ) {
			return new WP_Error( 'mpab_details', __( 'Please enter your names, a valid email address, and confirmation before sending.', 'mark-pacura-album-builder' ), array( 'status' => 422 ) );
		}
		$existing_orders = get_posts(
			array(
				'post_type'      => self::POST_TYPE,
				'post_status'    => 'private',
				'posts_per_page' => 1,
				'fields'         => 'ids',
				'no_found_rows'  => true,
				'meta_key'       => '_mpab_idempotency_key',
				'meta_value'     => $idempotency_key,
			)
		);
		if ( ! empty( $existing_orders ) ) {
			$existing_id = absint( $existing_orders[0] );
			return rest_ensure_response(
				array(
					'reference'   => (string) get_post_meta( $existing_id, '_mpab_reference', true ),
					'message'     => __( 'Your album choices were already saved.', 'mark-pacura-album-builder' ),
					'clientEmail' => 'yes' === get_post_meta( $existing_id, '_mpab_client_email_sent', true ),
					'ownerEmail'  => 'yes' === get_post_meta( $existing_id, '_mpab_owner_email_sent', true ),
				)
			);
		}
		$current_promotion = $this->promotion( $settings );
		$submitted_promotion_percent = isset( $payload['promotion_percent'] ) ? absint( $payload['promotion_percent'] ) : 0;
		$submitted_promotion_end_date = isset( $payload['promotion_end_date'] ) ? sanitize_text_field( $payload['promotion_end_date'] ) : '';
		$expected_promotion_percent = $current_promotion['active'] ? (int) $current_promotion['percent'] : 0;
		$expected_promotion_end_date = $current_promotion['active'] ? (string) $current_promotion['end_date'] : '';
		if ( $submitted_promotion_percent !== $expected_promotion_percent || $submitted_promotion_end_date !== $expected_promotion_end_date ) {
			return new WP_Error( 'mpab_promotion_changed', __( 'This special offer has just changed. Please refresh the page to see the latest total before sending your choices.', 'mark-pacura-album-builder' ), array( 'status' => 409 ) );
		}

		$catalogue = $this->catalogue( $settings );
		$spreads   = isset( $payload['spreads'] ) ? absint( $payload['spreads'] ) : 0;
		$quantity  = isset( $payload['quantity'] ) ? absint( $payload['quantity'] ) : 1;
		if ( $spreads < 10 || $spreads > 40 ) {
			return new WP_Error( 'mpab_spreads', __( 'Choose between 10 and 40 spreads.', 'mark-pacura-album-builder' ), array( 'status' => 422 ) );
		}
		if ( $quantity < 1 || $quantity > 20 ) {
			return new WP_Error( 'mpab_quantity', __( 'Choose between 1 and 20 copies.', 'mark-pacura-album-builder' ), array( 'status' => 422 ) );
		}
		$selected  = array(
			'album_type'      => isset( $payload['album_type'] ) ? sanitize_key( $payload['album_type'] ) : '',
			'size'            => isset( $payload['size'] ) ? sanitize_key( $payload['size'] ) : '',
			'cover_material'  => isset( $payload['cover_material'] ) ? sanitize_key( $payload['cover_material'] ) : '',
			'cover'           => isset( $payload['cover'] ) ? sanitize_key( $payload['cover'] ) : '',
			'personalisation' => isset( $payload['personalisation'] ) ? sanitize_key( $payload['personalisation'] ) : '',
			'personalisation_category' => isset( $payload['personalisation_category'] ) ? sanitize_key( $payload['personalisation_category'] ) : '',
			'personalisation_font' => isset( $payload['personalisation_font'] ) ? sanitize_key( $payload['personalisation_font'] ) : '',
			'personalisation_design' => isset( $payload['personalisation_design'] ) ? sanitize_key( $payload['personalisation_design'] ) : '',
			'foil_colour'     => isset( $payload['foil_colour'] ) ? sanitize_key( $payload['foil_colour'] ) : '',
			'box'             => isset( $payload['box'] ) ? sanitize_key( $payload['box'] ) : '',
			'box_colour'      => isset( $payload['box_colour'] ) ? sanitize_key( $payload['box_colour'] ) : '',
		);
		$maximum_text_lines = (int) $settings['personalisation_text_lines'];
		$raw_personal_lines = isset( $payload['personal_text_lines'] ) && is_array( $payload['personal_text_lines'] )
			? $payload['personal_text_lines']
			: preg_split( '/\r\n|\r|\n/', isset( $payload['personal_text'] ) ? (string) $payload['personal_text'] : '' );
		$raw_personal_lines = is_array( $raw_personal_lines ) ? $raw_personal_lines : array();
		$personal_text_lines = array();
		$personal_text_invalid = count( $raw_personal_lines ) > $maximum_text_lines;
		foreach ( array_slice( $raw_personal_lines, 0, $maximum_text_lines ) as $raw_personal_line ) {
			if ( is_array( $raw_personal_line ) || is_object( $raw_personal_line ) ) {
				$personal_text_invalid = true;
				continue;
			}
			$personal_text_line = sanitize_text_field( (string) $raw_personal_line );
			if ( $this->text_length( $personal_text_line ) > 25 ) {
				$personal_text_invalid = true;
			}
			$personal_text_line = trim( $this->truncate( $personal_text_line, 25 ) );
			if ( '' !== $personal_text_line ) {
				$personal_text_lines[] = $personal_text_line;
			}
		}
		$personal_text = implode( "\n", $personal_text_lines );
		if ( 'text' === $selected['personalisation_category'] && $personal_text_invalid ) {
			return new WP_Error( 'mpab_personal_text', __( 'Use only the available text lines and enter no more than 25 characters on each line.', 'mark-pacura-album-builder' ), array( 'status' => 422 ) );
		}

		$album_type     = isset( $catalogue['album_types'][ $selected['album_type'] ] ) ? $catalogue['album_types'][ $selected['album_type'] ] : null;
		$size_options   = isset( $catalogue['sizes'][ $selected['album_type'] ] ) ? $catalogue['sizes'][ $selected['album_type'] ] : array();
		$size           = $this->option_by_id( $size_options, $selected['size'] );
		$cover_material = isset( $catalogue['cover_materials'][ $selected['cover_material'] ] ) ? $catalogue['cover_materials'][ $selected['cover_material'] ] : null;
		$cover_options  = isset( $catalogue['covers'][ $selected['cover_material'] ] ) ? $catalogue['covers'][ $selected['cover_material'] ] : array();
		$cover          = $this->option_by_id( $cover_options, $selected['cover'] );
		$personal       = isset( $catalogue['personalisation'][ $selected['personalisation'] ] ) ? $catalogue['personalisation'][ $selected['personalisation'] ] : null;
		$personal_category = 'none' !== $selected['personalisation'] ? $this->option_by_id( $catalogue['personalisation_categories'], $selected['personalisation_category'] ) : array( 'id' => 'none', 'label' => __( 'Not applicable', 'mark-pacura-album-builder' ), 'price' => 0 );
		$personal_font  = 'text' === $selected['personalisation_category'] ? $this->option_by_id( $catalogue['fonts'], $selected['personalisation_font'] ) : array( 'id' => 'none', 'label' => __( 'Not applicable', 'mark-pacura-album-builder' ), 'price' => 0 );
		$personal_design = in_array( $selected['personalisation_category'], array( 'wb-design', 'full-cover' ), true ) ? $this->option_by_id( $catalogue['designs'], $selected['personalisation_design'] ) : array( 'id' => 'none', 'label' => __( 'Not applicable', 'mark-pacura-album-builder' ), 'price' => 0 );
		$foil_colour    = 'foil' === $selected['personalisation'] ? $this->option_by_id( $catalogue['foil_colours'], $selected['foil_colour'] ) : array( 'id' => 'none', 'label' => __( 'No foil', 'mark-pacura-album-builder' ), 'price' => 0 );
		$boxes_are_disabled = empty( $catalogue['boxes'] );
		$box            = $boxes_are_disabled || 'none' === $selected['box'] ? array( 'id' => 'none', 'label' => __( 'Just the album, please', 'mark-pacura-album-builder' ), 'price' => 0 ) : ( isset( $catalogue['boxes'][ $selected['box'] ] ) ? $catalogue['boxes'][ $selected['box'] ] : null );
		$box_colour     = ! $boxes_are_disabled && 'wooden' === $selected['box'] ? $this->option_by_id( $catalogue['box_colours'], $selected['box_colour'] ) : array( 'id' => 'none', 'label' => __( 'Not applicable', 'mark-pacura-album-builder' ), 'price' => 0 );
		$personal_is_compatible = $personal && in_array( $selected['cover_material'], $personal['families'], true );
		$allowed_categories = 'foil' === $selected['personalisation'] ? array( 'text', 'wb-design' ) : array( 'text', 'wb-design', 'own-design', 'full-cover' );
		$category_is_valid = 'none' === $selected['personalisation'] || in_array( $selected['personalisation_category'], $allowed_categories, true );
		$font_is_valid = 'text' !== $selected['personalisation_category'] || ( $personal_font && in_array( $selected['personalisation'], $personal_font['methods'], true ) && ! empty( $personal_text_lines ) );
		$design_is_valid = ! in_array( $selected['personalisation_category'], array( 'wb-design', 'full-cover' ), true ) || ( $personal_design && in_array( $selected['personalisation'], $personal_design['methods'], true ) );
		if ( $personal_design && 'none' !== $personal_design['id'] ) {
			$design_is_valid = $design_is_valid && ( 'full-cover' === $selected['personalisation_category'] ? 'collection.full-cover' === $personal_design['collection'] : 'collection.full-cover' !== $personal_design['collection'] );
		}

		if ( ! $album_type || ! $size || ! $cover_material || ! $cover || ! $personal || ! $personal_category || ! $personal_font || ! $personal_design || ! $foil_colour || ! $box || ! $box_colour || ! $personal_is_compatible || ! $category_is_valid || ! $font_is_valid || ! $design_is_valid ) {
			return new WP_Error( 'mpab_options', __( 'One or more selected album options are no longer available. Please review your choices and try again.', 'mark-pacura-album-builder' ), array( 'status' => 422 ) );
		}

		$rate_limit = $this->enforce_submission_rate_limit( $email );
		if ( is_wp_error( $rate_limit ) ) {
			return $rate_limit;
		}

		$extra_spreads = $spreads - 10;
		$spread_total  = $extra_spreads * (float) $size['spread_price'];
		$promotion_totals = $this->promotion_totals( ( (float) $size['price'] + $spread_total + (float) $box['price'] ) * $quantity, $settings );
		$total         = $promotion_totals['total'];
		$configuration = array(
			'version'          => MPAB_VERSION,
			'locale'           => $locale,
			'album_type'       => $album_type,
			'size'             => $size,
			'spreads'          => $spreads,
			'pages'            => $spreads * 2,
			'quantity'         => $quantity,
			'extra_spreads'    => $extra_spreads,
			'extra_spread_total' => $spread_total,
			'cover_material'   => $cover_material,
			'cover'            => $cover,
			'personalisation'  => $personal,
			'personalisation_category' => $personal_category,
			'personalisation_font' => $personal_font,
			'personalisation_design' => $personal_design,
			'personal_text'    => $personal_text,
			'personal_text_lines' => $personal_text_lines,
			'foil_colour'      => $foil_colour,
			'box'              => $box,
			'box_colour'       => $box_colour,
			'normal_total'     => $promotion_totals['normal_total'],
			'promotion'        => array(
				'applied'   => $promotion_totals['active'],
				'percent'   => $promotion_totals['percent'],
				'discount'  => $promotion_totals['discount'],
				'end_date'  => $promotion_totals['end_date'],
				'end_label' => $promotion_totals['end_label'],
			),
			'total'            => $total,
			'currency_symbol'  => $this->currency_symbol( $settings['currency'] ),
			'order_intent'     => 'confirmed',
			'order_intent_at'  => current_time( 'mysql' ),
		);
		$idempotency_reservation = '_mpab_idem_' . $idempotency_key;
		if ( ! $this->reserve_submission_attempt( $idempotency_key ) ) {
			return new WP_Error( 'mpab_submission_in_progress', __( 'Your album choices are currently being processed. Please wait a moment.', 'mark-pacura-album-builder' ), array( 'status' => 409 ) );
		}

		$post_id = wp_insert_post(
			array(
				'post_type'   => self::POST_TYPE,
				'post_status' => 'private',
				'post_title'  => sprintf( __( 'Album order from %s', 'mark-pacura-album-builder' ), $name ),
			),
			true
		);
		if ( is_wp_error( $post_id ) ) {
			delete_option( $idempotency_reservation );
			return new WP_Error( 'mpab_save_failed', __( 'Your choices could not be saved. Please check your connection and try again.', 'mark-pacura-album-builder' ), array( 'status' => 500 ) );
		}
		$reference = $this->next_order_reference( $post_id );
		wp_update_post(
			array(
				'ID'         => $post_id,
				'post_title' => sprintf( __( 'Order %1$s — %2$s', 'mark-pacura-album-builder' ), $reference, $name ),
			)
		);

		update_post_meta( $post_id, '_mpab_reference', $reference );
		add_post_meta( $post_id, '_mpab_idempotency_key', $idempotency_key, true );
		update_option( $idempotency_reservation, 'saved:' . $post_id, false );
		update_post_meta( $post_id, '_mpab_client_name', $name );
		update_post_meta( $post_id, '_mpab_client_email', $email );
		update_post_meta( $post_id, '_mpab_client_note', $note );
		update_post_meta( $post_id, '_mpab_locale', $locale );
		update_post_meta( $post_id, '_mpab_configuration', wp_json_encode( $configuration ) );
		update_post_meta( $post_id, '_mpab_total', $total );
		update_post_meta( $post_id, '_mpab_order_intent', 'confirmed' );
		update_post_meta( $post_id, '_mpab_order_intent_at', $configuration['order_intent_at'] );
		update_post_meta( $post_id, '_mpab_status', 'new' );

		$admin_link   = admin_url( 'post.php?post=' . absint( $post_id ) . '&action=edit' );
		$client_sent  = $this->send_client_confirmation( $post_id );
		$owner_body   = $this->owner_email_html( $reference, $name, $email, $note, $configuration, $settings, $admin_link, $client_sent );
		$owner_headers = $this->email_headers( $settings['notification_email'], $settings['email_sender_name'], $email, $name );

		$owner_sent  = $this->deliver_mail( $settings['notification_email'], sprintf( __( 'New album order request %s', 'mark-pacura-album-builder' ), $reference ), $owner_body, $owner_headers );
		update_post_meta( $post_id, '_mpab_owner_email_sent', $owner_sent ? 'yes' : 'no' );
		update_post_meta( $post_id, '_mpab_owner_email_error', $owner_sent ? '' : $this->last_mail_error );
		if ( ! $client_sent && ! wp_next_scheduled( 'mpab_retry_client_email', array( $post_id ) ) ) {
			wp_schedule_single_event( time() + 60, 'mpab_retry_client_email', array( $post_id ) );
			update_post_meta( $post_id, '_mpab_client_email_retry', 'scheduled' );
		}

		return rest_ensure_response(
			array(
				'reference'   => $reference,
				'message'     => sprintf( __( 'Thank you. Your album order request has been saved as %s.', 'mark-pacura-album-builder' ), $reference ),
				'clientEmail' => (bool) $client_sent,
				'ownerEmail'  => (bool) $owner_sent,
			)
		);
	}

	/**
	 * Return the next short client-facing order number.
	 *
	 * The sequence is intentionally independent from WordPress post IDs so a new
	 * Album Studio installation begins at order 10 and then counts upwards.
	 *
	 * @param int $post_id Unique saved order ID used only as a safe database-failure fallback.
	 * @return string
	 */
	public function next_studio_order_reference( $post_id = 0 ) {
		global $wpdb;
		$option = self::ORDER_SEQUENCE;
		add_option( $option, 10, '', false );
		$legacy_wall_next = max( 10, absint( get_option( 'mpab_wall_order_sequence', 10 ) ) );
		$highest_saved = absint(
			$wpdb->get_var(
				$wpdb->prepare(
					"SELECT MAX(CAST(pm.meta_value AS UNSIGNED))
					 FROM {$wpdb->postmeta} pm
					 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
					 WHERE pm.meta_key = %s AND p.post_type IN (%s, %s)",
					'_mpab_reference',
					self::POST_TYPE,
					'mpab_wall_order'
				)
			)
		); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Allocating one sequence across two private order types.
		$minimum_next = max( 10, $legacy_wall_next, $highest_saved + 1 );
		$updated = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->options}
				 SET option_value = LAST_INSERT_ID(GREATEST(CAST(option_value AS UNSIGNED), %d) + 1)
				 WHERE option_name = %s",
				$minimum_next,
				$option
			)
		); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared immediately above.
		$allocated = 1 === (int) $updated ? absint( $wpdb->get_var( 'SELECT LAST_INSERT_ID()' ) ) - 1 : 0; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$reference = $allocated >= 10 ? $allocated : max( $minimum_next, absint( $post_id ) );
		return (string) $reference;
	}

	/** Preserve the album submission call while using the shared Studio sequence. */
	private function next_order_reference( $post_id ) {
		return $this->next_studio_order_reference( $post_id );
	}

	/**
	 * Capture a synchronous wp_mail failure so the order record can explain it.
	 *
	 * @param WP_Error $error WordPress mail error.
	 * @return void
	 */
	public function capture_mail_error( $error ) {
		$this->last_mail_error = is_wp_error( $error ) ? $error->get_error_message() : __( 'Your website could not deliver the email message.', 'mark-pacura-album-builder' );
	}

	/**
	 * Align the return path with the verified studio address when the mailer has
	 * not already supplied one. This improves delivery to external inboxes while
	 * respecting SMTP plugins that set their own authenticated envelope sender.
	 *
	 * @param PHPMailer\PHPMailer\PHPMailer $mailer Active WordPress mailer.
	 * @return void
	 */
	public function align_mail_sender( $mailer ) {
		if ( is_object( $mailer ) && is_email( $this->mail_sender_email ) && empty( $mailer->Sender ) ) {
			$mailer->Sender = $this->mail_sender_email;
		}
		if ( is_object( $mailer ) && '' !== $this->mail_alt_body ) {
			$mailer->AltBody = $this->mail_alt_body;
		}
	}

	/**
	 * Send one message while retaining any immediate WordPress error.
	 *
	 * @param string $to Recipient.
	 * @param string $subject Subject.
	 * @param string $body HTML body.
	 * @param array  $headers Headers.
	 * @param string $alt_body Optional plain-text alternative.
	 * @return bool
	 */
	private function deliver_mail( $to, $subject, $body, $headers, $alt_body = '' ) {
		$this->last_mail_error = '';
		$settings = $this->settings();
		$this->mail_sender_email = sanitize_email( $settings['notification_email'] );
		$this->mail_alt_body = trim( wp_strip_all_tags( (string) $alt_body ) );
		add_action( 'wp_mail_failed', array( $this, 'capture_mail_error' ) );
		add_action( 'phpmailer_init', array( $this, 'align_mail_sender' ) );
		$sent = wp_mail( $to, $subject, $body, $headers );
		remove_action( 'phpmailer_init', array( $this, 'align_mail_sender' ) );
		remove_action( 'wp_mail_failed', array( $this, 'capture_mail_error' ) );
		$this->mail_sender_email = '';
		$this->mail_alt_body = '';
		return (bool) $sent;
	}

	/**
	 * Send the client copy from a saved order.
	 *
	 * @param int $post_id Saved order ID.
	 * @return bool
	 */
	private function send_client_confirmation( $post_id ) {
		$order_locale = $this->normalise_locale( get_post_meta( $post_id, '_mpab_locale', true ) );
		$current_locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
		$locale_switched = $order_locale !== $current_locale && switch_to_locale( $order_locale );
		$email = sanitize_email( get_post_meta( $post_id, '_mpab_client_email', true ) );
		$name = (string) get_post_meta( $post_id, '_mpab_client_name', true );
		$reference = (string) get_post_meta( $post_id, '_mpab_reference', true );
		$configuration = json_decode( (string) get_post_meta( $post_id, '_mpab_configuration', true ), true );
		$settings = $this->settings();
		if ( ! is_email( $email ) || ! is_array( $configuration ) ) {
			update_post_meta( $post_id, '_mpab_client_email_sent', 'no' );
			update_post_meta( $post_id, '_mpab_client_email_error', __( 'Some client details or order information appears to be missing.', 'mark-pacura-album-builder' ) );
			if ( $locale_switched ) {
				restore_previous_locale();
			}
			return false;
		}
		$body = $this->client_email_html( $reference, $name, $configuration, $settings );
		$alt_body = $this->client_email_plain_text( $reference, $name, $email, $configuration );
		$headers = $this->email_headers( $settings['notification_email'], $settings['email_sender_name'], $settings['notification_email'], $settings['email_sender_name'] );
		$sent = $this->deliver_mail( $email, sprintf( __( 'Your album order request — %s', 'mark-pacura-album-builder' ), $reference ), $body, $headers, $alt_body );
		update_post_meta( $post_id, '_mpab_client_email_sent', $sent ? 'yes' : 'no' );
		update_post_meta( $post_id, '_mpab_client_email_error', $sent ? '' : $this->last_mail_error );
		update_post_meta( $post_id, '_mpab_client_email_last_attempt', current_time( 'mysql' ) );
		if ( $locale_switched ) {
			restore_previous_locale();
		}
		return $sent;
	}

	/**
	 * Retry one client confirmation when WordPress rejected the first send.
	 *
	 * @param int $post_id Saved order ID.
	 * @return void
	 */
	public function retry_client_email( $post_id ) {
		$post_id = absint( $post_id );
		if ( ! $post_id || self::POST_TYPE !== get_post_type( $post_id ) ) {
			return;
		}
		$sent = $this->send_client_confirmation( $post_id );
		update_post_meta( $post_id, '_mpab_client_email_retry', $sent ? 'sent' : 'failed' );
	}

	/**
	 * Let the photographer manually resend a saved client's confirmation.
	 *
	 * @return void
	 */
	public function resend_client_email() {
		$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;
		if ( ! $post_id || self::POST_TYPE !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
			wp_die( esc_html__( 'You do not have permission to resend this confirmation.', 'mark-pacura-album-builder' ) );
		}
		check_admin_referer( 'mpab_resend_client_email_' . $post_id );
		$sent = $this->send_client_confirmation( $post_id );
		update_post_meta( $post_id, '_mpab_client_email_retry', $sent ? 'manual-sent' : 'failed' );
		wp_safe_redirect( admin_url( 'post.php?post=' . $post_id . '&action=edit&mpab_email_resent=' . ( $sent ? '1' : '0' ) ) );
		exit;
	}

	/**
	 * Move one saved album order to WordPress Trash.
	 *
	 * @return void
	 */
	public function remove_order() {
		$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;
		if ( ! $post_id || self::POST_TYPE !== get_post_type( $post_id ) || ! current_user_can( 'delete_post', $post_id ) ) {
			wp_die( esc_html__( 'You do not have permission to delete this order record.', 'mark-pacura-album-builder' ) );
		}
		check_admin_referer( 'mpab_remove_order_' . $post_id );
		if ( ! wp_trash_post( $post_id ) ) {
			wp_die( esc_html__( 'Unable to move this order to Trash. Please try again.', 'mark-pacura-album-builder' ) );
		}
		wp_safe_redirect( add_query_arg( array( 'page' => 'mpab', 'mpab_order_removed' => '1' ), admin_url( 'admin.php' ) ) . '#mpab-previous-orders' );
		exit;
	}

	/**
	 * Build mail headers using the saved photographer address.
	 *
	 * @param string $from_email Sender email.
	 * @param string $from_name Sender name.
	 * @param string $reply_email Reply-to email.
	 * @param string $reply_name Reply-to name.
	 * @return array<int,string>
	 */
	private function email_headers( $from_email, $from_name, $reply_email, $reply_name ) {
		$from_name   = sanitize_text_field( wp_specialchars_decode( (string) $from_name, ENT_QUOTES ) );
		$from_email  = sanitize_email( $from_email );
		$reply_email = sanitize_email( $reply_email );
		$reply_name  = sanitize_text_field( wp_specialchars_decode( (string) $reply_name, ENT_QUOTES ) );
		$headers = array( 'Content-Type: text/html; charset=UTF-8' );
		if ( is_email( $from_email ) ) {
			$headers[] = sprintf( 'From: %s <%s>', $from_name, $from_email );
		}
		if ( is_email( $reply_email ) ) {
			$headers[] = sprintf( 'Reply-To: %s <%s>', $reply_name ? $reply_name : $from_name, $reply_email );
		}
		return $headers;
	}

	/**
	 * Return the configuration rows shared by both email designs.
	 *
	 * @param array $configuration Selected configuration.
	 * @return array<int,array<int,string>>
	 */
	private function email_summary_rows( $configuration ) {
		$spread_label = sprintf(
			_n( '%1$d spread · %2$d pages', '%1$d spreads · %2$d pages', absint( $configuration['spreads'] ), 'mark-pacura-album-builder' ),
			absint( $configuration['spreads'] ),
			absint( $configuration['pages'] )
		);
		$rows = array(
			array( __( 'Album', 'mark-pacura-album-builder' ), $configuration['album_type']['label'] ),
			array( __( 'Size', 'mark-pacura-album-builder' ), $configuration['size']['label'] ),
			array( __( 'Number of copies', 'mark-pacura-album-builder' ), isset( $configuration['quantity'] ) ? (string) absint( $configuration['quantity'] ) : '1' ),
			array( __( 'Spreads', 'mark-pacura-album-builder' ), $spread_label ),
			array( __( 'Cover material', 'mark-pacura-album-builder' ), $configuration['cover_material']['label'] ),
			array( __( 'Cover colour', 'mark-pacura-album-builder' ), $configuration['cover']['label'] ),
			array( __( 'Personalisation', 'mark-pacura-album-builder' ), $configuration['personalisation']['label'] ),
		);
		if ( 'none' !== $configuration['personalisation']['id'] ) {
			$rows[] = array( __( 'Personalisation choice', 'mark-pacura-album-builder' ), $configuration['personalisation_category']['label'] );
			if ( 'text' === $configuration['personalisation_category']['id'] ) {
				$rows[] = array( __( 'Text', 'mark-pacura-album-builder' ), $configuration['personal_text'] );
				$rows[] = array( __( 'Font', 'mark-pacura-album-builder' ), $configuration['personalisation_font']['label'] );
			}
			if ( in_array( $configuration['personalisation_category']['id'], array( 'wb-design', 'full-cover' ), true ) ) {
				$rows[] = array( __( 'Design', 'mark-pacura-album-builder' ), $configuration['personalisation_design']['label'] );
			}
			if ( 'foil' === $configuration['personalisation']['id'] ) {
				$rows[] = array( __( 'Foil colour', 'mark-pacura-album-builder' ), $configuration['foil_colour']['label'] );
			}
		}
		$rows[] = array( __( 'Presentation', 'mark-pacura-album-builder' ), $configuration['box']['label'] );
		if ( 'wooden' === $configuration['box']['id'] && isset( $configuration['box_colour']['label'] ) ) {
			$rows[] = array( __( 'Wooden box colour', 'mark-pacura-album-builder' ), $configuration['box_colour']['label'] );
		}
		if ( ! empty( $configuration['promotion']['applied'] ) ) {
			$symbol = isset( $configuration['currency_symbol'] ) ? $configuration['currency_symbol'] : '';
			$promotion_label = sprintf( __( '%d%% special offer', 'mark-pacura-album-builder' ), absint( $configuration['promotion']['percent'] ) );
			if ( ! empty( $configuration['promotion']['end_label'] ) ) {
				$promotion_label .= ' · ' . sprintf( __( 'ends %s', 'mark-pacura-album-builder' ), $configuration['promotion']['end_label'] );
			}
			$rows[] = array( __( 'Usual total', 'mark-pacura-album-builder' ), $this->format_price( $configuration['normal_total'], $symbol ) );
			$rows[] = array( __( 'Promotion', 'mark-pacura-album-builder' ), $promotion_label );
			$rows[] = array( __( 'You save', 'mark-pacura-album-builder' ), $this->format_price( $configuration['promotion']['discount'], $symbol ) );
		}
		return $rows;
	}

	/**
	 * Return the photographer logo configured by the active WordPress theme.
	 *
	 * @return string
	 */
	private function site_logo_url() {
		$logo_id = absint( get_theme_mod( 'custom_logo', 0 ) );
		$logo_url = $logo_id ? wp_get_attachment_image_url( $logo_id, 'full' ) : '';
		if ( ! $logo_url && function_exists( 'get_site_icon_url' ) ) {
			$logo_url = get_site_icon_url( 192 );
		}
		return $logo_url ? esc_url_raw( $logo_url ) : '';
	}

	/**
	 * Build the practical production checklist shown only to the photographer.
	 *
	 * @return string
	 */
	private function owner_next_steps_html() {
		$steps = array(
			__( 'Contact the client to confirm payment.', 'mark-pacura-album-builder' ),
			__( 'Start designing the album.', 'mark-pacura-album-builder' ) . ' ' . sprintf(
				__( 'We recommend %s, our free album designer.', 'mark-pacura-album-builder' ),
				'<a href="' . esc_url( 'https://woodenbanana.spreadu.com/' ) . '" style="color:#a85324;font-weight:700;text-decoration:underline;">Spreadu</a>'
			),
			__( 'Place the finished order with Wooden Banana.', 'mark-pacura-album-builder' ),
		);
		$items = '';
		foreach ( $steps as $index => $step ) {
			$items .= '<tr><td style="width:34px;padding:8px 12px 8px 0;vertical-align:top;"><span style="display:inline-block;width:28px;height:28px;border-radius:50%;background:#de8a3f;color:#ffffff;font-size:12px;font-weight:800;line-height:28px;text-align:center;">' . esc_html( $index + 1 ) . '</span></td><td style="padding:9px 0;color:#343126;font-size:14px;line-height:1.55;vertical-align:top;">' . wp_kses( $step, array( 'a' => array( 'href' => array(), 'style' => array() ) ) ) . '</td></tr>';
		}
		return '<tr><td style="padding:26px 34px 0;"><div style="padding:22px 24px;background:#fff8ef;border:1px solid #efd7c2;"><div style="margin-bottom:8px;color:#a85324;font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;">' . esc_html__( 'Next steps', 'mark-pacura-album-builder' ) . '</div><h2 style="margin:0 0 10px;color:#343126;font-family:Georgia,\'Times New Roman\',serif;font-size:24px;font-weight:400;line-height:1.2;">' . esc_html__( 'Turn this request into a finished album', 'mark-pacura-album-builder' ) . '</h2><table role="presentation" width="100%" cellspacing="0" cellpadding="0">' . $items . '</table></div></td></tr>';
	}

	/** Build the practical production checklist for a wall-art order. */
	private function wall_art_owner_next_steps_html() {
		$steps = array(
			__( 'Contact the client to confirm payment.', 'mark-pacura-album-builder' ),
			__( 'Download and review the approved crop from the saved order record.', 'mark-pacura-album-builder' ),
			__( 'Place the finished wall art order with Wooden Banana.', 'mark-pacura-album-builder' ),
		);
		$items = '';
		foreach ( $steps as $index => $step ) {
			$items .= '<tr><td style="width:34px;padding:8px 12px 8px 0;vertical-align:top;"><span style="display:inline-block;width:28px;height:28px;border-radius:50%;background:#de8a3f;color:#ffffff;font-size:12px;font-weight:800;line-height:28px;text-align:center;">' . esc_html( $index + 1 ) . '</span></td><td style="padding:9px 0;color:#343126;font-size:14px;line-height:1.55;vertical-align:top;">' . esc_html( $step ) . '</td></tr>';
		}
		return '<tr><td style="padding:26px 34px 0;"><div style="padding:22px 24px;background:#fff8ef;border:1px solid #efd7c2;"><div style="margin-bottom:8px;color:#a85324;font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;">' . esc_html__( 'Next steps', 'mark-pacura-album-builder' ) . '</div><h2 style="margin:0 0 10px;color:#343126;font-family:Georgia,\'Times New Roman\',serif;font-size:24px;font-weight:400;line-height:1.2;">' . esc_html__( 'Turn this request into finished wall art', 'mark-pacura-album-builder' ) . '</h2><table role="presentation" width="100%" cellspacing="0" cellpadding="0">' . $items . '</table></div></td></tr>';
	}

	/**
	 * Render a compact, table-based HTML email that works in common mail clients.
	 *
	 * @param array  $settings Plugin settings.
	 * @param string $eyebrow Small heading.
	 * @param string $heading Main heading.
	 * @param string $intro Introductory copy.
	 * @param array  $rows Summary rows.
	 * @param float  $total Calculated total.
	 * @param string $symbol Currency symbol.
	 * @param string $note Optional note.
	 * @param string $cta_url Optional action URL.
	 * @param string $cta_label Optional action label.
	 * @param string $brand Either site or wooden-banana.
	 * @param string $extra_html Optional trusted table rows.
	 * @param string $total_label Optional total heading.
	 * @return string
	 */
	private function email_template( $settings, $eyebrow, $heading, $intro, $rows, $total, $symbol, $note = '', $cta_url = '', $cta_label = '', $brand = 'site', $extra_html = '', $total_label = '' ) {
		$accent = sanitize_hex_color( isset( $settings['accent_colour'] ) ? $settings['accent_colour'] : '' );
		$accent = $accent ? $accent : '#c47750';
		$site_name = isset( $settings['email_sender_name'] ) && '' !== $settings['email_sender_name'] ? $settings['email_sender_name'] : wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
		$is_wb = 'wooden-banana' === $brand;
		$email_accent = $is_wb ? '#de8a3f' : $accent;
		$email_background = $is_wb ? '#f6f0e8' : '#f4f1eb';
		if ( $is_wb ) {
			$brand_html = '<table role="presentation" width="100%" cellspacing="0" cellpadding="0"><tr><td><img src="' . esc_url( MPAB_URL . 'assets/images/wooden-banana-logo.png' ) . '" width="230" alt="Wooden Banana" style="display:block;width:100%;max-width:230px;height:auto;border:0;"></td><td style="color:#6c624e;font-size:10px;font-weight:800;letter-spacing:.16em;text-align:right;text-transform:uppercase;">' . esc_html__( 'Studio', 'mark-pacura-album-builder' ) . '</td></tr></table>';
		} else {
			$site_logo = $this->site_logo_url();
			$brand_html = $site_logo
				? '<img src="' . esc_url( $site_logo ) . '" alt="' . esc_attr( $site_name ) . '" style="display:block;width:auto;max-width:220px;height:auto;max-height:76px;border:0;">'
				: '<div style="color:#24231f;font-family:Georgia,\'Times New Roman\',serif;font-size:20px;">' . esc_html( $site_name ) . '</div>';
		}
		$rows_html = '';
		foreach ( $rows as $row ) {
			$rows_html .= '<tr><td style="padding:13px 0;border-bottom:1px solid #e5e0d8;color:#77736d;font-size:13px;line-height:1.45;vertical-align:top;width:38%;">' . esc_html( $row[0] ) . '</td><td style="padding:13px 0;border-bottom:1px solid #e5e0d8;color:#24231f;font-size:14px;font-weight:600;line-height:1.45;text-align:right;vertical-align:top;">' . nl2br( esc_html( $row[1] ) ) . '</td></tr>';
		}
		$note_html = $note ? '<tr><td style="padding:22px 34px 0;"><div style="padding:18px 20px;background:#f4f1eb;border-left:3px solid ' . esc_attr( $email_accent ) . ';color:#4b4945;font-size:14px;line-height:1.6;"><strong style="display:block;margin-bottom:5px;color:#24231f;">' . esc_html__( 'Client note', 'mark-pacura-album-builder' ) . '</strong>' . nl2br( esc_html( $note ) ) . '</div></td></tr>' : '';
		$cta_html = $cta_url && $cta_label ? '<tr><td style="padding:26px 34px 0;"><a href="' . esc_url( $cta_url ) . '" style="display:inline-block;padding:14px 21px;border-radius:5px;background:' . esc_attr( $email_accent ) . ';color:#ffffff;font-size:13px;font-weight:700;letter-spacing:.04em;text-decoration:none;">' . esc_html( $cta_label ) . '</a></td></tr>' : '';
		$eyebrow_html = $eyebrow ? '<div style="margin-bottom:10px;color:#77736d;font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;">' . esc_html( $eyebrow ) . '</div>' : '';
		$footer = $is_wb
			? sprintf( __( 'Wooden Banana Studio · Order captured from %s.', 'mark-pacura-album-builder' ), $site_name )
			: sprintf( __( 'This automatic confirmation was generated from your website order page at %s.', 'mark-pacura-album-builder' ), $site_name );
		$total_label = $total_label ? $total_label : __( 'Album total', 'mark-pacura-album-builder' );

		return '<!doctype html><html><body style="margin:0;padding:0;background:' . esc_attr( $email_background ) . ';font-family:-apple-system,BlinkMacSystemFont,\'Segoe UI\',Arial,sans-serif;color:#24231f;"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="width:100%;background:' . esc_attr( $email_background ) . ';"><tr><td align="center" style="padding:34px 14px;"><table role="presentation" width="640" cellspacing="0" cellpadding="0" style="width:100%;max-width:640px;background:#ffffff;border:1px solid #e5e0d8;"><tr><td style="padding:28px 34px 24px;border-top:5px solid ' . esc_attr( $email_accent ) . ';">' . $brand_html . '</td></tr><tr><td style="padding:8px 34px 22px;">' . $eyebrow_html . '<h1 style="margin:0;color:#24231f;font-family:Georgia,\'Times New Roman\',serif;font-size:34px;font-weight:400;line-height:1.15;">' . esc_html( $heading ) . '</h1><p style="margin:16px 0 0;color:#5f5b55;font-size:15px;line-height:1.7;">' . esc_html( $intro ) . '</p></td></tr><tr><td style="padding:0 34px;"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="width:100%;border-top:1px solid #e5e0d8;">' . $rows_html . '</table></td></tr><tr><td style="padding:24px 34px 0;"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="width:100%;background:#24231f;"><tr><td style="padding:18px 20px;color:#ffffff;font-size:12px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;">' . esc_html( $total_label ) . '</td><td style="padding:18px 20px;color:#ffffff;font-family:Georgia,\'Times New Roman\',serif;font-size:28px;text-align:right;">' . esc_html( $this->format_price( $total, $symbol ) ) . '</td></tr></table></td></tr>' . $note_html . $extra_html . $cta_html . '<tr><td style="padding:30px 34px;color:#8a867f;font-size:12px;line-height:1.6;">' . esc_html( $footer ) . '</td></tr></table></td></tr></table></body></html>';
	}

	/**
	 * Photographer notification email.
	 */
	private function owner_email_html( $reference, $name, $email, $note, $configuration, $settings, $admin_link, $client_email_sent = null ) {
		$rows = array_merge(
			array(
				array( __( 'Order number', 'mark-pacura-album-builder' ), $reference ),
				array( __( 'Client', 'mark-pacura-album-builder' ), $name ),
				array( __( 'Client email', 'mark-pacura-album-builder' ), $email ),
			),
			$this->email_summary_rows( $configuration )
		);
		$delivery_alert = false === $client_email_sent
			? '<tr><td style="padding:22px 34px 0;"><div style="padding:16px 18px;background:#fff1f0;border-left:3px solid #b83a36;color:#5c2b29;font-size:14px;line-height:1.55;"><strong style="display:block;margin-bottom:4px;">' . esc_html__( 'Client confirmation required', 'mark-pacura-album-builder' ) . '</strong>' . esc_html__( 'Your website did not deliver the client confirmation on the first attempt. Studio will retry automatically; your saved order shows the current email status.', 'mark-pacura-album-builder' ) . '</div></td></tr>'
			: '';
		$total_label = ! empty( $configuration['promotion']['applied'] ) ? __( 'Promotional total', 'mark-pacura-album-builder' ) : __( 'Album total', 'mark-pacura-album-builder' );
		return $this->email_template( $settings, '', sprintf( __( 'New album order from %s', 'mark-pacura-album-builder' ), $name ), __( 'The client has confirmed they wish to proceed with the album selections listed below.', 'mark-pacura-album-builder' ), $rows, $configuration['total'], $configuration['currency_symbol'], $note, $admin_link, __( 'View order in Studio', 'mark-pacura-album-builder' ), 'wooden-banana', $delivery_alert . $this->owner_next_steps_html(), $total_label );
	}

	/**
	 * Client confirmation email.
	 */
	private function client_email_html( $reference, $name, $configuration, $settings ) {
		$rows = array_merge( array( array( __( 'Order number', 'mark-pacura-album-builder' ), $reference ) ), $this->email_summary_rows( $configuration ) );
		$intro = 'we' === $this->site_client_voice()
			? __( 'This is an automatic confirmation that we have received your album choices. We will contact you shortly to confirm production, delivery, and payment.', 'mark-pacura-album-builder' )
			: __( 'This is an automatic confirmation that I have received your album choices. I will contact you shortly to confirm production, delivery, and payment.', 'mark-pacura-album-builder' );
		$total_label = ! empty( $configuration['promotion']['applied'] ) ? __( 'Promotional total', 'mark-pacura-album-builder' ) : __( 'Album total', 'mark-pacura-album-builder' );
		return $this->email_template( $settings, __( 'Album order received', 'mark-pacura-album-builder' ), sprintf( __( 'Thank you, %s', 'mark-pacura-album-builder' ), $name ), $intro, $rows, $configuration['total'], $configuration['currency_symbol'], '', '', '', 'site', '', $total_label );
	}

	/**
	 * Send a wall-art order through the same sender settings and visual templates
	 * used by album orders.
	 *
	 * @return array{client:bool,owner:bool}
	 */
	public function send_wall_art_order_emails( $post_id, $reference, $name, $email, $configuration, $admin_link ) {
		$settings = $this->settings();
		$size = $configuration['item']['width_cm'] . 'x' . $configuration['item']['height_cm'] . ' cm · ' . $configuration['item']['width_in'] . 'x' . $configuration['item']['height_in'] . ' in';
		$choice_rows = array(
			array( __( 'Product', 'mark-pacura-album-builder' ), $configuration['product']['label'] ),
			array( __( 'Size', 'mark-pacura-album-builder' ), $size ),
		);
		if ( ! empty( $configuration['finish'] ) ) {
			$choice_rows[] = array( __( 'Finish', 'mark-pacura-album-builder' ), $configuration['finish'] );
		}
		$choice_rows[] = array( __( 'Number of copies', 'mark-pacura-album-builder' ), (string) absint( $configuration['quantity'] ) );
		if ( ! empty( $configuration['promotion']['applied'] ) ) {
			$choice_rows[] = array( __( 'Usual total', 'mark-pacura-album-builder' ), $this->format_price( $configuration['normal_total'], $configuration['currency_symbol'] ) );
			$choice_rows[] = array( __( 'Promotion', 'mark-pacura-album-builder' ), sprintf( __( '%d%% off', 'mark-pacura-album-builder' ), absint( $configuration['promotion']['percent'] ) ) );
			$choice_rows[] = array( __( 'You save', 'mark-pacura-album-builder' ), $this->format_price( $configuration['promotion']['discount'], $configuration['currency_symbol'] ) );
		}
		$total_label = ! empty( $configuration['promotion']['applied'] ) ? __( 'Promotional total', 'mark-pacura-album-builder' ) : __( 'Wall art total', 'mark-pacura-album-builder' );
		$client_rows = array_merge( array( array( __( 'Order number', 'mark-pacura-album-builder' ), $reference ) ), $choice_rows );
		$client_intro = 'we' === $this->site_client_voice()
			? __( 'This is an automatic confirmation that we have received your wall art choices. We will contact you shortly to confirm production, delivery, and payment.', 'mark-pacura-album-builder' )
			: __( 'This is an automatic confirmation that I have received your wall art choices. I will contact you shortly to confirm production, delivery, and payment.', 'mark-pacura-album-builder' );
		$client_body = $this->email_template( $settings, __( 'Wall art order received', 'mark-pacura-album-builder' ), sprintf( __( 'Thank you, %s', 'mark-pacura-album-builder' ), $name ), $client_intro, $client_rows, $configuration['total'], $configuration['currency_symbol'], '', '', '', 'site', '', $total_label );
		$client_text = sprintf( __( "Thank you, %1$s\n\n%2$s\n\nOrder number: %3$s\nProduct: %4$s\nSize: %5$s\nNumber of copies: %6$d\nTotal: %7$s%8$s", 'mark-pacura-album-builder' ), $name, $client_intro, $reference, $configuration['product']['label'], $size, absint( $configuration['quantity'] ), $configuration['currency_symbol'], number_format_i18n( $configuration['total'], 2 ) );
		$client_headers = $this->email_headers( $settings['notification_email'], $settings['email_sender_name'], $settings['notification_email'], $settings['email_sender_name'] );
		$client_sent = $this->deliver_mail( $email, sprintf( __( 'Your wall art order request — %s', 'mark-pacura-album-builder' ), $reference ), $client_body, $client_headers, $client_text );
		update_post_meta( $post_id, '_mpab_client_email_sent', $client_sent ? 'yes' : 'no' );
		update_post_meta( $post_id, '_mpab_client_email_error', $client_sent ? '' : $this->last_mail_error );
		update_post_meta( $post_id, '_mpab_client_email_last_attempt', current_time( 'mysql' ) );

		$owner_rows = array_merge(
			array(
				array( __( 'Order number', 'mark-pacura-album-builder' ), $reference ),
				array( __( 'Client', 'mark-pacura-album-builder' ), $name ),
				array( __( 'Client email', 'mark-pacura-album-builder' ), $email ),
			),
			$choice_rows
		);
		$delivery_alert = ! $client_sent
			? '<tr><td style="padding:22px 34px 0;"><div style="padding:16px 18px;background:#fff1f0;border-left:3px solid #b83a36;color:#5c2b29;font-size:14px;line-height:1.55;"><strong style="display:block;margin-bottom:4px;">' . esc_html__( 'Client confirmation required', 'mark-pacura-album-builder' ) . '</strong>' . esc_html__( 'Your website did not deliver the client confirmation email. Your order is saved safely, but please contact the client directly.', 'mark-pacura-album-builder' ) . '</div></td></tr>'
			: '';
		$owner_body = $this->email_template( $settings, '', sprintf( __( 'New wall art order from %s', 'mark-pacura-album-builder' ), $name ), __( 'The client has confirmed they wish to proceed with the wall art selections listed below.', 'mark-pacura-album-builder' ), $owner_rows, $configuration['total'], $configuration['currency_symbol'], '', $admin_link, __( 'View order in Studio', 'mark-pacura-album-builder' ), 'wooden-banana', $delivery_alert . $this->wall_art_owner_next_steps_html(), $total_label );
		$owner_headers = $this->email_headers( $settings['notification_email'], $settings['email_sender_name'], $email, $name );
		$owner_sent = $this->deliver_mail( $settings['notification_email'], sprintf( __( 'New wall art order request %s', 'mark-pacura-album-builder' ), $reference ), $owner_body, $owner_headers );
		update_post_meta( $post_id, '_mpab_owner_email_sent', $owner_sent ? 'yes' : 'no' );
		update_post_meta( $post_id, '_mpab_owner_email_error', $owner_sent ? '' : $this->last_mail_error );
		return array( 'client' => $client_sent, 'owner' => $owner_sent );
	}

	/**
	 * Build a plain-text client confirmation for inboxes that block HTML-only mail.
	 *
	 * @param string $reference Reference.
	 * @param string $name Client name.
	 * @param string $email Client email.
	 * @param array  $configuration Selected configuration.
	 * @return string
	 */
	private function client_email_plain_text( $reference, $name, $email, $configuration ) {
		$intro = 'we' === $this->site_client_voice()
			? __( 'This is an automatic confirmation that we have received your album choices. We will contact you shortly to confirm production, delivery, and payment.', 'mark-pacura-album-builder' )
			: __( 'This is an automatic confirmation that I have received your album choices. I will contact you shortly to confirm production, delivery, and payment.', 'mark-pacura-album-builder' );
		return sprintf( __( "Thank you, %s\n\n%s\n\n%s", 'mark-pacura-album-builder' ), $name, $intro, $this->plain_summary( $reference, $name, $email, '', $configuration, true ) );
	}

	/**
	 * Create a readable, portable email summary.
	 *
	 * @param string $reference Reference.
	 * @param string $name Client name.
	 * @param string $email Client email.
	 * @param string $note Client note.
	 * @param array  $configuration Selected configuration.
	 * @param bool   $show_prices Whether price is client-visible.
	 * @return string
	 */
	private function plain_summary( $reference, $name, $email, $note, $configuration, $show_prices ) {
		$lines = array(
			sprintf( __( 'Album order number: %s', 'mark-pacura-album-builder' ), $reference ),
			'',
			sprintf( __( 'Names: %s', 'mark-pacura-album-builder' ), $name ),
			sprintf( __( 'Email: %s', 'mark-pacura-album-builder' ), $email ),
			sprintf( __( 'Album type: %s', 'mark-pacura-album-builder' ), $configuration['album_type']['label'] ),
			sprintf( __( 'Size: %s', 'mark-pacura-album-builder' ), $configuration['size']['label'] ),
			sprintf( __( 'Spreads: %1$d (%2$d pages)', 'mark-pacura-album-builder' ), absint( $configuration['spreads'] ), absint( $configuration['pages'] ) ),
			sprintf( __( 'Cover material: %s', 'mark-pacura-album-builder' ), $configuration['cover_material']['label'] ),
			sprintf( __( 'Cover: %s', 'mark-pacura-album-builder' ), $configuration['cover']['label'] ),
			sprintf( __( 'Personalisation: %s', 'mark-pacura-album-builder' ), $configuration['personalisation']['label'] ),
			sprintf( __( 'Presentation: %s', 'mark-pacura-album-builder' ), $configuration['box']['label'] ),
			sprintf( __( 'Order intent: Client confirmed their choice on %s', 'mark-pacura-album-builder' ), $configuration['order_intent_at'] ),
		);
		if ( 'none' !== $configuration['personalisation']['id'] ) {
			$lines[] = sprintf( __( 'Personalisation category: %s', 'mark-pacura-album-builder' ), $configuration['personalisation_category']['label'] );
			if ( 'text' === $configuration['personalisation_category']['id'] ) {
				$lines[] = sprintf( __( 'Wording: %s', 'mark-pacura-album-builder' ), $configuration['personal_text'] );
				$lines[] = sprintf( __( 'Font: %s', 'mark-pacura-album-builder' ), $configuration['personalisation_font']['label'] );
			}
			if ( in_array( $configuration['personalisation_category']['id'], array( 'wb-design', 'full-cover' ), true ) ) {
				$lines[] = sprintf( __( 'Design: %s', 'mark-pacura-album-builder' ), $configuration['personalisation_design']['label'] );
			}
			if ( 'foil' === $configuration['personalisation']['id'] ) {
				$lines[] = sprintf( __( 'Foil colour: %s', 'mark-pacura-album-builder' ), $configuration['foil_colour']['label'] );
			}
		}
		if ( 'wooden' === $configuration['box']['id'] && isset( $configuration['box_colour']['label'] ) ) {
			$lines[] = sprintf( __( 'Wooden box colour: %s', 'mark-pacura-album-builder' ), $configuration['box_colour']['label'] );
		}
		if ( $show_prices ) {
			if ( ! empty( $configuration['promotion']['applied'] ) ) {
				$lines[] = sprintf( __( 'Usual total: %s', 'mark-pacura-album-builder' ), $this->format_price( $configuration['normal_total'], $configuration['currency_symbol'] ) );
				$offer = sprintf( __( '%d%% off', 'mark-pacura-album-builder' ), absint( $configuration['promotion']['percent'] ) );
				if ( ! empty( $configuration['promotion']['end_label'] ) ) {
					$offer .= ' ' . sprintf( __( 'until %s', 'mark-pacura-album-builder' ), $configuration['promotion']['end_label'] );
				}
				$lines[] = sprintf( __( 'Special offer: %s', 'mark-pacura-album-builder' ), $offer );
				$lines[] = sprintf( __( 'You save: %s', 'mark-pacura-album-builder' ), $this->format_price( $configuration['promotion']['discount'], $configuration['currency_symbol'] ) );
			}
			$lines[] = sprintf( __( 'Total price: %s', 'mark-pacura-album-builder' ), $this->format_price( $configuration['total'], $configuration['currency_symbol'] ) );
		}
		if ( $note ) {
			$lines[] = '';
			$lines[] = __( 'Client note:', 'mark-pacura-album-builder' );
			$lines[] = $note;
		}
		return implode( "\n", $lines );
	}

	/**
	 * Mark a new album order as read through an explicit protected action.
	 *
	 * @return void
	 */
	public function mark_submission_as_read() {
		$post_id = isset( $_POST['post_id'] ) ? absint( wp_unslash( $_POST['post_id'] ) ) : 0;
		if ( ! $post_id || self::POST_TYPE !== get_post_type( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
			wp_die( esc_html__( 'You do not have permission to modify this order record.', 'mark-pacura-album-builder' ) );
		}
		check_admin_referer( 'mpab_mark_order_read_' . $post_id );
		if ( 'new' === get_post_meta( $post_id, '_mpab_status', true ) ) {
			update_post_meta( $post_id, '_mpab_status', 'read' );
			update_post_meta( $post_id, '_mpab_read_at', current_time( 'mysql' ) );
		}
		wp_safe_redirect( admin_url( 'post.php?post=' . $post_id . '&action=edit&mpab_marked_read=1' ) );
		exit;
	}

	/**
	 * Add a read-only request summary metabox.
	 *
	 * @return void
	 */
	public function register_submission_metabox() {
		add_meta_box(
			'mpab-submission-summary',
			__( 'Order details', 'mark-pacura-album-builder' ),
			array( $this, 'render_submission_metabox' ),
			self::POST_TYPE,
			'normal',
			'high'
		);
	}

	/**
	 * Render saved request data without exposing JSON editing.
	 *
	 * @param WP_Post $post Current submission.
	 * @return void
	 */
	public function render_submission_metabox( $post ) {
		$config = json_decode( (string) get_post_meta( $post->ID, '_mpab_configuration', true ), true );
		$config = is_array( $config ) ? $config : array();
		$personalisation_id = isset( $config['personalisation']['id'] ) ? $config['personalisation']['id'] : 'none';
		$read_at = get_post_meta( $post->ID, '_mpab_read_at', true );
		$owner_email_sent = 'yes' === get_post_meta( $post->ID, '_mpab_owner_email_sent', true );
		$client_email_sent = 'yes' === get_post_meta( $post->ID, '_mpab_client_email_sent', true );
		$client_email_retry = (string) get_post_meta( $post->ID, '_mpab_client_email_retry', true );
		$client_email_label = $client_email_sent ? __( 'Sent', 'mark-pacura-album-builder' ) : ( 'scheduled' === $client_email_retry ? __( 'Waiting to retry', 'mark-pacura-album-builder' ) : __( 'Failed', 'mark-pacura-album-builder' ) );
		$resend_result = isset( $_GET['mpab_email_resent'] ) ? sanitize_key( wp_unslash( $_GET['mpab_email_resent'] ) ) : '';
		$promotion_applied = ! empty( $config['promotion']['applied'] );
		$promotion_summary = '';
		if ( $promotion_applied ) {
			$promotion_summary = absint( $config['promotion']['percent'] ) . '% off';
			if ( ! empty( $config['promotion']['end_label'] ) ) {
				$promotion_summary .= ' · ' . sprintf( __( 'ends %s', 'mark-pacura-album-builder' ), $config['promotion']['end_label'] );
			}
		}
		echo '<div class="mpab-order-received"><span class="mpab-order-received__icon" aria-hidden="true">&#10003;</span><div><strong>' . esc_html( $read_at ? __( 'Order has been read', 'mark-pacura-album-builder' ) : __( 'New album order', 'mark-pacura-album-builder' ) ) . '</strong><p>';
		if ( $read_at ) {
			echo esc_html( sprintf( __( 'Marked as read on %s.', 'mark-pacura-album-builder' ), mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $read_at ) ) );
		} else {
			esc_html_e( 'Review the client choices below, then mark the order as read when ready.', 'mark-pacura-album-builder' );
		}
		echo '</p><p class="mpab-order-received__mail">' . esc_html( sprintf( __( 'Email status: Photographer %1$s · Client %2$s', 'mark-pacura-album-builder' ), $owner_email_sent ? __( 'Sent', 'mark-pacura-album-builder' ) : __( 'Failed', 'mark-pacura-album-builder' ), $client_email_label ) ) . '</p></div></div>';
		if ( ! $read_at ) {
			echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '"><input type="hidden" name="action" value="mpab_mark_order_read"><input type="hidden" name="post_id" value="' . absint( $post->ID ) . '">';
			wp_nonce_field( 'mpab_mark_order_read_' . $post->ID );
			submit_button( __( 'Mark order as read', 'mark-pacura-album-builder' ), 'secondary', 'submit', false );
			echo '</form>';
		}
		$rows   = array(
			__( 'Order number', 'mark-pacura-album-builder' ) => get_post_meta( $post->ID, '_mpab_reference', true ),
			__( 'Names', 'mark-pacura-album-builder' )        => get_post_meta( $post->ID, '_mpab_client_name', true ),
			__( 'Email', 'mark-pacura-album-builder' )        => get_post_meta( $post->ID, '_mpab_client_email', true ),
			__( 'Album type', 'mark-pacura-album-builder' )   => isset( $config['album_type']['label'] ) ? $config['album_type']['label'] : '',
			__( 'Album', 'mark-pacura-album-builder' )        => isset( $config['size']['label'] ) ? $config['size']['label'] : '',
			__( 'Spreads', 'mark-pacura-album-builder' )      => isset( $config['spreads'] ) ? $config['spreads'] . ' (' . ( isset( $config['pages'] ) ? $config['pages'] : $config['spreads'] * 2 ) . ' pages)' : '',
			__( 'Cover material', 'mark-pacura-album-builder' ) => isset( $config['cover_material']['label'] ) ? $config['cover_material']['label'] : '',
			__( 'Cover', 'mark-pacura-album-builder' )        => isset( $config['cover']['label'] ) ? $config['cover']['label'] : '',
			__( 'Personalisation', 'mark-pacura-album-builder' ) => isset( $config['personalisation']['label'] ) ? $config['personalisation']['label'] : '',
			__( 'Personalisation category', 'mark-pacura-album-builder' ) => 'none' !== $personalisation_id && isset( $config['personalisation_category']['label'] ) ? $config['personalisation_category']['label'] : '',
			__( 'Wording', 'mark-pacura-album-builder' )      => isset( $config['personalisation_category']['id'] ) && 'text' === $config['personalisation_category']['id'] && isset( $config['personal_text'] ) ? $config['personal_text'] : '',
			__( 'Font', 'mark-pacura-album-builder' )         => isset( $config['personalisation_category']['id'] ) && 'text' === $config['personalisation_category']['id'] && isset( $config['personalisation_font']['label'] ) ? $config['personalisation_font']['label'] : '',
			__( 'Design', 'mark-pacura-album-builder' )       => isset( $config['personalisation_category']['id'] ) && in_array( $config['personalisation_category']['id'], array( 'wb-design', 'full-cover' ), true ) && isset( $config['personalisation_design']['label'] ) ? $config['personalisation_design']['label'] : '',
			__( 'Foil colour', 'mark-pacura-album-builder' )  => 'foil' === $personalisation_id && isset( $config['foil_colour']['label'] ) ? $config['foil_colour']['label'] : '',
			__( 'Presentation', 'mark-pacura-album-builder' ) => isset( $config['box']['label'] ) ? $config['box']['label'] : '',
			__( 'Wooden box colour', 'mark-pacura-album-builder' ) => isset( $config['box']['id'], $config['box_colour']['label'] ) && 'wooden' === $config['box']['id'] ? $config['box_colour']['label'] : '',
			__( 'Usual total', 'mark-pacura-album-builder' )  => $promotion_applied && isset( $config['normal_total'] ) ? $this->format_price( $config['normal_total'], isset( $config['currency_symbol'] ) ? $config['currency_symbol'] : '' ) : '',
			__( 'Promotion', 'mark-pacura-album-builder' )    => $promotion_summary,
			__( 'You save', 'mark-pacura-album-builder' )     => $promotion_applied && isset( $config['promotion']['discount'] ) ? $this->format_price( $config['promotion']['discount'], isset( $config['currency_symbol'] ) ? $config['currency_symbol'] : '' ) : '',
			__( 'Total', 'mark-pacura-album-builder' )        => isset( $config['total'] ) ? $this->format_price( $config['total'], isset( $config['currency_symbol'] ) ? $config['currency_symbol'] : '' ) : '',
			__( 'Client confirmation email', 'mark-pacura-album-builder' ) => $client_email_label,
			__( 'Client email error', 'mark-pacura-album-builder' ) => get_post_meta( $post->ID, '_mpab_client_email_error', true ),
			__( 'Order intent', 'mark-pacura-album-builder' ) => isset( $config['order_intent_at'] ) ? sprintf( __( 'Client confirmed their choice on %s', 'mark-pacura-album-builder' ), $config['order_intent_at'] ) : '',
			__( 'Status', 'mark-pacura-album-builder' )       => ucfirst( (string) get_post_meta( $post->ID, '_mpab_status', true ) ),
		);
		echo '<table class="widefat striped"><tbody>';
		foreach ( $rows as $label => $value ) {
			if ( '' === (string) $value ) {
				continue;
			}
			echo '<tr><th style="width:180px">' . esc_html( $label ) . '</th><td>' . esc_html( $value ) . '</td></tr>';
		}
		echo '</tbody></table>';
		if ( '1' === $resend_result ) {
			echo '<p class="notice notice-success inline"><strong>' . esc_html__( 'A request has been sent to resend the client confirmation email.', 'mark-pacura-album-builder' ) . '</strong></p>';
		} elseif ( '0' === $resend_result ) {
			echo '<p class="notice notice-error inline"><strong>' . esc_html__( 'The confirmation email could not be sent. Please review the error message above or contact your website provider to check outgoing email settings.', 'mark-pacura-album-builder' ) . '</strong></p>';
		}
		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="mpab-resend-client-email"><input type="hidden" name="action" value="mpab_resend_client_email"><input type="hidden" name="post_id" value="' . absint( $post->ID ) . '">';
		wp_nonce_field( 'mpab_resend_client_email_' . $post->ID );
		submit_button( __( 'Resend client confirmation', 'mark-pacura-album-builder' ), 'secondary', 'submit', false );
		echo '<p class="description">' . esc_html__( 'Resend the confirmation email if your client has not received it. If delivery fails, verify your website\'s outgoing email settings with your host.', 'mark-pacura-album-builder' ) . '</p></form>';
		echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" class="mpab-remove-order" onsubmit="return window.confirm(\'' . esc_js( __( 'Remove this order? It will move to Trash, where you can restore it if needed.', 'mark-pacura-album-builder' ) ) . '\');"><input type="hidden" name="action" value="mpab_remove_order"><input type="hidden" name="post_id" value="' . absint( $post->ID ) . '">';
		wp_nonce_field( 'mpab_remove_order_' . $post->ID );
		submit_button( __( 'Remove order', 'mark-pacura-album-builder' ), 'secondary mpab-remove-order__button', 'submit', false );
		echo '<p class="description">' . esc_html__( 'This moves the order to Trash, where it can be recovered if needed.', 'mark-pacura-album-builder' ) . '</p></form>';
		$note = get_post_meta( $post->ID, '_mpab_client_note', true );
		if ( $note ) {
			echo '<h3>' . esc_html__( 'Client note', 'mark-pacura-album-builder' ) . '</h3><p>' . nl2br( esc_html( $note ) ) . '</p>';
		}
	}

	/**
	 * Custom request-list columns.
	 *
	 * @param array $columns Default columns.
	 * @return array
	 */
	public function submission_columns( $columns ) {
		return array(
			'cb'        => isset( $columns['cb'] ) ? $columns['cb'] : '<input type="checkbox" />',
			'title'     => __( 'Request', 'mark-pacura-album-builder' ),
			'mpab_email'=> __( 'Client email', 'mark-pacura-album-builder' ),
			'mpab_album'=> __( 'Album', 'mark-pacura-album-builder' ),
			'mpab_total'=> __( 'Total', 'mark-pacura-album-builder' ),
			'mpab_status'=> __( 'Status', 'mark-pacura-album-builder' ),
			'date'      => __( 'Date', 'mark-pacura-album-builder' ),
		);
	}

	/**
	 * Render request-list values.
	 *
	 * @param string $column Column name.
	 * @param int    $post_id Submission ID.
	 * @return void
	 */
	public function render_submission_column( $column, $post_id ) {
		$config = json_decode( (string) get_post_meta( $post_id, '_mpab_configuration', true ), true );
		$config = is_array( $config ) ? $config : array();
		if ( 'mpab_email' === $column ) {
			echo esc_html( get_post_meta( $post_id, '_mpab_client_email', true ) );
		} elseif ( 'mpab_album' === $column ) {
			echo esc_html( isset( $config['size']['label'] ) ? $config['size']['label'] : '' );
		} elseif ( 'mpab_total' === $column ) {
			echo esc_html( isset( $config['total'] ) ? $this->format_price( $config['total'], isset( $config['currency_symbol'] ) ? $config['currency_symbol'] : '' ) : '' );
		} elseif ( 'mpab_status' === $column ) {
			$status = (string) get_post_meta( $post_id, '_mpab_status', true );
			$status = $status ? $status : 'new';
			echo '<span class="mpab-order-status mpab-order-status--' . esc_attr( sanitize_html_class( $status ) ) . '">' . esc_html( ucfirst( $status ) ) . '</span>';
		}
	}
}
