<?php
/**
 * NeccoSpeak — Admin screen: setup checklist + form-tab renderers (General,
 * Voice, Appearance, Rate Limits, Prompt, Safety) and the G18 prompt preview.
 *
 * Required unconditionally (is_admin() guard) from the bottom of
 * includes/settings.php (design rev.2 §14.3 rule R1). Every function here is
 * called from neccospeak_render_settings_page() in settings.php.
 *
 * Owner: impl-admin.
 * Public contract (used by settings.php):
 *   neccospeak_render_setup_checklist( $opts )
 *   neccospeak_render_tab_general( $opts )
 *   neccospeak_render_tab_voice( $opts )
 *   neccospeak_render_tab_appearance( $opts )
 *   neccospeak_render_tab_rate_limits( $opts )
 *   neccospeak_render_tab_prompt( $opts )
 *   neccospeak_render_tab_safety( $opts )       (Phase 3 design rev.2 §12.2 —
 *                                                G21 blocked words + G20 PII)
 *
 * Phase 3 note (design rev.2 §1.5/C16): includes/prompt.php's
 * neccospeak_prompt_presets() and neccospeak_prompt_faq_fit() are impl-core
 * owned. Every call site below guards with function_exists() so this file
 * degrades gracefully (Quick start / FAQ-fit notices simply do not render)
 * if it is ever loaded before those land — the same pattern the Phase 1
 * neccospeak_compose_system_prompt_ex() call already uses.
 *
 * All PHP output in this file follows the escaping convention in design
 * rev.2 §9.7: esc_html()/esc_html__() for text nodes, esc_attr()/esc_attr__()
 * for attribute values, esc_url( add_query_arg( …, admin_url( 'admin.php' ) ) )
 * for in-plugin links.
 *
 * @package NeccoSpeak
 */

defined( 'ABSPATH' ) || exit;

/**
 * "Max N bytes (~M Japanese characters)" note for a byte-capped prompt field
 * (design rev.2 §3.1 'prompt_text' fields). None of these <textarea>/<input>
 * fields have a maxlength or a live byte counter, so this description is the
 * only up-front signal an admin gets before neccospeak_sanitize_prompt_text()
 * silently cuts anything past the cap on save (paired with the
 * add_settings_error() warning settings.php now raises when that actually
 * happens).
 *
 * @param int $max_bytes Byte cap (e.g. neccospeak_field_specs()[$key]['max']).
 * @return string Translated, plain-text note (caller wraps/escapes it).
 */
function neccospeak_field_max_bytes_note( $max_bytes ) {
	$max_bytes = (int) $max_bytes;
	$approx_ja = (int) round( $max_bytes / 3 );
	return sprintf(
		/* translators: 1: maximum size in bytes, 2: approximate Japanese character count for that many bytes. */
		__( 'Maximum %1$s bytes (about %2$s characters in Japanese).', 'neccospeak' ),
		number_format_i18n( $max_bytes ),
		number_format_i18n( $approx_ja )
	);
}

/**
 * Render a wp-color-picker-ready text input for a 'color_optional' option
 * (design phase4 §4.2). Empty stored value means "unspecified — the
 * widget.css built-in default applies" (INV-1), which is why the field is
 * never pre-filled with $default_swatch — that value is only the picker's
 * "reset to" swatch and the placeholder text.
 *
 * @param string $key            Option key (e.g. 'theme_panel_bg').
 * @param array  $opts           neccospeak_get_options().
 * @param string $default_swatch Hex color shown as the picker's default swatch + placeholder.
 * @return void
 */
function neccospeak_render_color_field( $key, $opts, $default_swatch ) {
	$current = isset( $opts[ $key ] ) ? (string) $opts[ $key ] : '';
	$swatch  = ( '' !== $current && preg_match( '/^#[0-9a-fA-F]{6}$/', $current ) ) ? $current : $default_swatch;
	printf(
		'<span class="neccospeak-color-field ai-agent-color-field">' .
		'<input type="color" class="neccospeak-color-swatch" value="%2$s" aria-hidden="true" tabindex="-1" oninput="var t=document.getElementById(\'neccospeak_%1$s\'); t.value=this.value; t.dispatchEvent(new Event(\'input\'));" />' .
		'<input type="text" id="neccospeak_%1$s" class="neccospeak-color-hex" data-neccospeak-color data-default-color="%2$s" name="%3$s[%1$s]" value="%4$s" placeholder="%2$s" pattern="^#[0-9a-fA-F]{6}$" maxlength="7" oninput="var s=this.previousElementSibling; if(/^#[0-9a-fA-F]{6}$/.test(this.value)){s.value=this.value;}" />' .
		'</span>',
		esc_attr( $key ),
		esc_attr( '' !== $swatch ? $swatch : '#ffffff' ),
		esc_attr( NECCOSPEAK_OPTION ),
		esc_attr( $current )
	);
}

/**
 * Render a generic media picker (design phase4 §8.4): a plain number input
 * holding the attachment ID (works with JS disabled — an admin can type an
 * ID directly), plus a JS-only "Select image"/"Remove image" pair that
 * assets/admin.js's initMediaPickers() wires up to wp.media() when
 * available.
 *
 * data-neccospeak-media-url carries the CURRENTLY SAVED attachment's preview
 * URL. This exists purely for the G6 live preview (design phase4 §11.5):
 * that preview cannot resolve an attachment ID to a URL itself (it never
 * calls the REST API, INV-11), so it reads the URL straight off this
 * attribute — and initMediaPickers() keeps the attribute updated when a new
 * image is picked, before the form is ever saved.
 *
 * @param string $key  Option key (e.g. 'theme_watermark_id').
 * @param array  $opts neccospeak_get_options().
 * @param string $size Attachment image size to resolve data-neccospeak-media-url
 *                     against (design phase4 §11.5 review fix). Must match
 *                     the size includes/frontend-config.php's
 *                     neccospeak_widget_attachment_url() requests for this
 *                     same $key, or the G6 live preview shows a differently
 *                     cropped/scaled image than production. Also emitted as
 *                     data-neccospeak-media-size so assets/admin.js's
 *                     initMediaPickers() can request the matching size from
 *                     wp.media() when a new image is picked, instead of
 *                     always hardcoding 'medium'. NOT used for the visible
 *                     on-page <img> (design phase4 review fix) — that one is
 *                     always resolved against the 'thumbnail' size instead,
 *                     since $size can be as large as 'large' (1024px) and
 *                     showing that inline in the .form-table blows out the
 *                     row.
 * @return void
 */
function neccospeak_render_media_field( $key, $opts, $size = 'thumbnail' ) {
	$id          = isset( $opts[ $key ] ) && is_scalar( $opts[ $key ] ) ? (int) $opts[ $key ] : 0;
	$preview_url = ( $id > 0 && function_exists( 'wp_get_attachment_image_url' ) ) ? (string) wp_get_attachment_image_url( $id, $size ) : '';

	// The visible <img> always resolves against the 'thumbnail' size,
	// regardless of $size (design phase4 review fix): $size is the
	// production-fidelity size (e.g. 'large' for the watermark, up to
	// 1024px, or 'medium' for the home icon, up to 300px) and is only ever
	// meant to drive data-neccospeak-media-url above / the G6 live preview
	// — showing that full-size image inline in the .form-table renders it
	// at its native size and blows out the row.
	$thumb        = ( $id > 0 && function_exists( 'wp_get_attachment_image_src' ) ) ? wp_get_attachment_image_src( $id, 'thumbnail' ) : false;
	$thumb_url    = is_array( $thumb ) ? (string) $thumb[0] : '';
	$thumb_width  = is_array( $thumb ) ? (int) $thumb[1] : 0;
	$thumb_height = is_array( $thumb ) ? (int) $thumb[2] : 0;
	?>
	<div class="neccospeak-media" data-neccospeak-media>
		<input type="number" min="0" step="1" class="small-text neccospeak-media-id"
			data-neccospeak-media-input
			data-neccospeak-media-url="<?php echo esc_url( $preview_url ); ?>"
			data-neccospeak-media-size="<?php echo esc_attr( $size ); ?>"
			name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[<?php echo esc_attr( $key ); ?>]"
			value="<?php echo esc_attr( (string) $id ); ?>" />
		<img class="neccospeak-media-preview" data-neccospeak-media-preview alt=""
			src="<?php echo esc_url( $thumb_url ); ?>"
			<?php echo $thumb_width ? ' width="' . esc_attr( (string) $thumb_width ) . '"' : ''; ?>
			<?php echo $thumb_height ? ' height="' . esc_attr( (string) $thumb_height ) . '"' : ''; ?>
			<?php echo $thumb_url ? '' : 'hidden'; ?> />
		<button type="button" class="button" data-neccospeak-media-select><?php echo esc_html__( 'Select image', 'neccospeak' ); ?></button>
		<button type="button" class="button-link" data-neccospeak-media-remove><?php echo esc_html__( 'Remove image', 'neccospeak' ); ?></button>
	</div>
	<?php
}

/**
 * Generic tab-content insertion point (design phase4 §8.2). Other files that
 * want to add UI into an existing tab's rendered output hook
 * `neccospeak_admin_tab_slot_{$slot}` (or the catch-all `neccospeak_admin_tab_slot`,
 * which receives $slot as its first argument) instead of editing this file.
 *
 * 0.3.0-dev.42: every slot is currently an empty extension point. The last
 * listeners were includes/admin-preview.php's G6 live preview
 * (`..._appearance_top`) and includes/admin-logs.php's Logs/Usage subviews
 * (`..._logs_after_form` / `..._usage_after_form`), all deleted with their
 * unreachable screens. The dispatcher stays because
 * includes/agent-ui-tabs/tab-security.php still fires `rate_limits_bottom` /
 * `safety_bottom` for third-party code.
 *
 * Called from inside the settings page's `<form method="post" action="options.php">`
 * (settings.php's neccospeak_render_settings_page()), so anything hooked here
 * must never emit its own nested `<form>`.
 *
 * @param string $slot 'appearance_top', 'general_bottom', etc.
 * @param array  $opts neccospeak_get_options().
 * @return void
 */
function neccospeak_admin_tab_slot( $slot, $opts ) {
	$slot = sanitize_key( $slot );
	/**
	 * Fires for every tab slot, with the slot name as the first parameter.
	 *
	 * @param string $slot
	 * @param array  $opts
	 */
	do_action( 'neccospeak_admin_tab_slot', $slot, $opts );
	/**
	 * Fires for one specific tab slot only.
	 *
	 * @param array $opts
	 */
	do_action( 'neccospeak_admin_tab_slot_' . $slot, $opts );
}

/**
 * Enqueues wp-color-picker + wp.media on this plugin's own settings page
 * only (design phase4 §8.5, D1). Rather than editing neccospeak.php (whose
 * file-ownership entry for impl-admin is version-string-only, design phase4
 * §1.2), each admin screen file that needs its own extra admin assets
 * registers its own admin_enqueue_scripts hook — this is that hook for the
 * G23/G24/G25 color and media fields on the Appearance tab.
 *
 * wp-color-picker is registered by WP core with header output; this
 * plugin's own assets/admin.js (registered by neccospeak.php with
 * $in_footer=true) is guaranteed to run after it without any dependency
 * array changes, but assets/admin.js still guards every
 * `window.jQuery`/`window.wp` access defensively (never assumes load order).
 *
 * @param string $hook_suffix Value passed by the admin_enqueue_scripts hook.
 * @return void
 */
function neccospeak_admin_enqueue_appearance_assets( $hook_suffix ) {
	if ( ! function_exists( 'neccospeak_is_admin_page' ) || ! neccospeak_is_admin_page( $hook_suffix ) ) {
		return;
	}
	if ( ! function_exists( 'wp_enqueue_style' ) || ! function_exists( 'wp_enqueue_script' ) || ! function_exists( 'wp_enqueue_media' ) ) {
		return; // Test environment / a WP install missing these core functions for some other reason.
	}
	wp_enqueue_style( 'wp-color-picker' );
	wp_enqueue_script( 'wp-color-picker' ); // deps: jquery, iris — both WP core-bundled.
	wp_enqueue_media(); // wp.media + its backbone templates, for the G23/G24/G25 image pickers.
}
add_action( 'admin_enqueue_scripts', 'neccospeak_admin_enqueue_appearance_assets' );

/**
 * G1: "未設定チェックリスト" — a short list of setup problems that would keep
 * the widget from working or from appearing anywhere, each linking to the
 * tab where it can be fixed. Shown above the tab nav regardless of which tab
 * is open, since these are cross-cutting (e.g. a missing API key matters
 * whether the admin is looking at Voice or Rate Limits).
 *
 * @param array $opts neccospeak_get_options().
 * @return void
 */
