<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * The Design Tokens page: art direction, palette, fonts and structural tokens.
 *
 * These are the plugin's own design tokens (the `--aipb-*` namespace), independent of the theme:
 * the AI uses them to generate consistently, in the preview and on the frontend.
 */
class AIPB_Admin_Brand {

	/** The groups of structural tokens, in the order they are shown. */
	const GROUPS = array( 'text', 'space', 'radius', 'shadow', 'layout', 'motion' );

	/** Saves only the real overrides (values different from the default → defaults stay dynamic). */
	public static function sanitize_tokens( $input ) {
		$defaults = AIPB_Design::flat_defaults();
		$allowed  = $defaults + self::submitted_custom_ids();
		$out      = array();

		if ( is_array( $input ) ) {
			foreach ( $input as $id => $val ) {
				$id  = sanitize_text_field( (string) $id );
				$val = AIPB_Design::with_unit( AIPB_Design::sanitize_value( $val ), $id );
				if ( '' === $val || ! isset( $allowed[ $id ] ) ) {
					continue;
				}
				if ( isset( $defaults[ $id ] ) && $defaults[ $id ] === $val ) {
					continue; // same as the default: do not save it, so it stays dynamic
				}
				$out[ $id ] = $val;
			}
		}

		return $out;
	}

	/**
	 * The ids of the custom tokens as they are BEING SAVED, not as they were.
	 *
	 * The order in which options.php sanitises the settings of one group is not ours to choose: a
	 * token declared in this very submission may well be sanitised after its value, and reading the
	 * registry from the database would then throw the value away — the user would type a name and a
	 * value together, and only the name would survive the round trip.
	 *
	 * The nonce was verified by options.php before any of these callbacks ran.
	 *
	 * @return array<string,string> id => '' (a set, shaped like flat_defaults so the two can merge)
	 */
	protected static function submitted_custom_ids() {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing — options.php's nonce is already verified
		$raw = isset( $_POST[ AIPB_Design::OPT_CUSTOM ] ) ? wp_unslash( $_POST[ AIPB_Design::OPT_CUSTOM ] ) : null;

		$list = is_array( $raw ) ? AIPB_Design::sanitize_custom( $raw ) : AIPB_Design::custom_tokens();
		$out  = array();

		foreach ( $list as $token ) {
			$out[ $token['group'] . ':' . $token['key'] ] = '';
		}

		return $out;
	}

	/** The format's version, written into every export and checked on the way back in. */
	const FILE_VERSION = 1;