function neccospeak_render_setup_checklist( $opts ) {
	$items = array();

	$cipher = isset( $opts['openai_api_key'] ) ? trim( (string) $opts['openai_api_key'] ) : '';
	$key_ok = '' !== $cipher && '' !== neccospeak_decrypt( $cipher );

	if ( '' === $cipher ) {
		$items[] = array(
			'text' => __( 'No OpenAI API key is saved yet — the widget will not appear until one is set.', 'neccospeak' ),
			'tab'  => 'general',
		);
	} elseif ( ! $key_ok ) {
		$items[] = array(
			'text' => __( 'The saved API key can no longer be decrypted — the widget is not being served. See Diagnostics for details.', 'neccospeak' ),
			'tab'  => 'diagnostics',
		);
	}

	if ( empty( $opts['enabled'] ) ) {
		$items[] = array(
			'text' => __( 'The widget is currently disabled.', 'neccospeak' ),
			'tab'  => 'general',
		);
	}

	if ( ! empty( $opts['voice_enabled'] ) && ! is_ssl() ) {
		$items[] = array(
			'text' => __( 'Voice conversation is enabled, but this request is not over HTTPS — voice requires HTTPS to work for visitors.', 'neccospeak' ),
			'tab'  => 'voice',
		);
	}

	$rule = isset( $opts['display_rule'] ) ? $opts['display_rule'] : 'all';
	if ( 'include' === $rule && '' === trim( (string) ( isset( $opts['included_urls'] ) ? $opts['included_urls'] : '' ) ) ) {
		$items[] = array(
			'text' => __( 'The display rule is "Show only on specified URLs" but no URLs are listed — the widget will not appear anywhere.', 'neccospeak' ),
			'tab'  => 'appearance',
		);
	}
	if ( 'exclude' === $rule && '' === trim( (string) ( isset( $opts['excluded_urls'] ) ? $opts['excluded_urls'] : '' ) ) ) {
		$items[] = array(
			'text' => __( 'The display rule is "Show except on specified URLs" but no URLs are listed, which has the same effect as "Show on all pages."', 'neccospeak' ),
			'tab'  => 'appearance',
		);
	}

	// G36 client timeout vs. server worst case (design rev.2 §8.3/§0.3 D3/R11):
	// includes/rest.php's neccospeak_rest_chat() can issue the upstream
	// request twice (the G17 400-retry), each with a 60s wp_remote_post
	// timeout — a worst case of about 120s. 125s leaves a small margin.
	$chat_timeout = isset( $opts['chat_timeout_seconds'] ) ? (int) $opts['chat_timeout_seconds'] : 130;
	if ( $chat_timeout < 125 ) {
		$items[] = array(
			'text' => __( 'The client request timeout is shorter than the server’s worst-case processing time (upstream retries can take up to about 120 seconds) — a slow reply may be cut off and cannot be retried.', 'neccospeak' ),
			'tab'  => 'general',
		);
	}

	if ( empty( $items ) ) {
		return;
	}

	echo '<div class="notice notice-warning"><p><strong>' . esc_html__( 'Setup checklist', 'neccospeak' ) . '</strong></p><ul style="list-style:disc;margin-left:1.5em;">';
	foreach ( $items as $item ) {
		$url = add_query_arg(
			array(
				'page' => NECCOSPEAK_SETTINGS_SLUG,
				'tab'  => $item['tab'],
			),
			admin_url( 'admin.php' )
		);
		echo '<li>' . esc_html( $item['text'] ) . ' <a href="' . esc_url( $url ) . '">' . esc_html__( 'Fix this', 'neccospeak' ) . '</a></li>';
	}
	echo '</ul></div>';
}

/**
 * General tab: enable toggle, API key, model + model fetch, temperature,
 * max output tokens.
 *
 * @param array $opts neccospeak_get_options().
 * @return void
 */
function neccospeak_render_tab_general( $opts ) {
	$stored_cipher = isset( $opts['openai_api_key'] ) ? (string) $opts['openai_api_key'] : '';
	$has_key       = '' !== $stored_cipher;
	$decrypted     = $has_key ? neccospeak_decrypt( $stored_cipher ) : '';
	$key_ok        = '' !== $decrypted;
	$model_list    = array( 'gpt-4o-mini', 'gpt-4o', 'gpt-4.1-mini', 'gpt-4.1' );
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Enable', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[enabled]" value="1" <?php checked( ! empty( $opts['enabled'] ) ); ?> />
						<?php echo esc_html__( 'Enable the chat widget', 'neccospeak' ); ?>
					</label>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_api_key"><?php echo esc_html__( 'OpenAI API key', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="password"
						id="neccospeak_api_key"
						class="regular-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[openai_api_key]"
						value=""
						autocomplete="new-password"
						placeholder="<?php echo $has_key ? esc_attr__( 'Saved (••••••••) — enter a new key only to change it', 'neccospeak' ) : esc_attr__( 'sk-...', 'neccospeak' ); ?>"
					/>
					<p class="description">
						<?php
						if ( $has_key && $key_ok ) {
							echo esc_html__( 'An API key is already saved. Enter a new key only if you want to change it (leaving this field blank keeps the current key).', 'neccospeak' );
						} elseif ( $has_key ) {
							$diag_url = add_query_arg(
								array(
									'page' => NECCOSPEAK_SETTINGS_SLUG,
									'tab'  => 'diagnostics',
								),
								admin_url( 'admin.php' )
							);
							echo esc_html__( 'A key is saved but can no longer be decrypted.', 'neccospeak' ) . ' <a href="' . esc_url( $diag_url ) . '">' . esc_html__( 'View diagnostics', 'neccospeak' ) . '</a>.';
						} else {
							echo esc_html__( 'Enter your OpenAI API key. The key is stored encrypted and is never sent to the frontend.', 'neccospeak' );
						}
						?>
					</p>
					<?php if ( function_exists( 'neccospeak_draft_workflow_active' ) && neccospeak_draft_workflow_active() ) : ?>
						<p class="description">
							<?php echo esc_html__( 'The API key always takes effect immediately, even when the draft workflow is enabled.', 'neccospeak' ); ?>
						</p>
					<?php endif; ?>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_model"><?php echo esc_html__( 'Model', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="text"
						id="neccospeak_model"
						class="regular-text"
						list="neccospeak-model-list"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[model]"
						value="<?php echo esc_attr( $opts['model'] ); ?>"
					/>
					<datalist id="neccospeak-model-list">
						<?php foreach ( $model_list as $m ) : ?>
							<option value="<?php echo esc_attr( $m ); ?>"></option>
						<?php endforeach; ?>
					</datalist>
					<p class="description"><?php echo esc_html__( 'e.g. gpt-4o-mini (free-form input; you can also enter new model names not in the list)', 'neccospeak' ); ?></p>
					<p>
						<button type="button" class="button" id="neccospeak-fetch-models"><?php echo esc_html__( 'Fetch available models', 'neccospeak' ); ?></button>
						<span id="neccospeak-fetch-status" role="status" aria-live="polite"></span>
					</p>
					<p class="description"><?php echo esc_html__( 'Fetches the model list from OpenAI and adds it to the suggestions (save your API key first; fetching itself incurs no token charges).', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_temperature"><?php echo esc_html__( 'Creativity (temperature)', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="number"
						min="0"
						max="2"
						step="0.1"
						id="neccospeak_temperature"
						class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[temperature]"
						value="<?php echo esc_attr( null === $opts['temperature'] ? '' : (string) $opts['temperature'] ); ?>"
						placeholder="<?php echo esc_attr__( 'Default value', 'neccospeak' ); ?>"
					/>
					<p class="description"><?php echo esc_html__( 'Optional, 0–2. Lower values give more grounded, consistent answers; higher values give more creative answers. Leave blank to use the model default.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_max_tokens"><?php echo esc_html__( 'Maximum tokens', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="number"
						min="1"
						max="32000"
						step="1"
						id="neccospeak_max_tokens"
						class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[max_tokens]"
						value="<?php echo esc_attr( null === $opts['max_tokens'] ? '' : (string) $opts['max_tokens'] ); ?>"
						placeholder="<?php echo esc_attr__( 'Default value', 'neccospeak' ); ?>"
					/>
					<p class="description"><?php echo esc_html__( 'Optional, 1–32000. Leave blank to use the model\'s default output limit.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_chat_timeout_seconds"><?php echo esc_html__( 'Client request timeout (seconds)', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="number"
						min="10"
						max="240"
						step="1"
						id="neccospeak_chat_timeout_seconds"
						class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[chat_timeout_seconds]"
						value="<?php echo esc_attr( (string) $opts['chat_timeout_seconds'] ); ?>"
					/>
					<p class="description"><?php echo esc_html__( 'How long the visitor\'s browser waits for a response before showing a timeout message (10-240). Keep it at 130 or higher unless you know your server responds faster. Shorter values may cut off replies that are still being generated.', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>

	<?php // Section: Analytics (G37). Design phase4 §9.3. ?>
	<h2 class="title"><?php echo esc_html__( 'Analytics', 'neccospeak' ); ?></h2>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Send events to the data layer', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[ga_enabled]" value="1" <?php checked( ! empty( $opts['ga_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Enable Google Tag Manager / Analytics integration', 'neccospeak' ); ?>
					</label>
					<p class="description"><?php echo esc_html__( 'Sends events to the window.dataLayer array created by Google Tag Manager and similar tools. NeccoSpeak itself never sends anything to external services, and message text is never included. If no data layer exists on the page, none is created.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Events', 'neccospeak' ); ?></th>
				<td>
					<?php
					$ga_event_labels = array(
						'open'              => __( 'Widget opened', 'neccospeak' ),
						'close'             => __( 'Widget closed', 'neccospeak' ),
						'message_sent'      => __( 'Visitor sent a message', 'neccospeak' ),
						'message_received'  => __( 'Reply received from the assistant', 'neccospeak' ),
						'voice_start'       => __( 'Voice conversation started', 'neccospeak' ),
						'voice_end'         => __( 'Voice conversation ended', 'neccospeak' ),
						'cta_click'         => __( 'Quick action / CTA triggered', 'neccospeak' ),
					);
					$ga_events_selected = ( isset( $opts['ga_events'] ) && is_array( $opts['ga_events'] ) ) ? $opts['ga_events'] : array();
					foreach ( $ga_event_labels as $ga_event_val => $ga_event_label ) :
						?>
						<label style="display:block;">
							<input type="checkbox"
								name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[ga_events][]"
								value="<?php echo esc_attr( $ga_event_val ); ?>"
								<?php checked( in_array( $ga_event_val, $ga_events_selected, true ) ); ?> />
							<?php echo esc_html( $ga_event_label ); ?>
						</label>
					<?php endforeach; ?>
					<p class="description"><?php echo esc_html__( 'If the visitor closes the tab or leaves the page, the voice_end event is not sent, so start counts may exceed end counts.', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>

	<?php // Section: Draft/publish workflow (G2). Design phase5 §4.1/§0.3 D1. ?>
	<h2 class="title"><?php echo esc_html__( 'Change management workflow', 'neccospeak' ); ?></h2>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Draft / publish workflow', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[draft_workflow_enabled]" value="1" <?php checked( ! empty( $opts['draft_workflow_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Save changes as drafts and publish them explicitly', 'neccospeak' ); ?>
					</label>
					<p class="description"><?php echo esc_html__( 'When enabled, saving a tab stores the changes as a draft — visitors keep seeing the currently published settings until you click "Publish". Either way, the full change history is kept, so you can compare and roll back at any time.', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>
	<?php
	if ( function_exists( 'neccospeak_admin_tab_slot' ) ) {
		neccospeak_admin_tab_slot( 'general_bottom', $opts );
	}
}

/**
 * Appearance tab, G23 theme sections: "Theme — Colors" / "Theme — Shape and
 * type" / "Theme — Background and custom CSS" (design phase4 §4.1). Split
 * out of neccospeak_render_tab_appearance() purely to keep that function
 * from growing past a readable length — no behavior boundary implied.
 *
 * @param array $opts neccospeak_get_options().
 * @return void
 */
function neccospeak_render_appearance_theme_sections( $opts ) {
	$header_styles = array(
		'solid'    => __( 'Solid color', 'neccospeak' ),
		'gradient' => __( 'Gradient', 'neccospeak' ),
	);
	$font_families = array(
		'system'  => __( 'System default', 'neccospeak' ),
		'sans'    => __( 'Sans-serif', 'neccospeak' ),
		'serif'   => __( 'Serif', 'neccospeak' ),
		'rounded' => __( 'Rounded sans-serif', 'neccospeak' ),
		'mono'    => __( 'Monospace', 'neccospeak' ),
		'inherit' => __( 'Inherit from page', 'neccospeak' ),
	);
	$can_css = current_user_can( 'unfiltered_html' );
	?>
	<h2 class="title"><?php echo esc_html__( 'Theme — Color settings', 'neccospeak' ); ?></h2>
	<p class="description"><?php echo esc_html__( 'Leave any field blank to use the default color for that element.', 'neccospeak' ); ?></p>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><label for="neccospeak_theme_panel_bg"><?php echo esc_html__( 'Panel background color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_render_color_field( 'theme_panel_bg', $opts, '#ffffff' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_panel_text"><?php echo esc_html__( 'Panel text color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_render_color_field( 'theme_panel_text', $opts, '#1f2533' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_panel_muted"><?php echo esc_html__( 'Muted text color', 'neccospeak' ); ?></label></th>
				<td>
					<?php neccospeak_render_color_field( 'theme_panel_muted', $opts, '#4b5563' ); ?>
					<p class="description"><?php echo esc_html__( 'Used for the status line, notes, placeholders, and read dots. Set this if the default gray is hard to read on a dark background.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_panel_border"><?php echo esc_html__( 'Panel border color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_render_color_field( 'theme_panel_border', $opts, '#e2e6ef' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_thread_bg"><?php echo esc_html__( 'Message list background color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_render_color_field( 'theme_thread_bg', $opts, '#f4f6fb' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_header_style"><?php echo esc_html__( 'Header style', 'neccospeak' ); ?></label></th>
				<td>
					<select id="neccospeak_theme_header_style" data-neccospeak-header-style name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[theme_header_style]">
						<?php foreach ( $header_styles as $hs_val => $hs_label ) : ?>
							<option value="<?php echo esc_attr( $hs_val ); ?>" <?php selected( $opts['theme_header_style'], $hs_val ); ?>><?php echo esc_html( $hs_label ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_header_bg"><?php echo esc_html__( 'Header background color', 'neccospeak' ); ?></label></th>
				<td>
					<?php neccospeak_render_color_field( 'theme_header_bg', $opts, isset( $opts['brand_color'] ) ? (string) $opts['brand_color'] : '#2f6fed' ); ?>
					<p class="description"><?php echo esc_html__( 'When the header style is set to "Gradient", this is also used as the gradient start color.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr data-neccospeak-header-gradient-row>
				<th scope="row"><label for="neccospeak_theme_header_bg2"><?php echo esc_html__( 'Header gradient end color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_render_color_field( 'theme_header_bg2', $opts, '#2f6fed' ); ?></td>
			</tr>
			<tr data-neccospeak-header-gradient-row>
				<th scope="row"><label for="neccospeak_theme_header_angle"><?php echo esc_html__( 'Header gradient angle', 'neccospeak' ); ?></label></th>
				<td>
					<input type="number" min="0" max="360" step="1" id="neccospeak_theme_header_angle" class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[theme_header_angle]"
						value="<?php echo esc_attr( (string) $opts['theme_header_angle'] ); ?>" /> <?php echo esc_html__( 'degrees', 'neccospeak' ); ?>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_header_text"><?php echo esc_html__( 'Header text color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_render_color_field( 'theme_header_text', $opts, '#ffffff' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_bubble_bot_bg"><?php echo esc_html__( 'AI bubble background color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_render_color_field( 'theme_bubble_bot_bg', $opts, '#ffffff' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_bubble_bot_text"><?php echo esc_html__( 'AI bubble text color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_render_color_field( 'theme_bubble_bot_text', $opts, '#1f2533' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_bubble_bot_border"><?php echo esc_html__( 'AI bubble border color', 'neccospeak' ); ?></label></th>
				<td>
					<?php neccospeak_render_color_field( 'theme_bubble_bot_border', $opts, '' ); ?>
					<p class="description"><?php echo esc_html__( 'If left blank, AI bubbles have no border by default.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_bubble_user_bg"><?php echo esc_html__( 'Visitor bubble background color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_render_color_field( 'theme_bubble_user_bg', $opts, isset( $opts['brand_color'] ) ? (string) $opts['brand_color'] : '#2f6fed' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_bubble_user_text"><?php echo esc_html__( 'Visitor bubble text color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_render_color_field( 'theme_bubble_user_text', $opts, '#ffffff' ); ?></td>
			</tr>
		</tbody>
	</table>

	<h2 class="title"><?php echo esc_html__( 'Theme — Corner radius (px)', 'neccospeak' ); ?></h2>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><label for="neccospeak_theme_panel_radius"><?php echo esc_html__( 'Panel corner radius', 'neccospeak' ); ?></label></th>
				<td>
					<input type="number" min="0" max="32" step="1" id="neccospeak_theme_panel_radius" class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[theme_panel_radius]"
						value="<?php echo esc_attr( (string) $opts['theme_panel_radius'] ); ?>" /> <?php echo esc_html( _x( 'px', 'abbreviation for pixels, shown right after a number input', 'neccospeak' ) ); ?>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_bubble_radius"><?php echo esc_html__( 'Bubble corner radius', 'neccospeak' ); ?></label></th>
				<td>
					<input type="number" min="0" max="24" step="1" id="neccospeak_theme_bubble_radius" class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[theme_bubble_radius]"
						value="<?php echo esc_attr( (string) $opts['theme_bubble_radius'] ); ?>" /> <?php echo esc_html( _x( 'px', 'abbreviation for pixels, shown right after a number input', 'neccospeak' ) ); ?>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_button_radius"><?php echo esc_html__( 'Button corner radius', 'neccospeak' ); ?></label></th>
				<td>
					<input type="number" min="0" max="24" step="1" id="neccospeak_theme_button_radius" class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[theme_button_radius]"
						value="<?php echo esc_attr( (string) $opts['theme_button_radius'] ); ?>" /> <?php echo esc_html( _x( 'px', 'abbreviation for pixels, shown right after a number input', 'neccospeak' ) ); ?>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_font_family"><?php echo esc_html__( 'Font', 'neccospeak' ); ?></label></th>
				<td>
					<select id="neccospeak_theme_font_family" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[theme_font_family]">
						<?php foreach ( $font_families as $ff_val => $ff_label ) : ?>
							<option value="<?php echo esc_attr( $ff_val ); ?>" <?php selected( $opts['theme_font_family'], $ff_val ); ?>><?php echo esc_html( $ff_label ); ?></option>
						<?php endforeach; ?>
					</select>
					<p class="description"><?php echo esc_html__( 'Only fonts already installed on the visitor\'s device are used (no web fonts are downloaded).', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_font_size"><?php echo esc_html__( 'Base font size', 'neccospeak' ); ?></label></th>
				<td>
					<input type="number" min="12" max="20" step="1" id="neccospeak_theme_font_size" class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[theme_font_size]"
						value="<?php echo esc_attr( (string) $opts['theme_font_size'] ); ?>" /> <?php echo esc_html( _x( 'px', 'abbreviation for pixels, shown right after a number input', 'neccospeak' ) ); ?>
					<p class="description"><?php echo esc_html__( 'Message bubbles and controls also scale with this value.', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>

	<h2 class="title"><?php echo esc_html__( 'Theme — Background and custom CSS', 'neccospeak' ); ?></h2>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Background watermark image', 'neccospeak' ); ?></th>
				<td>
					<?php neccospeak_render_media_field( 'theme_watermark_id', $opts, 'large' ); ?>
					<p class="description"><?php echo esc_html__( 'Shown faintly behind the conversation area. Choose an image, such as a logo, that does not make the text hard to read.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_watermark_opacity"><?php echo esc_html__( 'Watermark opacity', 'neccospeak' ); ?></label></th>
				<td>
					<input type="number" min="0" max="30" step="1" id="neccospeak_theme_watermark_opacity" class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[theme_watermark_opacity]"
						value="<?php echo esc_attr( (string) $opts['theme_watermark_opacity'] ); ?>" /> <?php echo esc_html( _x( '%', 'percent sign shown right after a number input', 'neccospeak' ) ); ?>
					<p class="description"><?php echo esc_html__( 'Set to 0 to hide the watermark even when an image is selected.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_custom_css"><?php echo esc_html__( 'Custom CSS', 'neccospeak' ); ?></label></th>
				<td>
					<textarea id="neccospeak_custom_css" class="large-text code" rows="8"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[custom_css]"
						<?php echo $can_css ? '' : ' disabled="disabled"'; ?>><?php echo esc_textarea( isset( $opts['custom_css'] ) ? (string) $opts['custom_css'] : '' ); ?></textarea>
					<?php if ( ! $can_css ) : ?>
						<p class="description" style="color:#b45309;font-weight:600;">
							<?php echo esc_html__( 'Editing custom CSS requires the "unfiltered_html" capability. This field is read-only for your account, and saving this tab does not change its contents. On multisite, only network administrators can edit it.', 'neccospeak' ); ?>
						</p>
					<?php else : ?>
						<p class="description">
							<?php
							echo esc_html(
								sprintf(
									/* translators: %s: maximum size of the Additional CSS box, formatted as a localized number (e.g. "8,000"). */
									__( 'Up to %s bytes. The characters "<" and "\\" and the patterns @import, @charset, expression(), behavior, javascript:, and vbscript: are not allowed. CSS with unbalanced braces is not saved. Media query range syntax such as (width < 600px) is not allowed (use min-width / max-width instead). Target classes starting with .neccospeak-.', 'neccospeak' ),
									number_format_i18n( NECCOSPEAK_CUSTOM_CSS_MAX_BYTES )
								)
							);
							?>
						</p>
					<?php endif; ?>
				</td>
			</tr>
		</tbody>
	</table>
	<?php
}

/**
 * Appearance tab, G24 launcher sections: "Launcher" / "Launcher — Mobile"
 * (design phase4 §4.1).
 *
 * @param array $opts neccospeak_get_options().
 * @return void
 */
function neccospeak_render_appearance_launcher_sections( $opts ) {
	$launcher_icons = array(
		'chat'    => __( 'Chat bubble (default)', 'neccospeak' ),
		'bubbles' => __( 'Two speech bubbles', 'neccospeak' ),
		'help'    => __( 'Question mark', 'neccospeak' ),
		'sparkle' => __( 'Sparkle', 'neccospeak' ),
		'headset' => __( 'Headset', 'neccospeak' ),
		'mic'     => __( 'Microphone', 'neccospeak' ),
		'custom'  => __( 'Custom image', 'neccospeak' ),
	);
	$launcher_sizes = array(
		's' => __( 'Small (48px)', 'neccospeak' ),
		'm' => __( 'Medium (60px)', 'neccospeak' ),
		'l' => __( 'Large (72px)', 'neccospeak' ),
	);
	$launcher_positions = array(
		'bottom-right' => __( 'Bottom right', 'neccospeak' ),
		'bottom-left'  => __( 'Bottom left', 'neccospeak' ),
	);
	$launcher_shapes = array(
		'circle'  => __( 'Circle', 'neccospeak' ),
		'rounded' => __( 'Rounded square', 'neccospeak' ),
	);
	$launcher_animations = array(
		'none'   => __( 'None', 'neccospeak' ),
		'pulse'  => __( 'Pulse', 'neccospeak' ),
		'bounce' => __( 'Bounce', 'neccospeak' ),
	);
	$launcher_mobile_sizes = array(
		'inherit' => __( 'Same as desktop', 'neccospeak' ),
		's'       => __( 'Small (48px)', 'neccospeak' ),
		'm'       => __( 'Medium (60px)', 'neccospeak' ),
		'l'       => __( 'Large (72px)', 'neccospeak' ),
	);
	$launcher_mobile_positions = array(
		'inherit'      => __( 'Same as desktop', 'neccospeak' ),
		'bottom-right' => __( 'Bottom right', 'neccospeak' ),
		'bottom-left'  => __( 'Bottom left', 'neccospeak' ),
	);
	?>
	<h2 class="title"><?php echo esc_html__( 'Launcher', 'neccospeak' ); ?></h2>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_icon"><?php echo esc_html__( 'Icon', 'neccospeak' ); ?></label></th>
				<td>
					<select id="neccospeak_launcher_icon" data-neccospeak-launcher-icon name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_icon]">
						<?php foreach ( $launcher_icons as $li_val => $li_label ) : ?>
							<option value="<?php echo esc_attr( $li_val ); ?>" <?php selected( $opts['launcher_icon'], $li_val ); ?>><?php echo esc_html( $li_label ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
			<tr data-neccospeak-launcher-icon-custom-row>
				<th scope="row"><?php echo esc_html__( 'Custom icon image', 'neccospeak' ); ?></th>
				<td>
					<?php neccospeak_render_media_field( 'launcher_icon_id', $opts, 'thumbnail' ); ?>
					<p class="description"><?php echo esc_html__( 'Used only when "Icon" above is set to "Custom image".', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_size"><?php echo esc_html__( 'Size', 'neccospeak' ); ?></label></th>
				<td>
					<select id="neccospeak_launcher_size" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_size]">
						<?php foreach ( $launcher_sizes as $ls_val => $ls_label ) : ?>
							<option value="<?php echo esc_attr( $ls_val ); ?>" <?php selected( $opts['launcher_size'], $ls_val ); ?>><?php echo esc_html( $ls_label ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_position"><?php echo esc_html__( 'Position', 'neccospeak' ); ?></label></th>
				<td>
					<select id="neccospeak_launcher_position" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_position]">
						<?php foreach ( $launcher_positions as $lp_val => $lp_label ) : ?>
							<option value="<?php echo esc_attr( $lp_val ); ?>" <?php selected( $opts['launcher_position'], $lp_val ); ?>><?php echo esc_html( $lp_label ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_offset_x"><?php echo esc_html__( 'Horizontal offset', 'neccospeak' ); ?></label></th>
				<td>
					<input type="number" min="0" max="200" step="1" id="neccospeak_launcher_offset_x" class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_offset_x]"
						value="<?php echo esc_attr( (string) $opts['launcher_offset_x'] ); ?>" /> <?php echo esc_html( _x( 'px', 'abbreviation for pixels, shown right after a number input', 'neccospeak' ) ); ?>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_offset_y"><?php echo esc_html__( 'Offset from bottom edge', 'neccospeak' ); ?></label></th>
				<td>
					<input type="number" min="0" max="200" step="1" id="neccospeak_launcher_offset_y" class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_offset_y]"
						value="<?php echo esc_attr( (string) $opts['launcher_offset_y'] ); ?>" /> <?php echo esc_html( _x( 'px', 'abbreviation for pixels, shown right after a number input', 'neccospeak' ) ); ?>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_shape"><?php echo esc_html__( 'Shape', 'neccospeak' ); ?></label></th>
				<td>
					<select id="neccospeak_launcher_shape" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_shape]">
						<?php foreach ( $launcher_shapes as $lsh_val => $lsh_label ) : ?>
							<option value="<?php echo esc_attr( $lsh_val ); ?>" <?php selected( $opts['launcher_shape'], $lsh_val ); ?>><?php echo esc_html( $lsh_label ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
			<?php // Phase 7d (docs/phase7d-plan.md §4 B-9, item 13): launcher pill label. ?>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Label', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_label_enabled]" value="1" <?php checked( ! empty( $opts['launcher_label_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Show a text label on the launcher (it becomes a pill shape instead of a simple circle / rounded icon)', 'neccospeak' ); ?>
					</label>
					<p>
						<label for="neccospeak_launcher_label_text"><?php echo esc_html__( 'Label text', 'neccospeak' ); ?></label><br />
						<input type="text" id="neccospeak_launcher_label_text" class="regular-text"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_label_text]"
							value="<?php echo esc_attr( isset( $opts['launcher_label_text'] ) ? (string) $opts['launcher_label_text'] : '' ); ?>"
							placeholder="<?php echo esc_attr__( 'Ask the AI', 'neccospeak' ); ?>" />
					</p>
					<p class="description"><?php echo esc_html__( 'When off, the launcher returns to the simple circle / rounded shape above.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Badge', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_badge]" value="1" <?php checked( ! empty( $opts['launcher_badge'] ) ); ?> />
						<?php echo esc_html__( 'Show a small decorative badge on the launcher', 'neccospeak' ); ?>
					</label>
					<p class="description"><?php echo esc_html__( 'Decorative only — it does not represent an actual unread message count.', 'neccospeak' ); ?></p>
					<p>
						<label for="neccospeak_launcher_badge_text"><?php echo esc_html__( 'Badge text', 'neccospeak' ); ?></label><br />
						<input type="text" id="neccospeak_launcher_badge_text" class="small-text" maxlength="8"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_badge_text]"
							value="<?php echo esc_attr( isset( $opts['launcher_badge_text'] ) ? (string) $opts['launcher_badge_text'] : '' ); ?>"
							placeholder="1" />
					</p>
					<p class="description"><?php echo esc_html__( 'Up to 2 characters. If left blank, "1" is shown.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_tooltip"><?php echo esc_html__( 'Tooltip text', 'neccospeak' ); ?></label></th>
				<td>
					<input type="text" id="neccospeak_launcher_tooltip" class="regular-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_tooltip]"
						value="<?php echo esc_attr( isset( $opts['launcher_tooltip'] ) ? (string) $opts['launcher_tooltip'] : '' ); ?>" />
					<p class="description"><?php echo esc_html__( 'Shown on hover or keyboard focus. If left blank, nothing is shown.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_animation"><?php echo esc_html__( 'Attention animation', 'neccospeak' ); ?></label></th>
				<td>
					<select id="neccospeak_launcher_animation" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_animation]">
						<?php foreach ( $launcher_animations as $la_val => $la_label ) : ?>
							<option value="<?php echo esc_attr( $la_val ); ?>" <?php selected( $opts['launcher_animation'], $la_val ); ?>><?php echo esc_html( $la_label ); ?></option>
						<?php endforeach; ?>
					</select>
					<p class="description"><?php echo esc_html__( 'Automatically respects the visitor\'s OS "reduce motion" setting.', 'neccospeak' ); ?></p>
					<p>
						<label>
							<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_animation_once]" value="1" <?php checked( ! empty( $opts['launcher_animation_once'] ) ); ?> />
							<?php echo esc_html__( 'Play once and stop immediately when the widget is opened', 'neccospeak' ); ?>
						</label>
					</p>
				</td>
			</tr>
		</tbody>
	</table>

	<h2 class="title"><?php echo esc_html__( 'Launcher — Mobile', 'neccospeak' ); ?></h2>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Hide on mobile', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_mobile_hide]" value="1" <?php checked( ! empty( $opts['launcher_mobile_hide'] ) ); ?> />
						<?php echo esc_html__( 'Hide the launcher (and the chat panel) on narrow screens', 'neccospeak' ); ?>
					</label>
					<p class="description"><?php echo esc_html__( 'The chat panel is hidden as well — without the launcher, visitors would have no way to close a panel left open.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_mobile_size"><?php echo esc_html__( 'Size on mobile', 'neccospeak' ); ?></label></th>
				<td>
					<select id="neccospeak_launcher_mobile_size" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_mobile_size]">
						<?php foreach ( $launcher_mobile_sizes as $lms_val => $lms_label ) : ?>
							<option value="<?php echo esc_attr( $lms_val ); ?>" <?php selected( $opts['launcher_mobile_size'], $lms_val ); ?>><?php echo esc_html( $lms_label ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_mobile_position"><?php echo esc_html__( 'Position on mobile', 'neccospeak' ); ?></label></th>
				<td>
					<select id="neccospeak_launcher_mobile_position" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_mobile_position]">
						<?php foreach ( $launcher_mobile_positions as $lmp_val => $lmp_label ) : ?>
							<option value="<?php echo esc_attr( $lmp_val ); ?>" <?php selected( $opts['launcher_mobile_position'], $lmp_val ); ?>><?php echo esc_html( $lmp_label ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_mobile_offset_x"><?php echo esc_html__( 'Horizontal offset on mobile', 'neccospeak' ); ?></label></th>
				<td>
					<input type="number" min="0" max="200" step="1" id="neccospeak_launcher_mobile_offset_x" class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_mobile_offset_x]"
						value="<?php echo esc_attr( (string) $opts['launcher_mobile_offset_x'] ); ?>" /> <?php echo esc_html( _x( 'px', 'abbreviation for pixels, shown right after a number input', 'neccospeak' ) ); ?>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_mobile_offset_y"><?php echo esc_html__( 'Offset from bottom edge on mobile', 'neccospeak' ); ?></label></th>
				<td>
					<input type="number" min="0" max="200" step="1" id="neccospeak_launcher_mobile_offset_y" class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_mobile_offset_y]"
						value="<?php echo esc_attr( (string) $opts['launcher_mobile_offset_y'] ); ?>" /> <?php echo esc_html( _x( 'px', 'abbreviation for pixels, shown right after a number input', 'neccospeak' ) ); ?>
				</td>
			</tr>
		</tbody>
	</table>
	<?php
}

/**
 * Appearance tab: widget title/welcome message/brand color/display rules.
 *
 * @param array $opts neccospeak_get_options().
 * @return void
 */
function neccospeak_render_tab_appearance( $opts ) {
	if ( function_exists( 'neccospeak_admin_tab_slot' ) ) {
		neccospeak_admin_tab_slot( 'appearance_top', $opts ); // design phase4 §8.2 — the G6 live preview hooks here.
	}

	$rules = array(
		'all'     => __( 'Show on all pages', 'neccospeak' ),
		'include' => __( 'Show only on specified URLs', 'neccospeak' ),
		'exclude' => __( 'Show on all pages except specified URLs', 'neccospeak' ), // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- false positive: display-rule option key, not a get_posts() argument.
	);
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row">
					<label for="neccospeak_widget_title"><?php echo esc_html__( 'Widget title', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="text"
						id="neccospeak_widget_title"
						class="regular-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[widget_title]"
						value="<?php echo esc_attr( $opts['widget_title'] ); ?>"
					/>
					<p class="description"><?php echo esc_html__( 'The title shown at the top of the widget.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<?php // Phase 7d (docs/phase7d-plan.md §4 B-1, item 2): header logo image. ?>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Header logo', 'neccospeak' ); ?></th>
				<td>
					<?php neccospeak_render_media_field( 'header_logo_id', $opts, 'medium' ); ?>
					<p class="description"><?php echo esc_html__( 'Shown to the left of the widget title. Scaled down to a height of 24px. If left blank, no logo is shown.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_welcome_message"><?php echo esc_html__( 'Welcome message', 'neccospeak' ); ?></label>
				</th>
				<td>
					<textarea
						id="neccospeak_welcome_message"
						class="large-text"
						rows="2"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[welcome_message]"
					><?php echo esc_textarea( $opts['welcome_message'] ); ?></textarea>
					<p class="description"><?php echo esc_html__( 'The first message shown when the widget is opened.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_brand_color"><?php echo esc_html__( 'Primary color', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="text"
						id="neccospeak_brand_color"
						class="regular-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[brand_color]"
						value="<?php echo esc_attr( $opts['brand_color'] ); ?>"
						placeholder="#2f6fed"
					/>
					<p class="description"><?php echo esc_html__( 'Color for the widget\'s buttons, header, and accents.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_display_rule"><?php echo esc_html__( 'Display rule', 'neccospeak' ); ?></label>
				</th>
				<td>
					<select id="neccospeak_display_rule" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[display_rule]">
						<?php foreach ( $rules as $val => $label ) : ?>
							<option value="<?php echo esc_attr( $val ); ?>" <?php selected( $opts['display_rule'], $val ); ?>><?php echo esc_html( $label ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_included_urls"><?php echo esc_html__( 'URLs to show on (one per line)', 'neccospeak' ); ?></label>
				</th>
				<td>
					<textarea
						id="neccospeak_included_urls"
						class="large-text"
						rows="3"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[included_urls]"
					><?php echo esc_textarea( $opts['included_urls'] ); ?></textarea>
					<p class="description"><?php echo esc_html__( 'Used when the display rule is "Show only on specified URLs".', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_excluded_urls"><?php echo esc_html__( 'URLs to exclude (one per line)', 'neccospeak' ); ?></label>
				</th>
				<td>
					<textarea
						id="neccospeak_excluded_urls"
						class="large-text"
						rows="3"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[excluded_urls]"
					><?php echo esc_textarea( $opts['excluded_urls'] ); ?></textarea>
					<p class="description"><?php echo esc_html__( 'Used when the display rule is "Show on all pages except specified URLs".', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>

	<?php neccospeak_render_appearance_theme_sections( $opts ); ?>
	<?php neccospeak_render_appearance_launcher_sections( $opts ); ?>

	<?php // Section 2: Message formatting (G30). Design rev.2 §8.2. ?>
	<h2 class="title"><?php echo esc_html__( 'Message display format', 'neccospeak' ); ?></h2>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Markdown rendering', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[markdown_enabled]" value="1" <?php checked( ! empty( $opts['markdown_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Render Markdown (headings, emphasis, lists, links, code) in assistant replies', 'neccospeak' ); ?>
					</label>
				</td>
			</tr>
			<?php // Phase 7d (docs/phase7d-plan.md §4 B-8, item 5): loading indicator style. ?>
			<tr>
				<th scope="row"><label for="neccospeak_loading_style"><?php echo esc_html__( 'Loading indicator', 'neccospeak' ); ?></label></th>
				<td>
					<?php
					$loading_styles = array(
						'dots'     => __( 'Three dots', 'neccospeak' ),
						'skeleton' => __( 'Skeleton placeholder', 'neccospeak' ),
					);
					?>
					<select id="neccospeak_loading_style" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[loading_style]">
						<?php foreach ( $loading_styles as $lst_val => $lst_label ) : ?>
							<option value="<?php echo esc_attr( $lst_val ); ?>" <?php selected( $opts['loading_style'], $lst_val ); ?>><?php echo esc_html( $lst_label ); ?></option>
						<?php endforeach; ?>
					</select>
					<p class="description"><?php echo esc_html__( 'Shown briefly while the assistant prepares a reply.', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>

	<?php // Section 3: Conversation continuity (G28). Design rev.2 §8.2. ?>
	<h2 class="title"><?php echo esc_html__( 'Conversation continuity', 'neccospeak' ); ?></h2>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Restore conversation', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[session_restore_enabled]" value="1" <?php checked( ! empty( $opts['session_restore_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Continue the conversation across page navigations within the same browser tab', 'neccospeak' ); ?>
					</label>
					<p class="description"><?php echo esc_html__( 'Stored only in the visitor\'s browser (sessionStorage) and cleared when the tab is closed. No identifiable information is stored.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_session_ttl_minutes"><?php echo esc_html__( 'Restore window (minutes)', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="number"
						min="5"
						max="1440"
						step="1"
						id="neccospeak_session_ttl_minutes"
						class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[session_ttl_minutes]"
						value="<?php echo esc_attr( (string) $opts['session_ttl_minutes'] ); ?>"
					/>
					<p class="description"><?php echo esc_html__( 'Saved conversations older than this are not restored (5–1440).', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_session_max_messages"><?php echo esc_html__( 'Maximum messages to store', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="number"
						min="5"
						max="100"
						step="1"
						id="neccospeak_session_max_messages"
						class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[session_max_messages]"
						value="<?php echo esc_attr( (string) $opts['session_max_messages'] ); ?>"
					/>
					<p class="description"><?php echo esc_html__( 'Only the most recent messages are kept, up to this count (5–100).', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_session_max_bytes"><?php echo esc_html__( 'Maximum size to store (bytes)', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="number"
						min="4000"
						max="200000"
						step="1"
						id="neccospeak_session_max_bytes"
						class="small-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[session_max_bytes]"
						value="<?php echo esc_attr( (string) $opts['session_max_bytes'] ); ?>"
					/>
					<p class="description"><?php echo esc_html__( 'When the saved conversation exceeds this size, the oldest messages are discarded (4000–200000).', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>

	<?php // Section 4: AI disclaimer (G22). Design rev.2 §8.2. ?>
	<h2 class="title"><?php echo esc_html__( 'AI disclaimer', 'neccospeak' ); ?></h2>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Always-visible note', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[disclaimer_inline_enabled]" value="1" <?php checked( ! empty( $opts['disclaimer_inline_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Always show a disclaimer note below the message input field', 'neccospeak' ); ?>
					</label>
					<p>
						<textarea
							class="large-text"
							rows="2"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[disclaimer_inline_text]"
							placeholder="<?php echo esc_attr__( 'AI answers may contain mistakes. Please verify important information.', 'neccospeak' ); ?>"
						><?php echo esc_textarea( $opts['disclaimer_inline_text'] ); ?></textarea>
					</p>
					<p class="description"><?php echo esc_html__( 'Optional. If left blank, the built-in text is used.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row"><?php echo esc_html__( 'First-open modal', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[disclaimer_modal_enabled]" value="1" <?php checked( ! empty( $opts['disclaimer_modal_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Require visitors to accept the disclaimer the first time they open the chat', 'neccospeak' ); ?>
					</label>
					<p>
						<textarea
							class="large-text"
							rows="3"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[disclaimer_modal_text]"
							placeholder="<?php echo esc_attr__( 'Answers are generated by AI and may be incomplete or inaccurate. Please do not enter personal or confidential information.', 'neccospeak' ); ?>"
						><?php echo esc_textarea( $opts['disclaimer_modal_text'] ); ?></textarea>
					</p>
					<p class="description"><?php echo esc_html__( 'Optional. If left blank, the built-in text is used.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_disclaimer_ack_scope"><?php echo esc_html__( 'How consent is remembered', 'neccospeak' ); ?></label>
				</th>
				<td>
					<?php
					$ack_scopes = array(
						'local'   => __( 'Keep across visits (this browser)', 'neccospeak' ),
						'session' => __( 'This tab only', 'neccospeak' ),
					);
					?>
					<select id="neccospeak_disclaimer_ack_scope" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[disclaimer_ack_scope]">
						<?php foreach ( $ack_scopes as $val => $label ) : ?>
							<option value="<?php echo esc_attr( $val ); ?>" <?php selected( $opts['disclaimer_ack_scope'], $val ); ?>><?php echo esc_html( $label ); ?></option>
						<?php endforeach; ?>
					</select>
					<p class="description"><?php echo esc_html__( 'Applies only to the "First-open modal" above.', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>

	<?php // Section 5: Home view (G33). Design rev.2 §8.2. ?>
	<h2 class="title"><?php echo esc_html__( 'Home screen', 'neccospeak' ); ?></h2>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Enable', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[home_enabled]" value="1" <?php checked( ! empty( $opts['home_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Enable the Home / Chat tab view, including the welcome screen', 'neccospeak' ); ?>
					</label>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_home_default_view"><?php echo esc_html__( 'Default view on open', 'neccospeak' ); ?></label>
				</th>
				<td>
					<?php
					$home_default_views = array(
						'home' => __( 'Home', 'neccospeak' ),
						'chat' => __( 'Chat', 'neccospeak' ),
					);
					?>
					<select id="neccospeak_home_default_view" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[home_default_view]">
						<?php foreach ( $home_default_views as $val => $label ) : ?>
							<option value="<?php echo esc_attr( $val ); ?>" <?php selected( $opts['home_default_view'], $val ); ?>><?php echo esc_html( $label ); ?></option>
						<?php endforeach; ?>
					</select>
					<p class="description"><?php echo esc_html__( 'When a conversation is in progress (e.g. one restored from the previous page), the chat view opens regardless of this setting.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_home_headline"><?php echo esc_html__( 'Headline', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="text"
						id="neccospeak_home_headline"
						class="regular-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[home_headline]"
						value="<?php echo esc_attr( $opts['home_headline'] ); ?>"
						placeholder="<?php echo esc_attr__( 'How can we help you?', 'neccospeak' ); ?>"
					/>
					<p class="description"><?php echo esc_html__( 'Optional. If left blank, the built-in text is used.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_home_body"><?php echo esc_html__( 'Body text', 'neccospeak' ); ?></label>
				</th>
				<td>
					<textarea
						id="neccospeak_home_body"
						class="large-text"
						rows="2"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[home_body]"
						placeholder="<?php echo esc_attr__( 'Ask us anything — the AI assistant answers right here.', 'neccospeak' ); ?>"
					><?php echo esc_textarea( $opts['home_body'] ); ?></textarea>
					<p class="description"><?php echo esc_html__( 'Optional. If left blank, the built-in text is used.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row"><?php echo esc_html__( 'New conversation button', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[home_new_chat_enabled]" value="1" <?php checked( ! empty( $opts['home_new_chat_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Show a button to start a new conversation from the home screen', 'neccospeak' ); ?>
					</label>
				</td>
			</tr>

			<tr>
				<?php // Phase 7d (docs/phase7d-plan.md §4 B-10, item 16): this indicator now lives permanently in the header, not just the Home view. ?>
				<th scope="row"><?php echo esc_html__( 'Online status indicator', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[home_status_indicator]" value="1" <?php checked( ! empty( $opts['home_status_indicator'] ) ); ?> />
						<?php echo esc_html__( 'Show a static "Online" label in the widget header', 'neccospeak' ); ?>
					</label>
					<p class="description"><?php echo esc_html__( 'This is a static label only — it does not reflect real-time availability. It is now always shown in the header, not just on the home screen.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<?php // G25 home view details (design phase4 §2.3/§4.1: appended to this existing table). ?>
			<tr>
				<th scope="row"><label for="neccospeak_home_icon"><?php echo esc_html__( 'Welcome icon', 'neccospeak' ); ?></label></th>
				<td>
					<?php
					$home_icons = array(
						'none'    => __( 'None', 'neccospeak' ),
						'chat'    => __( 'Speech bubble', 'neccospeak' ),
						'bubbles' => __( 'Two speech bubbles', 'neccospeak' ),
						'sparkle' => __( 'Sparkle', 'neccospeak' ),
						'help'    => __( 'Question mark', 'neccospeak' ),
						'headset' => __( 'Headset', 'neccospeak' ),
						'custom'  => __( 'Custom image', 'neccospeak' ),
					);
					?>
					<select id="neccospeak_home_icon" data-neccospeak-home-icon name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[home_icon]">
						<?php foreach ( $home_icons as $hi_val => $hi_label ) : ?>
							<option value="<?php echo esc_attr( $hi_val ); ?>" <?php selected( $opts['home_icon'], $hi_val ); ?>><?php echo esc_html( $hi_label ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
			<tr data-neccospeak-home-icon-custom-row>
				<th scope="row"><?php echo esc_html__( 'Custom welcome icon image', 'neccospeak' ); ?></th>
				<td>
					<?php neccospeak_render_media_field( 'home_icon_id', $opts, 'medium' ); ?>
					<p class="description"><?php echo esc_html__( 'Used only when "Welcome icon" above is set to "Custom image".', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Avatar image', 'neccospeak' ); ?></th>
				<td>
					<?php neccospeak_render_media_field( 'home_avatar_id', $opts, 'thumbnail' ); ?>
					<p class="description"><?php echo esc_html__( 'Shown as a small circular image above the headline.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_home_capabilities"><?php echo esc_html__( 'Suggested questions (one per line)', 'neccospeak' ); ?></label></th>
				<td>
					<textarea id="neccospeak_home_capabilities" class="large-text" rows="4"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[home_capabilities]"
					><?php echo esc_textarea( isset( $opts['home_capabilities'] ) ? (string) $opts['home_capabilities'] : '' ); ?></textarea>
					<p class="description"><?php echo esc_html__( 'Shown as clickable chips on the home screen. Up to 6 lines; each line is truncated to 60 characters.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<?php // Phase 7d (docs/phase7d-plan.md §4 B-2, item 3): non-clickable "capability" tags, distinct from the clickable Suggested questions above. ?>
			<tr>
				<th scope="row"><label for="neccospeak_home_capability_tags"><?php echo esc_html__( 'Capability tags (one per line)', 'neccospeak' ); ?></label></th>
				<td>
					<textarea id="neccospeak_home_capability_tags" class="large-text" rows="4"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[home_capability_tags]"
					><?php echo esc_textarea( isset( $opts['home_capability_tags'] ) ? (string) $opts['home_capability_tags'] : '' ); ?></textarea>
					<p class="description"><?php echo esc_html__( 'Shown as small display-only tags on the home screen (unlike "Suggested questions" above, they cannot be clicked). Up to 6 lines; each line is shortened to 60 characters.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_home_input_placeholder"><?php echo esc_html__( 'Input placeholder', 'neccospeak' ); ?></label></th>
				<td>
					<input type="text" id="neccospeak_home_input_placeholder" class="regular-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[home_input_placeholder]"
						value="<?php echo esc_attr( isset( $opts['home_input_placeholder'] ) ? (string) $opts['home_input_placeholder'] : '' ); ?>"
						placeholder="<?php echo esc_attr__( 'Type a message…', 'neccospeak' ); ?>" />
					<p class="description"><?php echo esc_html__( 'Applied only while the home screen is shown. If left blank, the default placeholder is used.', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>

	<?php // Section 6: Fallback (G35). Design rev.2 §8.2. ?>
	<h2 class="title"><?php echo esc_html_x( 'Fallback', 'settings section heading', 'neccospeak' ); ?></h2>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row">
					<label for="neccospeak_fallback_contact_url"><?php echo esc_html__( 'Contact link URL', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="url"
						id="neccospeak_fallback_contact_url"
						class="regular-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[fallback_contact_url]"
						value="<?php echo esc_attr( $opts['fallback_contact_url'] ); ?>"
						placeholder="https://example.com/contact"
					/>
					<p class="description"><?php echo esc_html__( 'Shown as a small link when the chat widget cannot run in the visitor\'s browser. If left blank, nothing is shown.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_fallback_contact_label"><?php echo esc_html__( 'Contact link label', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="text"
						id="neccospeak_fallback_contact_label"
						class="regular-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[fallback_contact_label]"
						value="<?php echo esc_attr( $opts['fallback_contact_label'] ); ?>"
						placeholder="<?php echo esc_attr__( 'Contact', 'neccospeak' ); ?>"
					/>
				</td>
			</tr>

			<?php // Phase 7d (docs/phase7d-plan.md §4 B-3, item 4): human-fallback phone number, shown alongside the contact link above. ?>
			<tr>
				<th scope="row">
					<label for="neccospeak_fallback_contact_phone"><?php echo esc_html__( 'Contact phone number', 'neccospeak' ); ?></label>
				</th>
				<td>
					<input
						type="text"
						id="neccospeak_fallback_contact_phone"
						class="regular-text"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[fallback_contact_phone]"
						value="<?php echo esc_attr( isset( $opts['fallback_contact_phone'] ) ? (string) $opts['fallback_contact_phone'] : '' ); ?>"
						placeholder="+1 555 0100"
					/>
					<p class="description"><?php echo esc_html__( 'Optional. Shown as a tel: link next to the contact link above. Shown only when the contact link URL is set.', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>

	<?php
	// Section 7: Progressive replies (G29). Design phase6 §9.5. Review fix:
	// stream_mode/stream_speed/stream_skip_enabled had defaults, a sanitizer,
	// a tab map entry, and frontend-config/JS/Diagnostics support, but no
	// admin control ever wrote to them — an admin had no way to turn this on
	// except a direct update_option() call.
	$stream_modes = array(
		'off'        => __( 'Off — show the whole reply at once', 'neccospeak' ),
		'typewriter' => __( 'Typewriter — reveal the received reply bit by bit', 'neccospeak' ),
		'sse'        => __( 'Live streaming (SSE) — relay the reply from OpenAI as it arrives', 'neccospeak' ),
	);
	$stream_speeds = array(
		'slow'   => __( 'Slow', 'neccospeak' ),
		'normal' => __( 'Standard', 'neccospeak' ),
		'fast'   => __( 'Fast', 'neccospeak' ),
	);
	?>
	<h2 class="title"><?php echo esc_html__( 'Progressive replies', 'neccospeak' ); ?></h2>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row">
					<label for="neccospeak_stream_mode"><?php echo esc_html__( 'Display mode', 'neccospeak' ); ?></label>
				</th>
				<td>
					<select id="neccospeak_stream_mode" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[stream_mode]">
						<?php foreach ( $stream_modes as $sm_val => $sm_label ) : ?>
							<option value="<?php echo esc_attr( $sm_val ); ?>" <?php selected( $opts['stream_mode'], $sm_val ); ?>><?php echo esc_html( $sm_label ); ?></option>
						<?php endforeach; ?>
					</select>
					<p class="description"><?php echo esc_html__( 'Typewriter replays a reply the browser has already received, one character at a time. Live streaming relays OpenAI\'s response as it is generated and requires a compatible server — when unavailable, it automatically falls back to typewriter mode (see Diagnostics for details).', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row">
					<label for="neccospeak_stream_speed"><?php echo esc_html__( 'Display speed', 'neccospeak' ); ?></label>
				</th>
				<td>
					<select id="neccospeak_stream_speed" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[stream_speed]">
						<?php foreach ( $stream_speeds as $ss_val => $ss_label ) : ?>
							<option value="<?php echo esc_attr( $ss_val ); ?>" <?php selected( $opts['stream_speed'], $ss_val ); ?>><?php echo esc_html( $ss_label ); ?></option>
						<?php endforeach; ?>
					</select>
					<p class="description"><?php echo esc_html__( 'Used only when "Display mode" above is not off.', 'neccospeak' ); ?></p>
				</td>
			</tr>

			<tr>
				<th scope="row"><?php echo esc_html__( 'Skip button', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[stream_skip_enabled]" value="1" <?php checked( ! empty( $opts['stream_skip_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Let visitors skip the reveal animation and show the whole reply at once', 'neccospeak' ); ?>
					</label>
				</td>
			</tr>
		</tbody>
	</table>
	<?php
	if ( function_exists( 'neccospeak_admin_tab_slot' ) ) {
		neccospeak_admin_tab_slot( 'appearance_bottom', $opts );
	}
}

/**
 * Content tab (Phase 6, design phase6 §9.6/§11.1, ★ new tab): "what the
 * assistant is allowed to see" — G15 per-page context + navigation + link
 * verification, G16 tools, G26 site content search. Reached via the same F5
 * dynamic-dispatch ('neccospeak_render_tab_' . str_replace('-','_',$tab)) as
 * every other form tab; no edit to neccospeak_render_settings_page() itself
 * was needed.
 *
 * Every value this renders that is owned by another Phase 6 agent's file
 * (includes/page-context.php / includes/tools.php / includes/rag.php) is
 * read through a function_exists() guard and degrades to "the row/notice is
 * simply not shown" — never a fatal — so this tab renders correctly whether
 * or not those files have landed yet (design phase6 §1.3 rule 1/2).
 *
 * @param array $opts neccospeak_get_options().
 * @return void
 */
function neccospeak_render_tab_content( $opts ) {
	$nav_types_selected = ( isset( $opts['nav_list_post_types'] ) && is_array( $opts['nav_list_post_types'] ) ) ? $opts['nav_list_post_types'] : array();
	$rag_types_selected = ( isset( $opts['rag_post_types'] ) && is_array( $opts['rag_post_types'] ) ) ? $opts['rag_post_types'] : array();

	/* ---- 1. Page context (G15) --------------------------------------- */
	echo '<h2 class="title">' . esc_html__( 'Page context', 'neccospeak' ) . '</h2>';
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Per-page instructions', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[page_context_enabled]" value="1" <?php checked( ! empty( $opts['page_context_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Use per-page instructions from the page editor', 'neccospeak' ); ?>
					</label>
					<p class="description"><?php echo esc_html__( 'Each post and page gets a "NeccoSpeak" box in the editor sidebar where you can add instructions just for that page, override the welcome message, hide the widget, or exclude the page from the assistant\'s content search. Editing that box requires a publishing-level role.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Page directory', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[nav_list_enabled]" value="1" <?php checked( ! empty( $opts['nav_list_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Give the assistant a list of your published pages', 'neccospeak' ); ?>
					</label>
					<p>
						<?php foreach ( array( 'page' => __( 'Pages', 'neccospeak' ), 'post' => __( 'Posts', 'neccospeak' ) ) as $nav_val => $nav_label ) : ?>
							<label style="margin-right:12px;">
								<input type="checkbox"
									name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[nav_list_post_types][]"
									value="<?php echo esc_attr( $nav_val ); ?>"
									<?php checked( in_array( $nav_val, $nav_types_selected, true ) ); ?> />
								<?php echo esc_html( $nav_label ); ?>
							</label>
						<?php endforeach; ?>
					</p>
					<p>
						<label for="neccospeak_nav_list_max"><?php echo esc_html__( 'At most', 'neccospeak' ); ?></label>
						<input type="number" id="neccospeak_nav_list_max" min="1" max="50" class="small-text"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[nav_list_max]"
							value="<?php echo esc_attr( (string) $opts['nav_list_max'] ); ?>" />
						<?php echo esc_html__( 'pages', 'neccospeak' ); ?>
					</p>
					<?php if ( function_exists( 'neccospeak_page_context_nav_fit' ) ) : ?>
						<?php $nav_fit = neccospeak_page_context_nav_fit( 'text' ); ?>
						<?php if ( is_array( $nav_fit ) && isset( $nav_fit['total'], $nav_fit['used'] ) && $nav_fit['total'] > 0 ) : ?>
							<p class="description">
								<?php
								echo esc_html(
									sprintf(
										/* translators: 1: number of pages that fit the prompt budget, 2: total number of published pages considered. */
										__( '%1$s of %2$s pages fit the prompt budget.', 'neccospeak' ),
										number_format_i18n( $nav_fit['used'] ),
										number_format_i18n( $nav_fit['total'] )
									)
								);
								?>
							</p>
						<?php endif; ?>
					<?php endif; ?>
				</td>
			</tr>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Link checking', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[link_verify_enabled]" value="1" <?php checked( ! empty( $opts['link_verify_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Remove links to pages that do not exist', 'neccospeak' ); ?>
					</label>
					<p class="description"><?php echo esc_html__( 'Checks any link the assistant produces to a page on this site before showing it to a visitor, and removes it (keeping the surrounding text) if the page cannot be found.', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>

	<?php
	/* ---- 2. Tools (G16) ------------------------------------------------ */
	echo '<h2 class="title">' . esc_html__( 'Tools', 'neccospeak' ) . '</h2>';
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Enable tools', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" data-neccospeak-master-toggle="tools" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[tools_enabled]" value="1" <?php checked( ! empty( $opts['tools_enabled'] ) ); ?> />
						<?php echo esc_html__( 'Let the assistant use tools', 'neccospeak' ); ?>
					</label>
				</td>
			</tr>
			<tr data-neccospeak-toggle-target="tools">
				<th scope="row"><?php echo esc_html__( 'Available tools', 'neccospeak' ); ?></th>
				<td>
					<label style="display:block;">
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[tools_site_search]" value="1" <?php checked( ! empty( $opts['tools_site_search'] ) ); ?> />
						<?php echo esc_html__( 'Search this site', 'neccospeak' ); ?>
					</label>
					<label style="display:block;">
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[tools_get_page_info]" value="1" <?php checked( ! empty( $opts['tools_get_page_info'] ) ); ?> />
						<?php echo esc_html__( 'Look up a page', 'neccospeak' ); ?>
					</label>
					<?php /* 2026-08 撤去記録: 'Current date and time' チェックボックス
					(tools_current_datetime) をここで削除 (0.3.0-dev.60) —
					includes/tools.php の撤去記録コメント参照。日時は現在、設定不要で
					常に AI に渡される (includes/prompt.php の current_datetime
					セクション)。 */ ?>
					<p class="description"><?php echo esc_html__( 'Both are read-only — the assistant can look things up but never change anything on your site.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr data-neccospeak-toggle-target="tools">
				<th scope="row"><label for="neccospeak_tools_instructions"><?php echo esc_html__( 'How to use them (optional)', 'neccospeak' ); ?></label></th>
				<td>
					<textarea id="neccospeak_tools_instructions" class="large-text" rows="3"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[tools_instructions]"
					><?php echo esc_textarea( isset( $opts['tools_instructions'] ) ? (string) $opts['tools_instructions'] : '' ); ?></textarea>
					<p class="description">
						<?php echo esc_html__( 'Optional guidance for the assistant on when to use each tool.', 'neccospeak' ); ?>
						<?php $tools_specs = function_exists( 'neccospeak_field_specs' ) ? neccospeak_field_specs() : array(); ?>
						<?php if ( isset( $tools_specs['tools_instructions']['max'] ) ) : ?>
							<?php echo esc_html( neccospeak_field_max_bytes_note( $tools_specs['tools_instructions']['max'] ) ); ?>
						<?php endif; ?>
					</p>
					<?php if ( function_exists( 'neccospeak_tools_instruction_fit' ) ) : ?>
						<?php $tools_fit = neccospeak_tools_instruction_fit( $opts, 'text' ); ?>
						<?php if ( is_array( $tools_fit ) && isset( $tools_fit['total'], $tools_fit['used'] ) && $tools_fit['total'] > 0 && $tools_fit['used'] < $tools_fit['total'] ) : ?>
							<p class="description" style="color:#b45309;">
								<?php echo esc_html__( 'This does not entirely fit the prompt budget and was shortened.', 'neccospeak' ); ?>
							</p>
						<?php endif; ?>
					<?php endif; ?>
				</td>
			</tr>
		</tbody>
	</table>
	<p class="description"><?php echo esc_html__( 'Each tool adds one extra round trip to the AI service when it is used, which increases both latency and token usage — up to 2 extra round trips per message, so one message can cost up to 3 requests. Consider lowering the values on the Rate Limits tab to about a third of what you use without tools.', 'neccospeak' ); ?></p>

	<?php
	/* ---- 3. Site content (G26) ------------------------------------------ */
	echo '<h2 class="title">' . esc_html__( 'Site content', 'neccospeak' ) . '</h2>';
	$rag_state  = function_exists( 'neccospeak_rag_state' ) ? neccospeak_rag_state() : array();
	$rag_engine = function_exists( 'neccospeak_rag_engine' ) ? neccospeak_rag_engine() : 'none';
	$rag_stale  = ! empty( $rag_state['stale'] );
	$engine_labels = array(
		'fulltext' => __( 'Full-text (ngram)', 'neccospeak' ),
		'like'     => __( 'Keyword fallback', 'neccospeak' ),
		'none'     => __( 'Not available yet', 'neccospeak' ),
	);
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Site content search', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[rag_enabled]" value="1" <?php checked( ! empty( $opts['rag_enabled'] ) ); ?> />
						<?php echo esc_html__( "Let the assistant quote your site's content", 'neccospeak' ); ?>
					</label>
					<p>
						<?php foreach ( array( 'post' => __( 'Posts', 'neccospeak' ), 'page' => __( 'Pages', 'neccospeak' ) ) as $rag_val => $rag_label ) : ?>
							<label style="margin-right:12px;">
								<input type="checkbox"
									name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[rag_post_types][]"
									value="<?php echo esc_attr( $rag_val ); ?>"
									<?php checked( in_array( $rag_val, $rag_types_selected, true ) ); ?> />
								<?php echo esc_html( $rag_label ); ?>
							</label>
						<?php endforeach; ?>
					</p>
					<p>
						<label for="neccospeak_rag_top_n"><?php echo esc_html__( 'Include up to', 'neccospeak' ); ?></label>
						<input type="number" id="neccospeak_rag_top_n" min="1" max="8" class="small-text"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[rag_top_n]"
							value="<?php echo esc_attr( (string) $opts['rag_top_n'] ); ?>" />
						<?php echo esc_html__( 'excerpts per answer', 'neccospeak' ); ?>
					</p>
					<p class="description"><?php echo esc_html__( 'Content generated by shortcodes is not indexed. Nothing is sent to any external service for indexing or search — only the excerpts actually quoted in an answer travel to the AI service, the same way any other part of the prompt does.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Content index', 'neccospeak' ); ?></th>
				<td id="neccospeak-rag-status">
					<?php if ( ! empty( $rag_state ) ) : ?>
						<p>
							<?php
							echo esc_html(
								sprintf(
									/* translators: 1: number of indexed content chunks, 2: number of indexed pages/posts. */
									__( 'Index status: %1$s chunks from %2$s pages.', 'neccospeak' ),
									number_format_i18n( isset( $rag_state['chunks'] ) ? (int) $rag_state['chunks'] : 0 ),
									number_format_i18n( isset( $rag_state['posts'] ) ? (int) $rag_state['posts'] : 0 )
								)
							);
							?>
							<br />
							<?php
							echo esc_html(
								sprintf(
									/* translators: %s: search engine currently in use (e.g. "Full-text (ngram)"). */
									__( 'Search engine: %s', 'neccospeak' ),
									isset( $engine_labels[ $rag_engine ] ) ? $engine_labels[ $rag_engine ] : $rag_engine
								)
							);
							?>
							<?php if ( ! empty( $rag_state['last_sync'] ) ) : ?>
								<br />
								<?php
								echo esc_html(
									sprintf(
										/* translators: %s: timestamp of the last content index rebuild. */
										__( 'Last synced: %s', 'neccospeak' ),
										(string) $rag_state['last_sync']
									)
								);
								?>
							<?php endif; ?>
						</p>
					<?php endif; ?>
					<?php if ( $rag_stale ) : ?>
						<p style="color:#b32d2e;font-weight:600;"><?php echo esc_html__( 'The content index needs to be rebuilt. While this feature was off, new and updated pages were not indexed — search is paused until you rebuild.', 'neccospeak' ); ?></p>
					<?php endif; ?>
					<p>
						<button type="button" id="neccospeak-rag-sync-btn" class="button<?php echo $rag_stale ? ' button-primary' : ''; ?>" data-neccospeak-rag-nonce="<?php echo esc_attr( wp_create_nonce( 'neccospeak_rag_sync' ) ); ?>">
							<?php echo esc_html__( 'Rebuild index', 'neccospeak' ); ?>
						</button>
					</p>
					<p id="neccospeak-rag-sync-progress" hidden>
						<progress id="neccospeak-rag-sync-bar" max="100" value="0" style="width:320px;max-width:100%;"></progress>
						<span id="neccospeak-rag-sync-status" role="status" aria-live="polite"></span>
					</p>
				</td>
			</tr>
		</tbody>
	</table>
	<?php
	if ( function_exists( 'neccospeak_admin_tab_slot' ) ) {
		neccospeak_admin_tab_slot( 'content_bottom', $opts );
	}
}

/**
 * Prompt tab (G11/G12/G13/G18): structured prompt builder, plus a read-only
 * preview of the composed, byte-budgeted prompt for both 'text' and
 * 'realtime' modes — computed via impl-core's
 * neccospeak_compose_system_prompt_ex() (includes/prompt.php). Preview
 * reflects the saved option values only; live (pre-save) recomposition is
 * out of scope (design rev.2 §11/§12.1).
 *
 * Layout (design rev.2 §12.1, review C10): Quick start -> Base instructions
 * -> Assistant profile -> FAQ -> Tone and personality -> Answer rules ->
 * Preview. 'Base instructions' (the existing system_prompt field) sits
 * second, immediately after Quick start, because it is priority 0 in
 * includes/prompt.php's section ordering — it is placed at the very front of
 * the generated prompt, so the UI position and its label now agree with what
 * it actually does (rev.1 put it last under the confusing label "Additional
 * instructions").
 *
 * @param array $opts neccospeak_get_options().
 * @return void
 */
function neccospeak_render_tab_prompt( $opts ) {
	$specs = function_exists( 'neccospeak_field_specs' ) ? neccospeak_field_specs() : array();
	$faq   = ( isset( $opts['prompt_faq'] ) && is_array( $opts['prompt_faq'] ) ) ? $opts['prompt_faq'] : array();
	// C12 (design rev.2 §4.4/§12.1): guard every row's shape too — a legacy
	// install / direct DB edit can leave prompt_faq holding scalars or rows
	// missing 'q'/'a'. Count first so data-neccospeak-faq-next reflects only
	// the rows that will actually be rendered (S11 requires zero warnings
	// even when $opts['prompt_faq'] is a broken string).
	$faq_next = 0;
	foreach ( $faq as $faq_row ) {
		if ( is_array( $faq_row ) && isset( $faq_row['q'], $faq_row['a'] ) ) {
			++$faq_next;
		}
	}

	// ---- 1. Quick start (industry presets, G11 §6) -----------------------
	if ( function_exists( 'neccospeak_prompt_presets' ) ) {
		echo '<h2 class="title">' . esc_html__( 'Quick start', 'neccospeak' ) . '</h2>';
		echo '<p class="description">' . esc_html__( 'Fills in the fields below with an industry-specific starting point. Nothing is saved until you press the save button.', 'neccospeak' ) . '</p>';
		echo '<p>';
		foreach ( neccospeak_prompt_presets() as $preset_slug => $preset ) {
			if ( ! is_array( $preset ) || ! isset( $preset['label'], $preset['values'] ) ) {
				continue; // Defensive: never trust a third-party filter shape blindly.
			}
			printf(
				'<button type="button" class="button neccospeak-preset" data-neccospeak-preset-slug="%1$s" data-neccospeak-preset="%2$s">%3$s</button> ',
				esc_attr( $preset_slug ),
				esc_attr( wp_json_encode( $preset['values'] ) ),
				esc_html( $preset['label'] )
			);
		}
		echo '</p>';
		echo '<p id="neccospeak-preset-status" role="status" aria-live="polite"></p>';
	}

	// ---- 2. Base instructions (existing system_prompt, C10) ---------------
	echo '<h2 class="title">' . esc_html__( 'Base instructions', 'neccospeak' ) . '</h2>';
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row">
					<label for="neccospeak_system_prompt"><?php echo esc_html__( 'Base instructions', 'neccospeak' ); ?></label>
				</th>
				<td>
					<textarea
						id="neccospeak_system_prompt"
						class="large-text"
						rows="6"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[system_prompt]"
					><?php echo esc_textarea( $opts['system_prompt'] ); ?></textarea>
					<p class="description"><?php echo esc_html__( 'This text is placed at the very front of the generated prompt, before all the sections below.', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>
	<?php

	// ---- 3. Assistant profile (G11: role / goals / company) ---------------
	echo '<h2 class="title">' . esc_html__( 'Assistant profile', 'neccospeak' ) . '</h2>';
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row">
					<label for="neccospeak_prompt_role"><?php echo esc_html__( 'Role', 'neccospeak' ); ?></label>
				</th>
				<td>
					<p class="description" style="margin-top:0;"><?php echo esc_html__( 'Start from a template:', 'neccospeak' ); ?></p>
					<p>
						<?php
						$role_templates = array(
							'customer_support' => array(
								'label' => __( 'Customer support', 'neccospeak' ),
								'text'  => __( 'You are this site\'s customer support agent. You respond politely to visitors\' questions and problems, and help them through procedures and troubleshooting as needed.', 'neccospeak' ),
							),
							'sales_assistant'   => array(
								'label' => __( 'Sales assistant', 'neccospeak' ),
								'text'  => __( 'You are this site\'s sales assistant. You listen to visitors\' needs, suggest the best products and services, and guide them toward a purchase or an inquiry.', 'neccospeak' ),
							),
							'faq_guide'         => array(
								'label' => __( 'FAQ guide', 'neccospeak' ),
								'text'  => __( 'You are this site\'s FAQ guide. Focusing on answers to frequently asked questions, you help visitors quickly reach the information they are looking for.', 'neccospeak' ),
							),
							'internal_knowledge' => array(
								'label' => __( 'Internal knowledge search', 'neccospeak' ),
								'text'  => __( 'You are an internal knowledge search assistant. Based on internal information and documents, you answer employees\' questions accurately.', 'neccospeak' ),
							),
						);
						foreach ( $role_templates as $rt_slug => $rt ) :
							?>
							<button type="button" class="button" data-neccospeak-role-slug="<?php echo esc_attr( $rt_slug ); ?>"
								onclick="var t=document.getElementById('neccospeak_prompt_role'); t.value=<?php echo esc_attr( wp_json_encode( (string) $rt['text'] ) ); ?>;">
								<?php echo esc_html( $rt['label'] ); ?>
							</button>
						<?php endforeach; ?>
					</p>
					<textarea
						id="neccospeak_prompt_role"
						class="large-text"
						rows="3"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_role]"
					><?php echo esc_textarea( $opts['prompt_role'] ); ?></textarea>
					<p class="description">
						<?php echo esc_html__( 'Describe who the assistant is and what it helps visitors with.', 'neccospeak' ); ?>
						<?php if ( isset( $specs['prompt_role']['max'] ) ) : ?>
							<?php echo esc_html( neccospeak_field_max_bytes_note( $specs['prompt_role']['max'] ) ); ?>
						<?php endif; ?>
					</p>
				</td>
			</tr>
			<tr>
				<th scope="row">
					<label for="neccospeak_prompt_goals"><?php echo esc_html__( 'Goals', 'neccospeak' ); ?></label>
				</th>
				<td>
					<textarea
						id="neccospeak_prompt_goals"
						class="large-text"
						rows="3"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_goals]"
					><?php echo esc_textarea( $opts['prompt_goals'] ); ?></textarea>
					<p class="description">
						<?php echo esc_html__( 'Enter one goal per line — each describes what a successful conversation looks like.', 'neccospeak' ); ?>
						<?php if ( isset( $specs['prompt_goals']['max'] ) ) : ?>
							<?php echo esc_html( neccospeak_field_max_bytes_note( $specs['prompt_goals']['max'] ) ); ?>
						<?php endif; ?>
					</p>
				</td>
			</tr>
			<tr>
				<th scope="row">
					<label for="neccospeak_prompt_company"><?php echo esc_html__( 'Company and site information', 'neccospeak' ); ?></label>
				</th>
				<td>
					<textarea
						id="neccospeak_prompt_company"
						class="large-text"
						rows="4"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_company]"
					><?php echo esc_textarea( $opts['prompt_company'] ); ?></textarea>
					<p class="description">
						<?php echo esc_html__( 'Information the assistant should know about this company or site, such as business hours, location, and policies.', 'neccospeak' ); ?>
						<?php if ( isset( $specs['prompt_company']['max'] ) ) : ?>
							<?php echo esc_html( neccospeak_field_max_bytes_note( $specs['prompt_company']['max'] ) ); ?>
						<?php endif; ?>
					</p>
				</td>
			</tr>
		</tbody>
	</table>
	<?php

	// ---- 4. FAQ (G11 §4) ---------------------------------------------------
	echo '<h3>' . esc_html__( 'FAQ settings', 'neccospeak' ) . '</h3>';
	echo '<p class="description">' . esc_html__( 'Answers the assistant reuses as-is. Both the question and the answer are required. Up to 30 entries can be registered.', 'neccospeak' ) . '</p>';
	echo '<p class="description">' . esc_html(
		sprintf(
			/* translators: 1: question byte-limit note, 2: answer byte-limit note. */
			__( 'Question — %1$s Answer — %2$s', 'neccospeak' ),
			neccospeak_field_max_bytes_note( NECCOSPEAK_FAQ_Q_MAX_BYTES ),
			neccospeak_field_max_bytes_note( NECCOSPEAK_FAQ_A_MAX_BYTES )
		)
	) . '</p>';
	?>
	<table class="widefat neccospeak-faq-table" data-neccospeak-faq-max="<?php echo esc_attr( NECCOSPEAK_FAQ_MAX_ITEMS ); ?>" data-neccospeak-faq-next="<?php echo esc_attr( (string) $faq_next ); ?>">
		<thead>
			<tr>
				<th><?php echo esc_html__( 'Question', 'neccospeak' ); ?></th>
				<th><?php echo esc_html__( 'Answer', 'neccospeak' ); ?></th>
				<th></th>
			</tr>
		</thead>
		<tbody id="neccospeak-faq-rows">
			<?php
			$faq_i = 0;
			foreach ( $faq as $faq_row ) {
				if ( ! is_array( $faq_row ) || ! isset( $faq_row['q'], $faq_row['a'] ) ) {
					continue; // Corrupted / legacy shape (C12): skip silently, never warn.
				}
				?>
				<tr class="neccospeak-faq-row">
					<td>
						<input type="text" class="large-text"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_faq][<?php echo (int) $faq_i; ?>][q]"
							value="<?php echo esc_attr( (string) $faq_row['q'] ); ?>" />
					</td>
					<td>
						<textarea class="large-text" rows="2"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_faq][<?php echo (int) $faq_i; ?>][a]"
						><?php echo esc_textarea( (string) $faq_row['a'] ); ?></textarea>
					</td>
					<td><button type="button" class="button-link neccospeak-faq-remove"><?php echo esc_html__( 'Remove', 'neccospeak' ); ?></button></td>
				</tr>
				<?php
				++$faq_i;
			}
			?>
		</tbody>
		<tbody id="neccospeak-faq-proto" hidden>
			<tr class="neccospeak-faq-row">
				<td>
					<input type="text" class="large-text" disabled
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_faq][__i__][q]" value="" />
				</td>
				<td>
					<textarea class="large-text" rows="2" disabled
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_faq][__i__][a]"></textarea>
				</td>
				<td><button type="button" class="button-link neccospeak-faq-remove"><?php echo esc_html__( 'Remove', 'neccospeak' ); ?></button></td>
			</tr>
		</tbody>
	</table>
	<p>
		<button type="button" class="button" id="neccospeak-faq-add"><?php echo esc_html__( 'Add FAQ', 'neccospeak' ); ?></button>
		<span id="neccospeak-faq-status" role="status" aria-live="polite"></span>
	</p>
	<?php

	// ---- 5. Tone and personality (G12) -------------------------------------
	// Segmented "tone" control (d15/tone-select-vs-segmented-buttons): only the
	// 4 buttons matching production's 丁寧/ビジネス/フレンドリー/カジュアル are
	// offered as quick picks, mapped onto 4 of the existing enum values already
	// accepted by includes/settings.php's sanitizer (no new allowed values
	// introduced) — 'default'/'neutral'/'warm' remain valid stored values but
	// are no longer offered here.
	$tone_buttons = array(
		'formal'       => __( 'Polite', 'neccospeak' ),
		'professional' => __( 'Business', 'neccospeak' ),
		'friendly'     => __( 'Friendly', 'neccospeak' ),
		'casual'       => __( 'Casual', 'neccospeak' ),
	);
	$styles = array(
		'concise'  => __( 'Answer concisely with just the key points', 'neccospeak' ),
		'bullets'  => __( 'Present multiple items as a bulleted list', 'neccospeak' ),
		'examples' => __( 'Explain with concrete examples', 'neccospeak' ),
		'steps'    => __( 'Present procedures as numbered steps', 'neccospeak' ),
	);
	$persona_templates = array(
		'kind_support' => array(
			'label' => __( 'Kind support agent', 'neccospeak' ),
			'text'  => __( 'Always kind and supportive, you answer visitors\' worries and questions politely. You avoid jargon and explain things in plain language.', 'neccospeak' ),
		),
		'logical_expert' => array(
			'label' => __( 'Logical expert', 'neccospeak' ),
			'text'  => __( 'You value logical, accurate explanations and answer with supporting evidence and reasons. You avoid vague wording and communicate concisely based on facts.', 'neccospeak' ),
		),
		'friendly_navigator' => array(
			'label' => __( 'Friendly navigator', 'neccospeak' ),
			'text'  => __( 'In a friendly, cheerful tone, you act as a guide so visitors never get lost on the site.', 'neccospeak' ),
		),
	);
	$verbosities = array(
		'default'  => __( 'Default', 'neccospeak' ),
		'brief'    => __( 'Brief (about 1–3 sentences)', 'neccospeak' ),
		'balanced' => __( 'Balanced (about 3–6 sentences)', 'neccospeak' ),
		'detailed' => __( 'Detailed', 'neccospeak' ),
	);
	$prompt_styles_selected = ( isset( $opts['prompt_styles'] ) && is_array( $opts['prompt_styles'] ) ) ? $opts['prompt_styles'] : array();

	echo '<h2 class="title">' . esc_html__( 'Tone and personality', 'neccospeak' ) . '</h2>';
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Tone', 'neccospeak' ); ?></th>
				<td>
					<div class="ai-agent-segmented" style="display:flex;gap:8px;flex-wrap:wrap;">
						<?php foreach ( $tone_buttons as $tone_val => $tone_label ) : ?>
							<label style="display:inline-flex;align-items:center;gap:4px;padding:6px 12px;border:1px solid #8c8f94;border-radius:4px;cursor:pointer;">
								<input type="radio"
									name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_tone]"
									value="<?php echo esc_attr( $tone_val ); ?>"
									<?php checked( $opts['prompt_tone'], $tone_val ); ?> />
								<?php echo esc_html( $tone_label ); ?>
							</label>
						<?php endforeach; ?>
					</div>
				</td>
			</tr>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Answer style', 'neccospeak' ); ?></th>
				<td>
					<?php foreach ( $styles as $style_val => $style_label ) : ?>
						<label style="display:block;">
							<input type="checkbox"
								name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_styles][]"
								value="<?php echo esc_attr( $style_val ); ?>"
								<?php checked( in_array( $style_val, $prompt_styles_selected, true ) ); ?> />
							<?php echo esc_html( $style_label ); ?>
						</label>
					<?php endforeach; ?>
				</td>
			</tr>
			<tr>
				<th scope="row"><?php echo esc_html__( 'Persona', 'neccospeak' ); ?></th>
				<td>
					<?php
					// persona-select-vs-character-templates: the named-persona <select>
					// (none/assistant/concierge/.../custom) is replaced with 3 starter
					// template buttons that fill the always-visible free-text field
					// below. The underlying prompt_persona_template value is fixed to
					// the existing, already-allowed 'custom' enum value via a hidden
					// field so includes/settings.php's sanitizer keeps accepting it
					// unchanged.
					?>
					<input type="hidden" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_persona_template]" value="custom" />
					<p class="description" style="margin-top:0;"><?php echo esc_html__( 'Start from a template:', 'neccospeak' ); ?></p>
					<p>
						<?php foreach ( $persona_templates as $pt_slug => $pt ) : ?>
							<button type="button" class="button" data-neccospeak-persona-slug="<?php echo esc_attr( $pt_slug ); ?>"
								onclick="var t=document.getElementById('neccospeak_prompt_persona_custom'); t.value=<?php echo esc_attr( wp_json_encode( (string) $pt['text'] ) ); ?>;">
								<?php echo esc_html( $pt['label'] ); ?>
							</button>
						<?php endforeach; ?>
					</p>
					<textarea
						id="neccospeak_prompt_persona_custom"
						class="large-text"
						rows="2"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_persona_custom]"
					><?php echo esc_textarea( $opts['prompt_persona_custom'] ); ?></textarea>
					<p class="description"><?php echo esc_html__( 'The template buttons fill in a starting point. Feel free to edit the text.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row">
					<label for="neccospeak_prompt_verbosity"><?php echo esc_html__( 'Level of detail', 'neccospeak' ); ?></label>
				</th>
				<td>
					<select id="neccospeak_prompt_verbosity" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_verbosity]">
						<?php foreach ( $verbosities as $verbosity_val => $verbosity_label ) : ?>
							<option value="<?php echo esc_attr( $verbosity_val ); ?>" <?php selected( $opts['prompt_verbosity'], $verbosity_val ); ?>><?php echo esc_html( $verbosity_label ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
		</tbody>
	</table>
	<?php

	// ---- 6. Answer rules (G13) ---------------------------------------------
	// unknown-behavior-select-vs-radio / d12: only the 2 real choices matching
	// production's "正直に伝える" / "問い合わせ窓口を案内する" are kept as
	// existing, already-allowed enum values (none/admit/contact unchanged) —
	// production's 3rd "推測して答える" choice is not offered here because it
	// has no matching allowed value in includes/settings.php's sanitizer and
	// adding one is out of scope for this file.
	$unknown_behaviors = array(
		'none'    => __( 'No special instruction', 'neccospeak' ),
		'admit'   => __( 'Honestly say "I don\'t know"', 'neccospeak' ),
		'contact' => __( 'Direct the visitor to the contact desk', 'neccospeak' ),
	);

	// Quick "escalate to a human" preset phrases (d13/escalation-textarea-vs-checkbox-presets):
	// implemented as insert buttons (rather than bound checkboxes, which would
	// need a new array-backed option key) that append a canned line to the
	// existing free-text prompt_escalation textarea — the underlying field
	// stays the same single 'prompt_text' string option.
	$escalation_presets = array(
		__( 'When the AI repeatedly fails to answer (guideline: 3 times in a row)', 'neccospeak' ),
		__( 'When a complaint or dissatisfaction is expressed', 'neccospeak' ),
		__( 'For specific procedures around contracts, cancellations, or returns', 'neccospeak' ),
		__( 'For specific quotes or individual pricing consultations', 'neccospeak' ),
		__( 'For questions about personal or confidential information', 'neccospeak' ),
		__( 'When the user explicitly asks for a human operator', 'neccospeak' ),
		__( 'For urgent issues (system outages, security, etc.)', 'neccospeak' ),
	);

	// NG rule (prompt_dont) quick suggestions (missing-ng-rule-suggestion-buttons).
	$ng_rule_presets = array(
		__( 'Do not give definitive answers about pricing', 'neccospeak' ),
		__( 'Do not give legal advice', 'neccospeak' ),
		__( 'Do not present unverified information as fact', 'neccospeak' ),
		__( 'Do not mention competitors', 'neccospeak' ),
	);

	echo '<h2 class="title">' . esc_html__( 'Response rules', 'neccospeak' ) . '</h2>';
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row">
					<label for="neccospeak_prompt_do"><?php echo esc_html__( 'Always do (one per line)', 'neccospeak' ); ?></label>
				</th>
				<td>
					<textarea
						id="neccospeak_prompt_do"
						class="large-text"
						rows="3"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_do]"
					><?php echo esc_textarea( $opts['prompt_do'] ); ?></textarea>
					<p class="description"><?php echo esc_html__( 'e.g. "Prefer information from the official site when answering", "End each answer by asking whether there are any other questions"', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row">
					<label for="neccospeak_prompt_dont"><?php echo esc_html__( 'Never do (one per line)', 'neccospeak' ); ?></label>
				</th>
				<td>
					<textarea
						id="neccospeak_prompt_dont"
						class="large-text"
						rows="3"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_dont]"
					><?php echo esc_textarea( $opts['prompt_dont'] ); ?></textarea>
					<p class="description"><?php echo esc_html__( 'Commonly used no-go rules:', 'neccospeak' ); ?></p>
					<p>
						<?php foreach ( $ng_rule_presets as $ng_line ) : ?>
							<button type="button" class="button"
								onclick="var t=document.getElementById('neccospeak_prompt_dont'); t.value = t.value ? t.value + '\n' + <?php echo esc_attr( wp_json_encode( (string) $ng_line ) ); ?> : <?php echo esc_attr( wp_json_encode( (string) $ng_line ) ); ?>;">
								<?php echo esc_html( $ng_line ); ?>
							</button>
						<?php endforeach; ?>
					</p>
				</td>
			</tr>
			<tr>
				<th scope="row">
					<label for="neccospeak_prompt_unknown_behavior"><?php echo esc_html__( 'When the AI does not know the answer', 'neccospeak' ); ?></label>
				</th>
				<td>
					<select id="neccospeak_prompt_unknown_behavior" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_unknown_behavior]">
						<?php foreach ( $unknown_behaviors as $ub_val => $ub_label ) : ?>
							<option value="<?php echo esc_attr( $ub_val ); ?>" <?php selected( $opts['prompt_unknown_behavior'], $ub_val ); ?>><?php echo esc_html( $ub_label ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
			<tr>
				<th scope="row">
					<label for="neccospeak_prompt_escalation"><?php echo esc_html__( 'Hand off to a human agent when', 'neccospeak' ); ?></label>
				</th>
				<td>
					<textarea
						id="neccospeak_prompt_escalation"
						class="large-text"
						rows="2"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[prompt_escalation]"
					><?php echo esc_textarea( $opts['prompt_escalation'] ); ?></textarea>
					<p class="description"><?php echo esc_html__( 'Add from common conditions:', 'neccospeak' ); ?></p>
					<p>
						<?php foreach ( $escalation_presets as $esc_line ) : ?>
							<button type="button" class="button"
								onclick="var t=document.getElementById('neccospeak_prompt_escalation'); t.value = t.value ? t.value + '\n' + <?php echo esc_attr( wp_json_encode( (string) $esc_line ) ); ?> : <?php echo esc_attr( wp_json_encode( (string) $esc_line ) ); ?>;">
								<?php echo esc_html( $esc_line ); ?>
							</button>
						<?php endforeach; ?>
					</p>
					<p class="description"><?php echo esc_html__( 'Add any other conditions (free text) directly in the text box above.', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>
	<p class="description">
		<?php echo esc_html__( 'These rules are instructions to the AI model, not a hard guarantee. The model may ignore, misread, or only partially follow them. Do not rely on them for critical legal, medical, financial, or safety decisions, and always provide an escalation path to a human agent.', 'neccospeak' ); ?>
	</p>
	<?php
	if ( function_exists( 'neccospeak_admin_tab_slot' ) ) {
		// Fired here (before the possible early return just below) so it
		// always runs regardless of whether includes/prompt.php has landed
		// (design phase4 §8.2: "neccospeak_render_tab_prompt() の最後").
		neccospeak_admin_tab_slot( 'prompt_bottom', $opts );
	}

	// ---- 7. Preview (G18) ---------------------------------------------------
	if ( ! function_exists( 'neccospeak_compose_system_prompt_ex' ) ) {
		return;
	}

	$text_result = neccospeak_compose_system_prompt_ex( $opts, 'text' );
	$rt_result   = neccospeak_compose_system_prompt_ex( $opts, 'realtime' );

	echo '<h2>' . esc_html__( 'Prompt preview', 'neccospeak' ) . '</h2>';
	// Phase 5 (design phase5 §5.5(f)): under the G2 draft workflow, $opts here
	// is the DRAFT (neccospeak_render_tab_prompt() receives $editing), so the
	// original "as it will be sent to OpenAI" copy would be false -- what is
	// actually sent right now is still the published prompt. The original
	// string is kept verbatim for the (default, W=false) case, where it
	// remains accurate.
	if ( function_exists( 'neccospeak_draft_workflow_active' ) && neccospeak_draft_workflow_active() ) {
		echo '<p class="description">' . esc_html__( 'This previews the prompt you are currently editing (the draft). It may differ from the published prompt actually being sent to OpenAI.', 'neccospeak' ) . '</p>';
	} else {
		echo '<p class="description">' . esc_html__( 'This previews the saved prompt as it will actually be sent to OpenAI (it is not a live preview of your unsaved edits above).', 'neccospeak' ) . '</p>';
	}

	echo '<p>';
	echo '<label style="margin-right:16px;"><input type="radio" name="neccospeak-preview-mode" value="text" checked="checked" /> ' . esc_html__( 'Text chat', 'neccospeak' ) . '</label>';
	echo '<label><input type="radio" name="neccospeak-preview-mode" value="realtime" /> ' . esc_html__( 'Voice (realtime)', 'neccospeak' ) . '</label>';
	echo '</p>';

	neccospeak_render_prompt_preview_mode( 'text', $text_result, '', $opts );
	neccospeak_render_prompt_preview_mode(
		'realtime',
		$rt_result,
		__( 'Realtime voice mode instructions are limited to about 8,000 bytes (roughly 2,600 Japanese characters).', 'neccospeak' ),
		$opts
	);
}

/**
 * Render one mode's ('text' | 'realtime') preview panel: byte-usage
 * <progress> gauge, truncation notice, the composed text itself, and the
 * per-section breakdown table. Both panels are always present in the DOM
 * (JS-disabled degrade, design rev.2 §11) — assets/admin.js toggles the
 * `hidden` attribute based on the radio selection.
 *
 * @param string $mode        'text' | 'realtime'.
 * @param array  $result      neccospeak_compose_system_prompt_ex() return value.
 * @param string $always_note Extra note always shown for this mode (may be '').
 * @param array  $opts        neccospeak_get_options() — used for the G11 FAQ
 *                             "N of M fit" notice below (design rev.2 §12.5).
 *                             Optional/back-compat: defaults to array() so any
 *                             other caller of this function keeps working.
 * @return void
 */
function neccospeak_render_prompt_preview_mode( $mode, $result, $always_note, $opts = array() ) {
	$bytes    = (int) $result['bytes'];
	$budget   = max( 1, (int) $result['budget'] );
	$pct      = (int) round( min( 100, ( $bytes / $budget ) * 100 ) );
	$level    = ( $bytes >= $budget ) ? 'error' : ( $pct >= 80 ? 'warn' : 'ok' );
	$jp_chars = (int) round( $bytes / 3 );

	echo '<div class="neccospeak-preview-panel" data-mode="' . esc_attr( $mode ) . '"' . ( 'text' === $mode ? '' : ' hidden' ) . '>';

	echo '<progress class="neccospeak-prompt-progress" data-level="' . esc_attr( $level ) . '" max="' . esc_attr( $budget ) . '" value="' . esc_attr( $bytes ) . '"></progress>';

	echo '<p>' . esc_html(
		sprintf(
			/* translators: 1: bytes used, 2: byte budget, 3: percentage used, 4: approximate Japanese character count. */
			__( '%1$s / %2$s bytes (%3$s%%) — about %4$s Japanese characters', 'neccospeak' ),
			number_format_i18n( $bytes ),
			number_format_i18n( $budget ),
			number_format_i18n( $pct ),
			number_format_i18n( $jp_chars )
		)
	) . '</p>';

	if ( ! empty( $result['truncated'] ) ) {
		echo '<p style="color:#b32d2e;font-weight:600;">' . esc_html__( 'The prompt exceeded the limit and part of it was truncated. Please shorten it — the truncated part is not sent.', 'neccospeak' ) . '</p>';
	}

	// (1) Sections evicted entirely by the byte budget — list ALL of them
	// (design rev.2 §12.5: realtime can drop faq AND company AND goals).
	if ( ! empty( $result['dropped'] ) ) {
		$dropped_labels = array();
		foreach ( $result['sections'] as $section ) {
			if ( in_array( $section['key'], $result['dropped'], true ) ) {
				$dropped_labels[] = $section['label'];
			}
		}
		echo '<p style="color:#b45309;font-weight:600;">' . esc_html(
			sprintf(
				/* translators: %s: comma-separated list of prompt section names that were dropped. */
				__( 'These sections did not fit the byte limit and are not sent in this mode: %s', 'neccospeak' ),
				implode( ', ', $dropped_labels )
			)
		) . '</p>';
	}

	// (2) FAQ entries trimmed inside a section that WAS included (design rev.2
	// §5.4/§12.5). This never shows up in $result['dropped'], so it needs its
	// own line — guarded because neccospeak_prompt_faq_fit() is impl-core
	// owned (includes/prompt.php). Skipped entirely when the faq SECTION
	// itself is in $result['dropped']: that section sends zero FAQ entries,
	// so an "N of M fit" notice here would contradict the "not sent" notice
	// above and mislead the admin into thinking N entries still went out.
	$faq_section_dropped = ! empty( $result['dropped'] ) && in_array( 'faq', (array) $result['dropped'], true );
	if ( ! $faq_section_dropped && function_exists( 'neccospeak_prompt_faq_fit' ) ) {
		$fit = neccospeak_prompt_faq_fit( $opts, $mode );
		if ( is_array( $fit ) && isset( $fit['total'], $fit['used'] ) && $fit['total'] > 0 && $fit['used'] < $fit['total'] ) {
			echo '<p style="color:#b45309;">' . esc_html(
				sprintf(
					/* translators: 1: number of FAQ entries included, 2: total number of FAQ entries saved. */
					__( 'Only %1$s of the %2$s saved FAQ entries fit this mode. The rest are not sent.', 'neccospeak' ),
					number_format_i18n( $fit['used'] ),
					number_format_i18n( $fit['total'] )
				)
			) . '</p>';
		}
	}

	if ( '' !== $always_note ) {
		echo '<p class="description">' . esc_html( $always_note ) . '</p>';
	}

	echo '<pre class="neccospeak-prompt-pre">' . esc_html( $result['text'] ) . '</pre>';

	if ( ! empty( $result['sections'] ) ) {
		echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
		echo '<th>' . esc_html__( 'Section', 'neccospeak' ) . '</th>';
		echo '<th>' . esc_html__( 'Bytes', 'neccospeak' ) . '</th>';
		echo '<th>' . esc_html__( 'Sent', 'neccospeak' ) . '</th>';
		echo '</tr></thead><tbody>';
		foreach ( $result['sections'] as $section ) {
			echo '<tr>';
			echo '<td>' . esc_html( $section['label'] ) . '</td>';
			echo '<td>' . esc_html( number_format_i18n( $section['bytes'] ) ) . '</td>';
			echo '<td>' . ( $section['included'] ? esc_html__( 'Yes', 'neccospeak' ) : esc_html__( 'No', 'neccospeak' ) ) . '</td>';
			echo '</tr>';
		}
		echo '</tbody></table>';
	}

	echo '</div>';
}