	/**
	 * The design system as a file.
	 *
	 * WHY. An agency rebuilds the same brand by hand on every client site, and a customer who wants
	 * to keep a copy of their own design system outside the database has, today, no way to. Both are
	 * one JSON away. It is also the shortest road to starting points: a bundle is a bundle, whoever
	 * wrote it.
	 *
	 * It carries no key, no URL and nothing about the licence — a design system is colours, fonts,
	 * numbers and a paragraph of prose, and it is meant to be handed around.
	 */
	public static function handle_export() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'aipb' ), '', array( 'response' => 403 ) );
		}
		check_admin_referer( 'aipb_brand_export' );

		$payload = array(
			'_format'   => 'aura-design-system',
			'_version'  => self::FILE_VERSION,
			'_exported' => gmdate( 'c' ),
			'_site'     => get_bloginfo( 'name' ),
			'design'    => AIPB_Design::bundle(),
		);

		$name = sanitize_file_name( 'aura-design-' . wp_date( 'Y-m-d' ) . '.json' );

		nocache_headers();
		header( 'Content-Type: application/json; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="' . $name . '"' );

		echo wp_json_encode( $payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
		exit;
	}

	/**
	 * Everything the versions panel can do: take a snapshot, go back to one, forget one, read a file
	 * back in.
	 *
	 * One handler and one nonce because they are one form: four buttons around the same list.
	 */
	public static function handle_versions() {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'aipb' ), '', array( 'response' => 403 ) );
		}
		check_admin_referer( 'aipb_brand_versions' );

		$done = '';

		if ( isset( $_POST['aipb_snapshot'] ) ) {
			AIPB_Design::snapshot( isset( $_POST['aipb_label'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['aipb_label'] ) ) : '' );
			$done = 'saved';
		} elseif ( isset( $_POST['aipb_restore'] ) ) {
			$done = AIPB_Design::restore_snapshot( sanitize_text_field( wp_unslash( (string) $_POST['aipb_restore'] ) ) ) ? 'restored' : 'gone';
		} elseif ( isset( $_POST['aipb_forget'] ) ) {
			AIPB_Design::forget_snapshot( sanitize_text_field( wp_unslash( (string) $_POST['aipb_forget'] ) ) );
			$done = 'forgotten';
		} elseif ( isset( $_POST['aipb_import'] ) ) {
			$done = self::import_file();
		}

		wp_safe_redirect( add_query_arg(
			array( 'page' => 'aipb-brand', 'aipb_done' => $done ),
			admin_url( 'edit.php?post_type=' . AIPB_CPT::POST_TYPE )
		) );
		exit;
	}

	/**
	 * Reads an exported design system back in.
	 *
	 * A copy of where you are is taken FIRST: importing replaces the palette, the fonts, the tokens
	 * and the direction at once, and somebody who picked the wrong file needs one click back, not an
	 * apology.
	 *
	 * @return string the outcome, for the notice
	 */
	protected static function import_file() {
		if ( empty( $_FILES['aipb_file']['tmp_name'] ) || ! is_uploaded_file( $_FILES['aipb_file']['tmp_name'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
			return 'nofile';
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_get_contents
		$raw = file_get_contents( $_FILES['aipb_file']['tmp_name'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
		$data = json_decode( (string) $raw, true );

		if ( ! is_array( $data ) || ! isset( $data['design'] ) || ! is_array( $data['design'] ) ) {
			return 'badfile';
		}

		// A format from the future is refused rather than half-read: the fields it would be missing
		// are the ones this version does not know it needs.
		if ( isset( $data['_version'] ) && (int) $data['_version'] > self::FILE_VERSION ) {
			return 'newer';
		}

		AIPB_Design::snapshot( __( 'before the import', 'aipb' ) );

		return AIPB_Design::restore_bundle( $data['design'] ) ? 'imported' : 'badfile';
	}

	/** A signed address for an action on the Brand page. */
	protected static function url( $args, $nonce ) {
		return wp_nonce_url(
			add_query_arg( array_merge( array( 'page' => 'aipb-brand' ), $args ), admin_url( 'edit.php?post_type=' . AIPB_CPT::POST_TYPE ) ),
			$nonce
		);
	}

	public static function render() {
		// The reset in TWO steps: the first shows what breaks, the second does it. The link coming
		// from the page carries 'aipb_reset=1' and stops here; only 'go' deletes.
		if ( isset( $_GET['aipb_reset'] ) && check_admin_referer( 'aipb_reset_brand' ) ) {
			if ( 'go' !== $_GET['aipb_reset'] ) {
				self::render_reset_confirm();
				return;
			}

			// The copy taken BEFORE the deletion: it is all that makes the action reversible.
			AIPB_Design::save_backup();
			delete_option( AIPB_Design::OPT_TOKENS );
			delete_option( AIPB_Design::OPT_PALETTE );
			delete_option( AIPB_Design::OPT_FONTS );
			delete_option( AIPB_Design::OPT_DIRECTION );
			echo '<div class="notice notice-warning"><p>' . esc_html__( 'Brand reset: palette, direction and fonts emptied, and your overrides on the structural tokens dropped — those go back to the plugin\'s scale, so they stay defined. Until you set new colours and fonts, the sections that referred to them lose the rules that used them.', 'aipb' ) . '</p></div>';
		}

		// The undo: it puts the tokens back as they were.
		if ( isset( $_GET['aipb_reset_undo'] ) && check_admin_referer( 'aipb_reset_undo' ) ) {
			echo '<div class="notice notice-' . ( AIPB_Design::restore_backup() ? 'success' : 'error' ) . ' is-dismissible"><p>'
				. esc_html__( 'Tokens restored as they were before the reset.', 'aipb' ) . '</p></div>';
		}

		self::render_done_notice();
		self::render_undo_notice();

		// The rejections from the effects validator (see AIPB_Effects::sanitize): on a custom admin
		// page nobody prints them for us, and a save that silently dropped an effect is the one
		// notice this page cannot afford to skip.
		settings_errors( AIPB_Effects::OPT );

		self::render_form();
	}

	/**
	 * The reset's confirmation screen.
	 *
	 * WHY IT EXISTS. On 9 August 2026 a reset emptied the tokens of a site with six published pages,
	 * and printed a GREEN success notice. The pages emptied themselves (262 references to variables
	 * that no longer existed, with no fallbacks), and regenerating them was no use. A destructive
	 * action that behaves like a successful one is the worst possible interface: it does not even
	 * give you the suspicion to start looking from.
	 *
	 * It says two things it did not say before: WHO breaks (by name, and only who really breaks) and
	 * that you can go back.
	 */
	protected static function render_reset_confirm() {
		$at_risk = AIPB_Design::dependents();
		$go      = self::url( array( 'aipb_reset' => 'go' ), 'aipb_reset_brand' );
		$back    = add_query_arg( array( 'page' => 'aipb-brand' ), admin_url( 'edit.php?post_type=' . AIPB_CPT::POST_TYPE ) );
		?>
		<div class="wrap aipb-brand">
			<h1><?php esc_html_e( 'Reset the brand?', 'aipb' ); ?></h1>

			<div class="notice notice-warning inline" style="padding:12px 14px;margin:16px 0">
				<p><?php echo wp_kses_post( __( 'This empties the <b>palette, fonts and art direction</b>, and drops your overrides on the <b>structural tokens</b> — spacing, text sizes, radii and shadows go back to the plugin\'s scale rather than disappearing. The pages already created are <b>not</b> touched: they go on referring to the tokens by their names.', 'aipb' ) ); ?></p>

				<?php if ( $at_risk ) : ?>
					<p><strong><?php
						echo esc_html( sprintf(
							/* translators: %d: number of contents. */
							_n( '%d content will break.', '%d contents will break.', count( $at_risk ), 'aipb' ),
							count( $at_risk )
						) );
					?></strong> <?php echo wp_kses_post( __( 'They contain CSS rules pointing at a token <b>with no fallback value</b>: without the token those declarations are not worth zero — they are discarded entirely, and the page empties itself.', 'aipb' ) ); ?></p>
					<ul style="margin:8px 0 8px 18px;list-style:disc">
						<?php foreach ( array_slice( $at_risk, 0, 10 ) as $p ) : ?>
							<li>
								<a href="<?php echo esc_url( (string) get_edit_post_link( $p['id'] ) ); ?>"><?php echo esc_html( '' !== $p['title'] ? $p['title'] : '#' . $p['id'] ); ?></a>
								<span class="aipb-muted">— <?php
									echo esc_html( sprintf(
										/* translators: %d: number of references. */
										_n( '%d reference with no fallback', '%d references with no fallback', $p['bare'], 'aipb' ),
										$p['bare']
									) );
								?></span>
							</li>
						<?php endforeach; ?>
						<?php if ( count( $at_risk ) > 10 ) : ?>
							<li class="aipb-muted"><?php
								/* translators: %d: how many more contents there are. */
								echo esc_html( sprintf( __( '…and %d more.', 'aipb' ), count( $at_risk ) - 10 ) );
							?></li>
						<?php endif; ?>
					</ul>
					<p><?php echo wp_kses_post( __( 'To put them out of harm\'s way first: <b>reopen and re-save</b> each one from the Studio. On saving, the engine sews into every <code>var()</code> the value the token has right now, and from then on the reset does not touch them.', 'aipb' ) ); ?></p>
				<?php else : ?>
					<p><strong><?php esc_html_e( 'Nothing will break.', 'aipb' ); ?></strong> <?php echo wp_kses_post( __( 'Every saved section already carries its own fallback value inside each <code>var()</code>: they will stay exactly as they were.', 'aipb' ) ); ?></p>
				<?php endif; ?>

				<p><?php echo wp_kses_post( __( 'The current tokens are set aside: after the reset you will find a button here to <b>undo</b> it.', 'aipb' ) ); ?></p>
			</div>

			<p>
				<a href="<?php echo esc_url( $go ); ?>" class="button button-primary"><?php esc_html_e( 'Reset the brand', 'aipb' ); ?></a>
				<a href="<?php echo esc_url( $back ); ?>" class="button"><?php esc_html_e( 'Leave it alone', 'aipb' ); ?></a>
			</p>
		</div>
		<?php
	}

	/**
	 * The undo button stays as long as the copy is there: whoever resets notices the damage by
	 * looking at the site, that is after leaving this page — and has to find it again here.
	 */
	protected static function render_undo_notice() {
		$backup = AIPB_Design::backup();
		if ( ! $backup ) {
			return;
		}

		echo '<div class="notice notice-info"><p>'
			. esc_html( sprintf(
				/* translators: %s: date and time. */
				__( 'There is a copy of the tokens from before the reset (%s).', 'aipb' ),
				wp_date( 'j F Y, H:i', (int) $backup['when'] )
			) )
			. ' <a class="button button-small" href="' . esc_url( self::url( array( 'aipb_reset_undo' => 1 ), 'aipb_reset_undo' ) ) . '">'
			. esc_html__( 'Undo the reset', 'aipb' ) . '</a></p></div>';
	}

	protected static function render_form() {
		$groups    = AIPB_Design::groups();
		$reset_url = self::url( array( 'aipb_reset' => 1 ), 'aipb_reset_brand' );
		?>
		<div class="wrap aipb-brand">
			<div class="aipb-brand-head">
				<h1><?php esc_html_e( 'Brand & Design', 'aipb' ); ?></h1>
				<p class="description"><?php echo wp_kses_post( __( 'The plugin\'s <b>design tokens</b> (the <code>--aipb-*</code> namespace), independent of the theme: the AI uses them to generate consistently, in the preview and on the frontend.', 'aipb' ) ); ?></p>
			</div>

			<?php /*
			 * TWO COLUMNS, and which side things go on is the whole point.
			 *
			 * Every control on this page changes a number, and the only way to judge a number in a
			 * design system is to see what it does to the others. With the specimen below the token
			 * grid you edited a value, scrolled, looked, scrolled back — which is not judging, it is
			 * remembering. Side by side, and with the preview pinned so it follows you down the
			 * grid, editing and seeing become the same gesture.
			 *
			 * Below 1200px there is not room for two useful columns, and it goes back to one.
			 */ ?>
			<div class="aipb-brand-cols">
			<div class="aipb-brand-main">

			<form method="post" action="options.php" id="aipb-brand-form">
				<?php settings_fields( 'aipb_brand' ); ?>

				<section class="aipb-panel aipb-import" id="aipb-import">
					<div class="aipb-panel-head">
						<div>
							<h2><?php esc_html_e( 'Import from a mockup', 'aipb' ); ?> <span class="aipb-muted"><?php esc_html_e( '(with the AI)', 'aipb' ); ?></span></h2>
							<p class="aipb-panel-sub"><?php echo wp_kses_post( __( 'Upload the mockup\'s screenshots — <b>4-5 different pages</b> give the best result, because the AI recognises the values that repeat and tells them apart from the exceptions. It measures the palette, fonts, type scale, spacing, radii, shadows and widths, and fills in the fields below. <b>Nothing is saved</b> until you press «Save design system»: you can review and correct everything first.', 'aipb' ) ); ?></p>
						</div>
					</div>

					<?php /*
					 * The other road in: an address.
					 *
					 * `/v1/capture` opens the page in a real browser and brings back the styles the
					 * cascade RESOLVED — the numbers themselves, not an inference over a JPEG — and it
					 * costs no credit of its own. For measuring a type scale that is a different class
					 * of evidence from a screenshot, so it goes first.
					 */ ?>
					<div class="aipb-imp-url">
						<label class="aipb-imp-url-lbl" for="aipb-imp-url"><?php esc_html_e( 'From a live page', 'aipb' ); ?></label>
						<div class="aipb-imp-url-row">
							<input type="url" id="aipb-imp-url" class="aipb-imp-url-in" placeholder="https://…" spellcheck="false" autocomplete="off">
							<button type="button" class="button" id="aipb-imp-url-run"><?php esc_html_e( 'Measure this page', 'aipb' ); ?></button>
						</div>
						<p class="aipb-imp-url-hint"><?php echo wp_kses_post( __( 'Aura opens the address in a real browser and reads the values the cascade resolved — sizes, radii, shadows, the fonts actually loaded. It is <b>more accurate than a screenshot</b>, because nothing has to be measured off an image. Public addresses only.', 'aipb' ) ); ?></p>
					</div>

					<div class="aipb-imp-or"><span><?php esc_html_e( 'or from images', 'aipb' ); ?></span></div>

					<div class="aipb-imp-drop" id="aipb-imp-drop" tabindex="0" role="button" aria-label="<?php esc_attr_e( 'Choose the mockup\'s screenshots', 'aipb' ); ?>">
						<input type="file" id="aipb-imp-file" accept="image/png,image/jpeg,image/webp" multiple hidden>
						<span class="aipb-imp-drop-ico" aria-hidden="true">🖼</span>
						<span class="aipb-imp-drop-txt">
							<b><?php esc_html_e( 'Drag the screenshots here', 'aipb' ); ?></b>
							<?php esc_html_e( 'or click to choose the files — up to 5, PNG or JPG', 'aipb' ); ?>
						</span>
					</div>

					<div class="aipb-imp-thumbs" id="aipb-imp-thumbs"></div>

					<label class="aipb-imp-notes-lbl" for="aipb-imp-notes"><?php esc_html_e( 'Notes for the AI (optional)', 'aipb' ); ?></label>
					<input type="text" id="aipb-imp-notes" class="large-text" placeholder="<?php esc_attr_e( 'E.g. «the dark green is the brand colour, the headings font looks like Playfair»', 'aipb' ); ?>">

					<div class="aipb-imp-actions">
						<button type="button" class="button button-primary" id="aipb-imp-run" disabled><?php esc_html_e( 'Analyse the mockups', 'aipb' ); ?></button>
						<span class="aipb-imp-status" id="aipb-imp-status" aria-live="polite"></span>
					</div>

					<div class="aipb-imp-result" id="aipb-imp-result" hidden></div>
				</section>

				<section class="aipb-panel">
					<div class="aipb-panel-head">
						<div>
							<h2><?php esc_html_e( 'Art direction', 'aipb' ); ?> <span class="aipb-muted">(<?php esc_html_e( 'the whole site', 'aipb' ); ?>)</span></h2>
							<p class="aipb-panel-sub"><?php echo wp_kses_post( __( 'A brief in plain language that guides <b>every</b> AI generation: mood, tone, density, rhythm, how to use the brand colours, copy style. E.g. <i>"Editorial and calm, plenty of air, sharp corners and no shadows, confident but understated tone."</i> It touches neither the palette nor the fonts: it decides only <b>how</b> the AI designs.', 'aipb' ) ); ?></p>
						</div>
					</div>
					<textarea name="<?php echo esc_attr( AIPB_Design::OPT_DIRECTION ); ?>" id="aipb-direction" rows="4" class="large-text" maxlength="<?php echo (int) AIPB_Design::DIRECTION_MAX; ?>" placeholder="<?php esc_attr_e( 'Describe the site\'s visual character: mood, tone, density, copy style…', 'aipb' ); ?>"><?php echo esc_textarea( AIPB_Design::direction() ); ?></textarea>
				</section>

				<?php self::render_manifest(); ?>

				<section class="aipb-panel">
					<div class="aipb-panel-head">
						<div>
							<h2><?php esc_html_e( 'Brand palette', 'aipb' ); ?></h2>
							<p class="aipb-panel-sub"><?php echo wp_kses_post( __( 'Add only the colours you need and name them however you like — three is fine. <b>You do not assign roles</b>: the AI works out where to apply them (backgrounds, text, CTAs, accents) from the name, the hue and the contrast.', 'aipb' ) ); ?></p>
						</div>
						<button type="button" class="button button-primary aipb-add" id="aipb-add-color">+ <?php esc_html_e( 'Add colour', 'aipb' ); ?></button>
					</div>

					<?php // The editor is built by brand.js from AIPB_BRAND.palette. ?>
					<div id="aipb-palette" class="aipb-palette" aria-live="polite"></div>

					<noscript><p class="description"><?php esc_html_e( 'Enable JavaScript to edit the palette.', 'aipb' ); ?></p></noscript>
				</section>

				<section class="aipb-panel">
					<div class="aipb-panel-head">
						<div>
							<h2><?php esc_html_e( 'Typography', 'aipb' ); ?></h2>
							<p class="aipb-panel-sub"><?php echo wp_kses_post( __( 'Import the fonts from <b>Google Fonts</b> and give each one a <b>name that suggests its use</b> (e.g. "Headings", "Body", "Quotes"). You do not assign roles: the AI works out where to apply them. For each one, choose the weights to load.', 'aipb' ) ); ?></p>
						</div>
						<button type="button" class="button button-primary aipb-add" id="aipb-add-font">+ <?php esc_html_e( 'Add font', 'aipb' ); ?></button>
					</div>
					<?php // The fonts editor is built by brand.js. ?>
					<div id="aipb-fonts" class="aipb-fonts" aria-live="polite"></div>

					<?php // What all this costs a visitor, and where the files come from. ?>
					<div class="aipb-fw" id="aipb-fw" hidden>
						<div class="aipb-fw-cost">
							<span class="aipb-fw-num" id="aipb-fw-num">—</span>
							<span class="aipb-fw-lbl" id="aipb-fw-lbl"></span>
						</div>
						<p class="aipb-fw-warn" id="aipb-fw-warn" hidden></p>
					</div>

					<?php self::render_font_hosting(); ?>
				</section>

				<?php self::render_token_grid( $groups ); ?>

				<?php self::render_effects(); ?>

			</form>

			<?php self::render_versions(); ?>

			</div><?php // .aipb-brand-main ?>

			<?php /*
			 * The specimen sits OUTSIDE the settings form on purpose. It holds one control — the dark
			 * background switch — and that is a way of looking, not a thing to save: no name, nothing
			 * submitted, and the unsaved-changes guard rightly ignores it.
			 */ ?>
			<aside class="aipb-brand-side">
				<?php self::render_specimen(); ?>
			</aside>

			</div><?php // .aipb-brand-cols ?>

			<?php self::render_bar( $reset_url ); ?>
		</div>
		<?php
	}

	/**
	 * The bar at the bottom of the screen.
	 *
	 * IT IS FIXED, not merely sticky, and that is the point. This page is now two tall columns and
	 * the only two irreversible-feeling actions on it — saving and resetting — used to live at the
	 * end of one of them. You could change a colour, scroll through the token grid, walk off to the
	 * Studio, and lose the lot without a word.
	 *
	 * IT ALSO CARRIES THE HISTORY, and that is what changes the way the page is used. A design
	 * system is judged by trying things: widen the container, look, put it back. Without an undo the
	 * only way back was to remember the number — so nobody tried anything, they typed once and
	 * hoped. Ctrl/⌘+Z does what it does everywhere else.
	 *
	 * The Save button lives OUTSIDE the settings form and reaches it with the `form` attribute: the
	 * bar has to be a child of nothing in particular to be positioned against the viewport.
	 */
	protected static function render_bar( $reset_url ) {
		?>
		<div class="aipb-bar" id="aipb-bar">
			<div class="aipb-bar-history">
				<button type="button" class="aipb-hist" id="aipb-undo" disabled
					title="<?php esc_attr_e( 'Undo (Ctrl/⌘ Z)', 'aipb' ); ?>" aria-label="<?php esc_attr_e( 'Undo', 'aipb' ); ?>">
					<svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M6 3.5 2.5 7 6 10.5M2.9 7h6.4a4 4 0 0 1 0 8H6.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
				</button>
				<button type="button" class="aipb-hist" id="aipb-redo" disabled
					title="<?php esc_attr_e( 'Redo (Ctrl/⌘ ⇧ Z)', 'aipb' ); ?>" aria-label="<?php esc_attr_e( 'Redo', 'aipb' ); ?>">
					<svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M10 3.5 13.5 7 10 10.5M13.1 7H6.7a4 4 0 0 0 0 8h2.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
				</button>
				<span class="aipb-hist-note" id="aipb-hist-note" hidden></span>
			</div>

			<span class="aipb-dirty" id="aipb-dirty" hidden></span>

			<div class="aipb-bar-actions">
				<a href="<?php echo esc_url( $reset_url ); ?>" class="aipb-reset" onclick="return confirm('<?php echo esc_js( __( 'Empty the palette, fonts and direction, and put the structural tokens back to the plugin\'s scale?', 'aipb' ) ); ?>')">↺ <?php esc_html_e( 'Reset', 'aipb' ); ?></a>
				<?php submit_button( __( 'Save design system', 'aipb' ), 'primary large', 'submit', false, array( 'form' => 'aipb-brand-form' ) ); ?>
			</div>
		</div>
		<?php
	}

	/** The outcome of the last action on the versions panel. */
	protected static function render_done_notice() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended — a read-only notice keyed off a redirect
		$done = isset( $_GET['aipb_done'] ) ? sanitize_key( wp_unslash( (string) $_GET['aipb_done'] ) ) : '';
		if ( '' === $done ) {
			return;
		}

		$messages = array(
			'saved'     => array( 'success', __( 'Snapshot taken.', 'aipb' ) ),
			'restored'  => array( 'success', __( 'Design system restored. A copy of where you were was taken first, so this is undoable too.', 'aipb' ) ),
			'forgotten' => array( 'info', __( 'Snapshot deleted.', 'aipb' ) ),
			'imported'  => array( 'success', __( 'Design system imported. A copy of where you were was taken first.', 'aipb' ) ),
			'gone'      => array( 'error', __( 'That snapshot is no longer there.', 'aipb' ) ),
			'nofile'    => array( 'error', __( 'No file arrived: choose one and try again.', 'aipb' ) ),
			'badfile'   => array( 'error', __( 'That file is not an Aura design system.', 'aipb' ) ),
			'newer'     => array( 'error', __( 'That file was written by a newer version of the plugin: update Aura and import it again.', 'aipb' ) ),
		);

		if ( ! isset( $messages[ $done ] ) ) {
			return;
		}

		printf(
			'<div class="notice notice-%s is-dismissible"><p>%s</p></div>',
			esc_attr( $messages[ $done ][0] ),
			esc_html( $messages[ $done ][1] )
		);
	}

	/**
	 * Versions: snapshots, and the design system as a file.
	 *
	 * Two things that look different and are the same one — a bundle of the whole brand, written
	 * somewhere it can be read back. One goes into an option, the other into a download.
	 */
	protected static function render_versions() {
		$snapshots = AIPB_Design::snapshots();
		?>
		<section class="aipb-panel aipb-versions">
			<div class="aipb-panel-head">
				<div>
					<h2><?php esc_html_e( 'Versions', 'aipb' ); ?></h2>
					<p class="aipb-panel-sub"><?php echo wp_kses_post( __( 'A design system is iterated, not written once. Take a <b>snapshot</b> before trying something and you can come back to it; going back takes a snapshot of where you were first, so that too is undoable. <b>Export</b> writes the whole brand — colours, fonts, tokens, direction — into a file that another site can read: it carries nothing about your licence.', 'aipb' ) ); ?></p>
				</div>
			</div>

			<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" enctype="multipart/form-data" class="aipb-ver-form">
				<?php wp_nonce_field( 'aipb_brand_versions' ); ?>
				<input type="hidden" name="action" value="aipb_brand_versions">

				<div class="aipb-ver-take">
					<input type="text" name="aipb_label" class="aipb-ver-label" maxlength="60"
						placeholder="<?php esc_attr_e( 'What is this version? e.g. «before the restyling»', 'aipb' ); ?>">
					<button type="submit" name="aipb_snapshot" value="1" class="button button-primary"><?php esc_html_e( 'Take a snapshot', 'aipb' ); ?></button>
				</div>

				<?php if ( $snapshots ) : ?>
					<ul class="aipb-ver-list">
						<?php foreach ( $snapshots as $snap ) :
							$label = isset( $snap['label'] ) ? (string) $snap['label'] : '';
							$when  = isset( $snap['when'] ) ? (int) $snap['when'] : 0;
							$who   = isset( $snap['user'] ) ? get_userdata( (int) $snap['user'] ) : null;
							?>
							<li>
								<span class="aipb-ver-name"><?php echo esc_html( '' !== $label ? $label : __( 'Untitled', 'aipb' ) ); ?></span>
								<span class="aipb-ver-when">
									<?php echo esc_html( $when ? wp_date( 'j M Y, H:i', $when ) : '—' ); ?>
									<?php if ( $who ) : ?>· <?php echo esc_html( $who->display_name ); ?><?php endif; ?>
									<?php echo esc_html( self::snapshot_summary( isset( $snap['bundle'] ) ? $snap['bundle'] : array() ) ); ?>
								</span>
								<button type="submit" name="aipb_restore" value="<?php echo esc_attr( isset( $snap['id'] ) ? $snap['id'] : '' ); ?>" class="button button-small"><?php esc_html_e( 'Go back to this', 'aipb' ); ?></button>
								<button type="submit" name="aipb_forget" value="<?php echo esc_attr( isset( $snap['id'] ) ? $snap['id'] : '' ); ?>" class="aipb-ver-drop" aria-label="<?php esc_attr_e( 'Delete this snapshot', 'aipb' ); ?>" title="<?php esc_attr_e( 'Delete this snapshot', 'aipb' ); ?>">×</button>
							</li>
						<?php endforeach; ?>
					</ul>
					<?php if ( count( $snapshots ) >= AIPB_Design::SNAPSHOTS_MAX ) : ?>
						<p class="aipb-ver-full"><?php echo esc_html( sprintf(
							/* translators: %d: the maximum number of snapshots kept. */
							__( '%d snapshots are kept: taking another drops the oldest.', 'aipb' ),
							AIPB_Design::SNAPSHOTS_MAX
						) ); ?></p>
					<?php endif; ?>
				<?php else : ?>
					<p class="aipb-ver-empty"><?php esc_html_e( 'No snapshot yet. One is taken automatically before a reset, before an import and before going back to an older version.', 'aipb' ); ?></p>
				<?php endif; ?>

				<div class="aipb-ver-file">
					<a class="button" href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'action', 'aipb_brand_export', admin_url( 'admin-post.php' ) ), 'aipb_brand_export' ) ); ?>">↓ <?php esc_html_e( 'Export as a file', 'aipb' ); ?></a>
					<span class="aipb-ver-sep"></span>
					<input type="file" name="aipb_file" id="aipb-ver-file" accept="application/json,.json">
					<button type="submit" name="aipb_import" value="1" class="button"
						onclick="return confirm('<?php echo esc_js( __( 'Importing replaces the palette, the fonts, the tokens and the direction. A snapshot of the current ones is taken first. Go ahead?', 'aipb' ) ); ?>')">↑ <?php esc_html_e( 'Import a file', 'aipb' ); ?></button>
				</div>
			</form>
		</section>
		<?php
	}

	/** What a snapshot actually holds, in one line: it is what tells two of them apart. */
	protected static function snapshot_summary( $bundle ) {
		$colours = isset( $bundle['palette'] ) && is_array( $bundle['palette'] ) ? count( $bundle['palette'] ) : 0;
		$fonts   = isset( $bundle['fonts'] ) && is_array( $bundle['fonts'] ) ? count( $bundle['fonts'] ) : 0;
		$tokens  = isset( $bundle['tokens'] ) && is_array( $bundle['tokens'] ) ? count( $bundle['tokens'] ) : 0;

		return ' · ' . sprintf(
			/* translators: 1: number of colours, 2: number of fonts, 3: number of token overrides. */
			__( '%1$d colours, %2$d fonts, %3$d tokens of yours', 'aipb' ),
			$colours,
			$fonts,
			$tokens
		);
	}

	/**
	 * Where the visitor's browser fetches the fonts from.
	 *
	 * The plugin has self-hosted them for a while, and the front end falls back to Google's domain
	 * without a word when the download fails — the one outcome the feature exists to prevent, and
	 * the only place it could be noticed was a network tab. Now it is on the page that decides the
	 * fonts.
	 */
	protected static function render_font_hosting() {
		if ( ! class_exists( 'AIPB_Fonts' ) ) {
			return;
		}

		$status = AIPB_Fonts::status();
		if ( ! $status['families'] ) {
			return;
		}

		if ( $status['local'] ) {
			$tone = 'ok';
			$text = sprintf(
				/* translators: 1: number of font files, 2: their total size, e.g. "84 KB". */
				__( 'Served from your own domain: %1$d files, %2$s. No request reaches Google, so no visitor IP does either.', 'aipb' ),
				$status['files'],
				size_format( $status['bytes'] )
			);
		} elseif ( ! $status['on'] ) {
			$tone = 'idle';
			$text = __( 'Self-hosting is switched off on this site (the aipb_self_host_fonts filter): the fonts are loaded from Google.', 'aipb' );
		} else {
			$tone = 'warn';
			$text = __( 'Not self-hosted yet: the fonts are being loaded from Google, so every visitor\'s IP address reaches them. The copy is made the first time a page is served — save the fonts, or open a page on the front end, and come back.', 'aipb' );
		}

		printf(
			'<p class="aipb-fw-host is-%s">%s</p>',
			esc_attr( $tone ),
			esc_html( $text )
		);
	}

	/**
	 * The specimen: the design system as a thing you can look at.
	 *
	 * WHY IT EXISTS. Every control on this page edits one value in isolation — a colour swatch, a
	 * font card, `clamp(1.75rem, 1.45rem + 1.4vw, 2.5rem)` beside an «Aa» twenty pixels tall. But
	 * nobody judges a type scale by one size: what is being decided is the RATIO between the levels,
	 * the air around a heading, whether the radius and the shadow belong to the same family. None of
	 * that is visible in twenty-four separate inputs, and the first place it became visible was the
	 * generated page — which is far too late and costs a credit.
	 *
	 * It is deliberately not a mock page. It is a specimen sheet: one of each thing the tokens
	 * decide, at their real values, so a wrong relationship shows up as a wrong relationship.
	 *
	 * The values are live. `brand.js` writes a `<style>` of its own from every field on the page, so
	 * what you see is the state of the FORM — including what the mockup import has just poured in
	 * and has not been saved yet.
	 */
	protected static function render_specimen() {
		?>
		<section class="aipb-panel aipb-spec-panel">
			<div class="aipb-panel-head">
				<div>
					<h2><?php esc_html_e( 'The specimen', 'aipb' ); ?> <span class="aipb-muted"><?php esc_html_e( '(live)', 'aipb' ); ?></span></h2>
					<p class="aipb-panel-sub"><?php echo wp_kses_post( __( 'One of each thing your tokens decide, at the values that are in the form <b>right now</b> — including changes you have not saved. It is not a page: it is the place to judge the <b>relationships</b>, which is the only thing a design system is. Nothing here is saved or generated.', 'aipb' ) ); ?></p>
				</div>
				<label class="aipb-spec-toggle"><input type="checkbox" id="aipb-spec-dark"> <?php esc_html_e( 'Dark background', 'aipb' ); ?></label>
			</div>

			<style id="aipb-spec-vars"></style>

			<div class="aipb-spec" id="aipb-spec">
				<div class="aipb-spec-stage">

					<div class="aipb-spec-row aipb-spec-type">
						<span class="aipb-spec-tag"><?php esc_html_e( 'Type scale', 'aipb' ); ?></span>
						<div>
							<p class="aipb-spec-4xl"><?php esc_html_e( 'Display', 'aipb' ); ?></p>
							<p class="aipb-spec-3xl"><?php esc_html_e( 'A large heading', 'aipb' ); ?></p>
							<p class="aipb-spec-2xl"><?php esc_html_e( 'A section heading', 'aipb' ); ?></p>
							<p class="aipb-spec-xl"><?php esc_html_e( 'A small heading', 'aipb' ); ?></p>
							<p class="aipb-spec-lg"><?php esc_html_e( 'A subheading that carries the sentence a little further', 'aipb' ); ?></p>
							<p class="aipb-spec-base"><?php esc_html_e( 'Body text, the size everything else is judged against. Two lines of it are enough to tell whether the measure is right and whether the leading has room to breathe.', 'aipb' ); ?></p>
							<p class="aipb-spec-sm"><?php esc_html_e( 'Small print, captions, the fine detail.', 'aipb' ); ?></p>
						</div>
					</div>

					<div class="aipb-spec-row">
						<span class="aipb-spec-tag"><?php esc_html_e( 'Surfaces', 'aipb' ); ?></span>
						<div class="aipb-spec-cards">
							<div class="aipb-spec-card aipb-spec-card--sm">
								<b><?php esc_html_e( 'Small radius, faint shadow', 'aipb' ); ?></b>
								<span><?php esc_html_e( 'The everyday surface.', 'aipb' ); ?></span>
							</div>
							<div class="aipb-spec-card aipb-spec-card--md">
								<b><?php esc_html_e( 'Medium', 'aipb' ); ?></b>
								<span><?php esc_html_e( 'A card that lifts off the page.', 'aipb' ); ?></span>
							</div>
							<div class="aipb-spec-card aipb-spec-card--lg">
								<b><?php esc_html_e( 'Large', 'aipb' ); ?></b>
								<span><?php esc_html_e( 'The one that carries an image.', 'aipb' ); ?></span>
							</div>
						</div>
					</div>

					<div class="aipb-spec-row">
						<span class="aipb-spec-tag"><?php esc_html_e( 'Buttons', 'aipb' ); ?></span>
						<div class="aipb-spec-buttons">
							<span class="aipb-spec-btn aipb-spec-btn--solid"><?php esc_html_e( 'Primary action', 'aipb' ); ?></span>
							<span class="aipb-spec-btn aipb-spec-btn--outline"><?php esc_html_e( 'Secondary', 'aipb' ); ?></span>
							<span class="aipb-spec-btn aipb-spec-btn--pill"><?php esc_html_e( 'Pill', 'aipb' ); ?></span>
						</div>
					</div>

					<div class="aipb-spec-row">
						<span class="aipb-spec-tag"><?php esc_html_e( 'Vertical rhythm', 'aipb' ); ?></span>
						<div class="aipb-spec-rhythm">
							<div class="aipb-spec-band">
								<p class="aipb-spec-2xl"><?php esc_html_e( 'A section', 'aipb' ); ?></p>
								<p class="aipb-spec-base"><?php esc_html_e( 'The padding above and below is the single value that decides whether a generated page looks composed or cramped.', 'aipb' ); ?></p>
							</div>
							<div class="aipb-spec-band aipb-spec-band--alt">
								<p class="aipb-spec-2xl"><?php esc_html_e( 'The next one', 'aipb' ); ?></p>
								<p class="aipb-spec-base"><?php esc_html_e( 'Two of them together are what the eye actually reads as rhythm.', 'aipb' ); ?></p>
							</div>
						</div>
					</div>

					<div class="aipb-spec-row">
						<span class="aipb-spec-tag"><?php esc_html_e( 'Content width', 'aipb' ); ?></span>
						<div class="aipb-spec-widths">
							<span class="aipb-spec-width aipb-spec-width--wide"><i></i><?php esc_html_e( 'standard', 'aipb' ); ?></span>
							<span class="aipb-spec-width aipb-spec-width--narrow"><i></i><?php esc_html_e( 'long text', 'aipb' ); ?></span>
						</div>
					</div>

					<div class="aipb-spec-row aipb-spec-row--palette" id="aipb-spec-palette" hidden>
						<span class="aipb-spec-tag"><?php esc_html_e( 'Palette', 'aipb' ); ?></span>
						<div class="aipb-spec-swatches" id="aipb-spec-swatches"></div>
					</div>

				</div>
			</div>
		</section>
		<?php
	}

	/**
	 * The brand MANIFEST: the direction's structured half.
	 *
	 * The panel sits right under the direction because they answer the same question — how this
	 * brand behaves — split by how binding the answer is: the direction is prose the AI interprets,
	 * these are rules it obeys, and "never" is a prohibition the prompt states as one.
	 */
	protected static function render_manifest() {
		$opt      = AIPB_Design::OPT_MANIFEST;
		$manifest = AIPB_Design::manifest();

		$line = static function ( $key ) use ( $manifest ) {
			return isset( $manifest[ $key ] ) ? (string) $manifest[ $key ] : '';
		};
		$list = static function ( $key ) use ( $manifest ) {
			return isset( $manifest[ $key ] ) && is_array( $manifest[ $key ] ) ? implode( "\n", $manifest[ $key ] ) : '';
		};
		?>
		<section class="aipb-panel" id="aipb-manifest">
			<div class="aipb-panel-head">
				<div>
					<h2><?php esc_html_e( 'Brand manifest', 'aipb' ); ?> <span class="aipb-muted">(<?php esc_html_e( 'structured rules', 'aipb' ); ?>)</span></h2>
					<p class="aipb-panel-sub"><?php echo wp_kses_post( __( 'The direction above is prose the AI <b>interprets</b>; these are rules it <b>obeys</b>, one per line. The «never» list is the strongest thing on this page: a hard prohibition, stated to the AI as one. All of it is optional — an empty field simply says nothing.', 'aipb' ) ); ?></p>
				</div>
			</div>

			<div class="aipb-man-grid">
				<div class="aipb-field">
					<label class="aipb-label" for="aipb-man-personality"><?php esc_html_e( 'Personality', 'aipb' ); ?></label>
					<input type="text" id="aipb-man-personality" class="large-text" name="<?php echo esc_attr( $opt . '[personality]' ); ?>" value="<?php echo esc_attr( $line( 'personality' ) ); ?>" maxlength="<?php echo (int) AIPB_Design::MANIFEST_LINE_MAX; ?>" placeholder="<?php esc_attr_e( 'e.g. «confident, warm, essential — never playful»', 'aipb' ); ?>">
				</div>
				<div class="aipb-field">
					<label class="aipb-label" for="aipb-man-rhythm"><?php esc_html_e( 'Rhythm of the page', 'aipb' ); ?></label>
					<input type="text" id="aipb-man-rhythm" class="large-text" name="<?php echo esc_attr( $opt . '[rhythm]' ); ?>" value="<?php echo esc_attr( $line( 'rhythm' ) ); ?>" maxlength="<?php echo (int) AIPB_Design::MANIFEST_LINE_MAX; ?>" placeholder="<?php esc_attr_e( 'e.g. «generous air between sections, dense inside the cards»', 'aipb' ); ?>">
				</div>
				<div class="aipb-field">
					<label class="aipb-label" for="aipb-man-imagery"><?php esc_html_e( 'Imagery', 'aipb' ); ?></label>
					<input type="text" id="aipb-man-imagery" class="large-text" name="<?php echo esc_attr( $opt . '[imagery]' ); ?>" value="<?php echo esc_attr( $line( 'imagery' ) ); ?>" maxlength="<?php echo (int) AIPB_Design::MANIFEST_LINE_MAX; ?>" placeholder="<?php esc_attr_e( 'e.g. «full-bleed photography, never illustrations or stock icons»', 'aipb' ); ?>">
				</div>
				<div class="aipb-field">
					<label class="aipb-label" for="aipb-man-motion"><?php esc_html_e( 'Motion rules', 'aipb' ); ?> <span class="aipb-muted"><?php esc_html_e( '(one per line)', 'aipb' ); ?></span></label>
					<textarea id="aipb-man-motion" class="large-text" rows="3" name="<?php echo esc_attr( $opt . '[motion_rules]' ); ?>" placeholder="<?php esc_attr_e( "e.g. «entrances rise, they never bounce»\n«one hero animation per page at most»", 'aipb' ); ?>"><?php echo esc_textarea( $list( 'motion_rules' ) ); ?></textarea>
				</div>
				<div class="aipb-field">
					<label class="aipb-label" for="aipb-man-never"><?php esc_html_e( 'Never', 'aipb' ); ?> <span class="aipb-muted"><?php esc_html_e( '(hard prohibitions, one per line)', 'aipb' ); ?></span></label>
					<textarea id="aipb-man-never" class="large-text" rows="3" name="<?php echo esc_attr( $opt . '[never]' ); ?>" placeholder="<?php esc_attr_e( "e.g. «no gradients»\n«no carousels in the hero»", 'aipb' ); ?>"><?php echo esc_textarea( $list( 'never' ) ); ?></textarea>
				</div>
			</div>
		</section>
		<?php
	}

	/**
	 * The brand's effects: named entrances the AI references by id instead of re-inventing.
	 *
	 * The shipped ones are shown but not editable — they live in code and move with the plugin,
	 * like the structural scale. The user's own are full rows: id, where they belong, when to use
	 * them, and the CSS. The CSS is validated on save (see AIPB_Effects::validate_css) and a row
	 * that fails is dropped WITH a notice, never repaired in silence.
	 */
	protected static function render_effects() {
		$opt  = AIPB_Effects::OPT;
		$mine = AIPB_Effects::user_effects();
		?>
		<section class="aipb-panel" id="aipb-effects">
			<div class="aipb-panel-head">
				<div>
					<h2><?php esc_html_e( 'Effects', 'aipb' ); ?> <span class="aipb-muted">(<?php esc_html_e( 'advanced', 'aipb' ); ?>)</span></h2>
					<p class="aipb-panel-sub"><?php echo wp_kses_post( __( 'Named entrances, defined <b>once</b>: a section uses one with <code>data-aura-reveal="id"</code> and the AI is told the id and when to use it — never the code, so it cannot drift. The CSS may only style its own attribute, and durations and easings must be the <b>motion tokens</b> (<code>var(--aipb-motion-…)</code>): that is what makes every effect, old and new, move at the brand\'s tempo — and what makes "snappier animations" a one-token change.', 'aipb' ) ); ?></p>
				</div>
				<button type="button" class="button button-primary aipb-add" id="aipb-add-effect">+ <?php esc_html_e( 'Add effect', 'aipb' ); ?></button>
			</div>

			<div class="aipb-fx-list" id="aipb-fx-list">
				<?php foreach ( AIPB_Effects::all() as $effect ) : ?>
					<?php if ( ! empty( $effect['shipped'] ) ) : ?>
						<div class="aipb-fx-row is-shipped">
							<div class="aipb-fx-top">
								<code class="aipb-fx-id">data-aura-reveal="<?php echo esc_html( $effect['id'] ); ?>"</code>
								<span class="aipb-fx-badge"><?php esc_html_e( 'shipped', 'aipb' ); ?></span>
								<span class="aipb-fx-applies"><?php echo esc_html( implode( ', ', $effect['applies_to'] ) ); ?></span>
							</div>
							<p class="aipb-fx-usage"><?php echo esc_html( $effect['usage'] ); ?></p>
						</div>
					<?php endif; ?>
				<?php endforeach; ?>

				<?php foreach ( $mine as $i => $effect ) : ?>
					<?php self::render_effect_row( $opt, $i, $effect ); ?>
				<?php endforeach; ?>
			</div>

			<template id="aipb-fx-tpl">
				<?php self::render_effect_row( $opt, '__I__', array(
					'id'         => '',
					'label'      => '',
					'applies_to' => array(),
					'usage'      => '',
					'css'        => '',
					'created_by' => 'user',
					'approved'   => true,
				) ); ?>
			</template>
		</section>
		<?php
	}

	/** One editable effect row. $i is the input index — '__I__' inside the template. */
	protected static function render_effect_row( $opt, $i, $effect ) {
		?>
		<div class="aipb-fx-row">
			<input type="hidden" name="<?php echo esc_attr( $opt . '[' . $i . '][created_by]' ); ?>" value="<?php echo esc_attr( $effect['created_by'] ); ?>">
			<input type="hidden" name="<?php echo esc_attr( $opt . '[' . $i . '][approved]' ); ?>" value="<?php echo $effect['approved'] ? '1' : ''; ?>">
			<div class="aipb-fx-top">
				<code class="aipb-fx-id">data-aura-reveal="<input type="text" class="aipb-fx-id-input" name="<?php echo esc_attr( $opt . '[' . $i . '][id]' ); ?>" value="<?php echo esc_attr( $effect['id'] ); ?>" placeholder="<?php esc_attr_e( 'name', 'aipb' ); ?>" spellcheck="false" autocomplete="off">"</code>
				<input type="text" class="aipb-fx-lbl" name="<?php echo esc_attr( $opt . '[' . $i . '][label]' ); ?>" value="<?php echo esc_attr( $effect['label'] ); ?>" placeholder="<?php esc_attr_e( 'label', 'aipb' ); ?>">
				<input type="text" class="aipb-fx-applies-in" name="<?php echo esc_attr( $opt . '[' . $i . '][applies_to]' ); ?>" value="<?php echo esc_attr( implode( ', ', $effect['applies_to'] ) ); ?>" placeholder="<?php esc_attr_e( 'applies to — e.g. img, figure', 'aipb' ); ?>" spellcheck="false">
				<button type="button" class="aipb-fx-drop" aria-label="<?php esc_attr_e( 'Delete this effect', 'aipb' ); ?>" title="<?php esc_attr_e( 'Delete this effect', 'aipb' ); ?>">×</button>
			</div>
			<input type="text" class="aipb-fx-usage-in large-text" name="<?php echo esc_attr( $opt . '[' . $i . '][usage]' ); ?>" value="<?php echo esc_attr( $effect['usage'] ); ?>" placeholder="<?php esc_attr_e( 'when to use it — this line is what the AI reads', 'aipb' ); ?>">
			<textarea class="aipb-fx-css large-text code" name="<?php echo esc_attr( $opt . '[' . $i . '][css]' ); ?>" rows="4" spellcheck="false" placeholder="<?php esc_attr_e( '@keyframes fx-name{…}  [data-aura-reveal="name"]{…}  [data-aura-reveal="name"][data-aura-in]{…}', 'aipb' ); ?>"><?php echo esc_textarea( $effect['css'] ); ?></textarea>
		</div>
		<?php
	}

	/** Heading, icon and subtitle of each group of structural tokens. */
	protected static function group_meta() {
		return array(
			'text'   => array( 'ico' => 'Aa', 'title' => __( 'Text sizes', 'aipb' ), 'sub' => __( 'Fluid type scale (clamp).', 'aipb' ) ),
			'space'  => array( 'ico' => '↕', 'title' => __( 'Spacing', 'aipb' ), 'sub' => __( 'Padding, margins and gaps between elements.', 'aipb' ) ),
			'radius' => array( 'ico' => '▢', 'title' => __( 'Radii', 'aipb' ), 'sub' => __( 'How much the corners are rounded.', 'aipb' ) ),
			'shadow' => array( 'ico' => '◧', 'title' => __( 'Shadows', 'aipb' ), 'sub' => __( 'Elevation and depth of the surfaces.', 'aipb' ) ),
			'layout' => array( 'ico' => '⇔', 'title' => __( 'Widths', 'aipb' ), 'sub' => __( 'Maximum width of the contents.', 'aipb' ) ),
			'motion' => array( 'ico' => '◐', 'title' => __( 'Motion', 'aipb' ), 'sub' => __( 'Durations, easing and distances of every entrance: the brand\'s voice in movement.', 'aipb' ) ),
		);
	}

	protected static function render_token_grid( $groups ) {
		$meta     = self::group_meta();
		$opt      = AIPB_Design::OPT_TOKENS;
		$defaults = AIPB_Design::flat_defaults();
		$changed  = self::count_overrides( $groups, $defaults );
		$custom   = array();
		foreach ( AIPB_Design::custom_tokens() as $i => $token ) {
			$custom[ $token['group'] . ':' . $token['key'] ] = $i;
		}
		$usage = AIPB_Design::token_usage();
		?>
		<section class="aipb-panel">
			<div class="aipb-panel-head">
				<div>
					<h2><?php esc_html_e( 'Structural tokens', 'aipb' ); ?> <span class="aipb-muted">(<?php esc_html_e( 'advanced', 'aipb' ); ?>)</span>
						<?php // Always in the markup, hidden at zero: the script keeps it up to date as you type. ?>
						<span class="aipb-tok-count" title="<?php esc_attr_e( 'Tokens you have changed from the plugin\'s scale', 'aipb' ); ?>"<?php echo $changed ? '' : ' hidden'; ?>><?php
							echo esc_html( sprintf(
								/* translators: %d: how many tokens differ from the shipped value. */
								_n( '%d value of yours', '%d values of yours', max( 1, $changed ), 'aipb' ),
								$changed
							) );
						?></span>
					</h2>
					<p class="aipb-panel-sub"><?php echo wp_kses_post( __( 'The base scales the AI uses in the sections. They <b>already have a value</b>: it is what keeps sections generated at different times on the same rhythm, instead of each inventing its own. Overwrite whichever you like — what you type becomes yours and travels with the brand. The values accept free CSS (px, rem, <code>clamp()</code>…) and the preview updates as you type. <b>↺</b> puts a single token back to the plugin\'s scale. You can also <b>add your own</b>: a token you invent — <code>--aipb-radius-card</code>, <code>--aipb-space-gutter</code> — is offered to the AI exactly like the ones shipped, and gives it a name for something it would otherwise re-invent in every section.', 'aipb' ) ); ?></p>
				</div>
			</div>

			<div class="aipb-tokgrid">
				<?php foreach ( self::GROUPS as $gk ) :
					if ( empty( $groups[ $gk ] ) ) {
						continue;
					}
					?>
					<div class="aipb-tok-card">
						<div class="aipb-tok-card-head">
							<span class="aipb-tok-ico" aria-hidden="true"><?php echo esc_html( $meta[ $gk ]['ico'] ); ?></span>
							<div>
								<h3><?php echo esc_html( $meta[ $gk ]['title'] ); ?></h3>
								<p><?php echo esc_html( $meta[ $gk ]['sub'] ); ?></p>
							</div>
						</div>
						<div class="aipb-tok-rows" data-group="<?php echo esc_attr( $gk ); ?>" data-prefix="<?php echo esc_attr( $groups[ $gk ]['prefix'] ); ?>">
							<?php foreach ( $groups[ $gk ]['items'] as $ik => $item ) :
								$id      = $gk . ':' . $ik;
								$default = isset( $defaults[ $id ] ) ? $defaults[ $id ] : '';
								$own     = isset( $custom[ $id ] );
								$mine    = $own || ( '' !== $default && $item[0] !== $default );
								?>
								<div class="aipb-tok-row<?php echo $mine ? ' is-mine' : ''; ?><?php echo $own ? ' is-own' : ''; ?>" data-kind="<?php echo esc_attr( $gk ); ?>">
									<?php if ( $own ) : ?>
										<input type="hidden" name="<?php echo esc_attr( AIPB_Design::OPT_CUSTOM . '[' . $custom[ $id ] . '][group]' ); ?>" value="<?php echo esc_attr( $gk ); ?>">
										<input type="hidden" name="<?php echo esc_attr( AIPB_Design::OPT_CUSTOM . '[' . $custom[ $id ] . '][key]' ); ?>" value="<?php echo esc_attr( $ik ); ?>">
										<input type="hidden" class="aipb-tok-own-label" name="<?php echo esc_attr( AIPB_Design::OPT_CUSTOM . '[' . $custom[ $id ] . '][label]' ); ?>" value="<?php echo esc_attr( $item[1] ); ?>">
									<?php endif; ?>
									<div class="aipb-tok-top">
										<code class="aipb-tok-var"><?php echo esc_html( $groups[ $gk ]['prefix'] . $ik ); ?></code>
										<?php if ( $own ) : ?>
											<input type="text" class="aipb-tok-lbl-input" value="<?php echo esc_attr( $item[1] ); ?>" placeholder="<?php esc_attr_e( 'what it is for', 'aipb' ); ?>" spellcheck="false">
											<button type="button" class="aipb-tok-drop" aria-label="<?php esc_attr_e( 'Delete this token', 'aipb' ); ?>" title="<?php esc_attr_e( 'Delete this token', 'aipb' ); ?>">×</button>
										<?php elseif ( $item[1] ) : ?><span class="aipb-tok-lbl">— <?php echo esc_html( $item[1] ); ?></span><?php endif; ?>
										<?php self::render_usage( $groups[ $gk ]['prefix'] . $ik, $usage ); ?>
										<?php if ( '' !== $default ) : ?>
											<button type="button" class="aipb-tok-revert" data-default="<?php echo esc_attr( $default ); ?>"
												title="<?php echo esc_attr( sprintf(
													/* translators: %s: the value the plugin ships for this token. */
													__( 'Back to the plugin\'s scale (%s)', 'aipb' ),
													$default
												) ); ?>" aria-label="<?php esc_attr_e( 'Back to the default value', 'aipb' ); ?>">↺</button>
										<?php endif; ?>
									</div>
									<div class="aipb-tok-pv"><?php echo self::token_preview( $gk, $item[0] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped — markup built here, values escaped ?></div>
									<?php // A token of the user's own has no shipped scale to fall back to: emptying it just switches it off. ?>
									<input type="text" class="aipb-tok-input" name="<?php echo esc_attr( $opt . '[' . $id . ']' ); ?>" value="<?php echo esc_attr( $item[0] ); ?>"
										placeholder="<?php echo $own ? esc_attr__( 'the value — px, rem, clamp()…', 'aipb' ) : esc_attr__( 'empty — back to the plugin\'s scale', 'aipb' ); ?>" spellcheck="false">
								</div>
							<?php endforeach; ?>
						</div>
						<button type="button" class="aipb-tok-add" data-group="<?php echo esc_attr( $gk ); ?>">+ <?php esc_html_e( 'Add a token', 'aipb' ); ?></button>
						<?php self::render_new_token_template( $gk, $groups[ $gk ]['prefix'] ); ?>
					</div>
				<?php endforeach; ?>
			</div>
		</section>
		<?php
	}

	/**
	 * How many contents lean on this token.
	 *
	 * Silence where the answer is zero: a badge reading «0» on twenty rows out of twenty-four is
	 * noise, and the whole value of this number is that it appears where there is something to lose.
	 */
	protected static function render_usage( $variable, $usage ) {
		$key = strtolower( $variable );
		if ( empty( $usage[ $key ]['n'] ) ) {
			return;
		}

		$n     = (int) $usage[ $key ]['n'];
		$names = array();
		foreach ( $usage[ $key ]['posts'] as $post ) {
			$names[] = '' !== $post['title'] ? $post['title'] : '#' . $post['id'];
		}
		if ( count( $names ) < $n ) {
			$names[] = '…';
		}

		printf(
			'<span class="aipb-tok-use" title="%s">%s</span>',
			esc_attr( implode( ' · ', $names ) ),
			esc_html( sprintf(
				/* translators: %d: how many contents use this token. */
				_n( 'used in %d', 'used in %d', $n, 'aipb' ),
				$n
			) )
		);
	}

	/**
	 * The blank row a new token is born into.
	 *
	 * It lives in PHP and not in the JavaScript so that the shape of a row — the preview markup in
	 * particular, which differs per group — is written once. The script clones this and fills in the
	 * two placeholders: `__I__` for the index of the declaration, `__K__` for the key as it is being
	 * typed.
	 */
	protected static function render_new_token_template( $group, $prefix ) {
		$custom = AIPB_Design::OPT_CUSTOM;
		$opt    = AIPB_Design::OPT_TOKENS;
		?>
		<template class="aipb-tok-tpl">
			<div class="aipb-tok-row is-mine is-own is-new is-nameless" data-kind="<?php echo esc_attr( $group ); ?>">
				<input type="hidden" name="<?php echo esc_attr( $custom . '[__I__][group]' ); ?>" value="<?php echo esc_attr( $group ); ?>">
				<input type="hidden" class="aipb-tok-own-key" name="<?php echo esc_attr( $custom . '[__I__][key]' ); ?>" value="">
				<input type="hidden" class="aipb-tok-own-label" name="<?php echo esc_attr( $custom . '[__I__][label]' ); ?>" value="">
				<div class="aipb-tok-top">
					<code class="aipb-tok-var aipb-tok-var-edit"><?php echo esc_html( $prefix ); ?><input type="text" class="aipb-tok-key-input" placeholder="<?php esc_attr_e( 'name', 'aipb' ); ?>" spellcheck="false" autocomplete="off"></code>
					<input type="text" class="aipb-tok-lbl-input" placeholder="<?php esc_attr_e( 'what it is for', 'aipb' ); ?>" spellcheck="false">
					<button type="button" class="aipb-tok-drop" aria-label="<?php esc_attr_e( 'Delete this token', 'aipb' ); ?>" title="<?php esc_attr_e( 'Delete this token', 'aipb' ); ?>">×</button>
				</div>
				<div class="aipb-tok-pv"><?php echo self::token_preview( $group, '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped — markup built here, values escaped ?></div>
				<input type="text" class="aipb-tok-input" data-name="<?php echo esc_attr( $opt . '[' . $group . ':__K__]' ); ?>" value="" placeholder="<?php esc_attr_e( 'the value — px, rem, clamp()…', 'aipb' ); ?>" spellcheck="false">
			</div>
		</template>
		<?php
	}

	/**
	 * How many tokens the user has actually moved off the shipped scale.
	 *
	 * It answers the question the grid could not: twenty-four identical inputs, and no way to see
	 * which of them are yours. A token whose default is empty cannot be «changed» in this sense —
	 * there is nothing to differ from — so it does not count.
	 */
	protected static function count_overrides( $groups, $defaults ) {
		$n = 0;

		foreach ( $groups as $group_key => $group ) {
			foreach ( $group['items'] as $item_key => $item ) {
				$id = $group_key . ':' . $item_key;
				if ( isset( $defaults[ $id ] ) && '' !== $defaults[ $id ] && $item[0] !== $defaults[ $id ] ) {
					$n++;
				}
			}
		}

		return $n;
	}

	/** The markup previewing a token, according to its group. */
	protected static function token_preview( $group, $value ) {
		$v = esc_attr( $value );

		switch ( $group ) {
			case 'text':
				return '<span class="aipb-pv aipb-pv--text" data-prop="fontSize" style="font-size:' . $v . '">Aa</span>';
			case 'motion':
				// No .aipb-pv: a duration or an easing has no property the little square could show,
				// and the generic repaint would write nonsense into one. The value speaks for itself.
				return '<span class="aipb-pv-none" aria-hidden="true"></span>';
			case 'radius':
				return '<span class="aipb-pv aipb-pv--radius" data-prop="borderRadius" style="border-radius:' . $v . '"></span>';
			case 'shadow':
				return '<span class="aipb-pv aipb-pv--shadow" data-prop="boxShadow" style="box-shadow:' . $v . '"></span>';
			case 'layout':
				return '<span class="aipb-pv-track"><span class="aipb-pv aipb-pv--bar" data-prop="width" data-kind="layout" style="width:' . self::bar_percent( $value ) . '%"></span></span>';
			case 'space':
			default:
				return '<span class="aipb-pv-track"><span class="aipb-pv aipb-pv--bar" data-prop="width" style="width:' . $v . ';max-width:100%"></span></span>';
		}
	}

	/** A width as a proportion of a 1280px reference, for the preview bars. */
	protected static function bar_percent( $value ) {
		if ( preg_match( '/(-?\d+(?:\.\d+)?)\s*px/', $value, $m ) ) {
			return round( max( 4, min( 100, ( (float) $m[1] / 1280 ) * 100 ) ), 1 );
		}

		return 100;
	}
}
