<?php
/**
 * NeccoSpeak agent-ui — Widget Settings tab.
 *
 * Structural equivalent of ai-agent-embed v1.5.1.1's
 * ai_agent_embed_render_tab_widget() (includes/admin-agent-settings.php),
 * i.e. ai_agent_embed_render_design_wrapper( 'all' ): theme colors/radius/
 * fonts/custom CSS, welcome/labels, launcher design.
 *
 * Screenshot-confirmed diff vs production (2026-08), applied directly in
 * this file rather than by re-cutting neccospeak_render_tab_appearance()'s
 * (includes/admin-tabs.php) HTML for the affected pieces:
 *
 *   1. テーマ設定 reordered to production's ai_agent_embed_render_section_
 *      design_appearance() (admin-agent-settings.php ~2219-2312): カラー設定,
 *      each row in production's 色見本+HEX style and production's row order/
 *      labels, bound to NeccoSpeak's existing theme_* keys. Production rows
 *      NeccoSpeak has no key for (ベーステーマ select +「プリセットを適用」/
 *      ユーザー吹き出しグラデーション/システム背景色/システムテキスト色/
 *      フッター背景色) are not rendered at all — settings the user cannot
 *      actually change are omitted rather than shown as dead controls.
 *      theme_bubble_bot_border has no production row (NeccoSpeak-only
 *      extra) — kept, placed next to its nearest neighbour.
 *   2. 「ドラフトに保存」button in the section header (production's blue
 *      primary button in .ai-agent-section-actions), bound to this tab's
 *      existing <form>/options.php save path — no new save codepath.
 *   3. ウィジェットタイトル(widget_title)・ヘッダーロゴ(header_logo_id,
 *      NeccoSpeak-only — production's design tab has no logo control, so
 *      it stays right beside ウィジェットタイトル rather than at a
 *      production position that doesn't exist)・初期メッセージ
 *      (welcome_message) ・プレビュー枠 (the G6 live preview panel hooked
 *      onto appearance_top, includes/admin-preview.php) are repositioned
 *      to match production: welcome_message under ウィジェット動作設定
 *      (production's ai_agent_embed_render_section_design_flow() — same
 *      row, same description text, as「初期メッセージ」), widget_title/
 *      header_logo/brand_color under ウィジェット外観設定 (production's
 *      …design_labels(), which opens with プライマリカラー then ヘッダー
 *      タイトル), and the live preview panel moved from the very top of the
 *      tab down to just after カスタムCSS — production's design tab has no
 *      inline preview at all (it was moved to a dedicated プレビュー tab;
 *      see admin-agent-settings.php line ~2310), and NeccoSpeak already has
 *      its own dedicated Preview tab (agent-ui-tabs/tab-preview.php), so
 *      the closest faithful position for the *inline* G6 panel this tab
 *      still carries is where production last shows anything preview-
 *      related in this tab: right after the custom-CSS block, before
 *      ウィジェット動作設定.
 *
 * All of the above keep the exact ids/names neccospeak_render_color_field()/
 * neccospeak_render_media_field() and the legacy renderer already used
 * (NECCOSPEAK_OPTION[key], id="neccospeak_{key}") — assets/admin-preview.js
 * resolves live-preview fields purely by that name attribute
 * (`[name="…[key]"]`), and duplicating a name is read by
 * `document.querySelector`, i.e. only the *first* matching element in the
 * DOM wins. Two spots below need care because of that:
 *   - The ヘッダーグラデーション「開始色」swatch is a read-only *display*
 *     mirror of ヘッダー背景色 (theme_header_bg), not a second submittable
 *     field with the same name — that would race with the real field for
 *     which one wins on submit and for what admin-preview.js reads live.
 *     It updates live off the real field's `input` event.
 *   - The「有効」checkbox itself doesn't carry NECCOSPEAK_OPTION[…] — it
 *     drives a same-value hidden <select name="…[theme_header_style]">
 *     (kept, just visually hidden, since admin-preview.js's readField()
 *     expects that key's element to expose a 'solid'/'gradient' *string*,
 *     which a bare checkbox's value can't give it) and re-dispatches a
 *     bubbling `change` on it so the live preview updates immediately
 *     instead of lagging one interaction behind (document-level listeners
 *     there are capture-phase, so they run before this field's own inline
 *     handler on the *same* event).
 *
 * Everything else in this tab (角丸設定／フォント設定／カスタムCSS本体／
 * ウィジェット動作設定のメッセージ表示形式・会話の継続・段階的表示／ランチ
 * ャーデザイン設定／モバイル設定／その他の外観設定) wasn't flagged as
 * differing from production, so it keeps re-using
 * neccospeak_render_tab_appearance()'s (includes/admin-tabs.php) rendered
 * HTML verbatim, cut at that renderer's existing heading boundaries — same
 * bytes, same escaping, same neccospeak_sanitize_options() paths, just
 * re-sequenced/re-labelled. Fails closed: if a future edit to that renderer
 * (or its neccospeak_render_appearance_theme_sections() /
 * neccospeak_render_appearance_launcher_sections() helpers) renames,
 * removes, or reorders one of the headings this depends on, the split below
 * bails out and prints the legacy output unmodified rather than risk
 * mangled markup (in which case the production-matching pieces above are
 * skipped too, since they're spliced into the same reassembly).
 *
 * welcome_message also still renders in includes/agent-ui-tabs/tab-flow.php
 * (「ウィジェット動作設定」→「初期メッセージ」there too) — that predates
 * this file's change and is out of this file's scope (widget only); the
 * same option key legitimately appearing under more than one NeccoSpeak tab
 * mirrors production keys that appear in more than one of *its* tabs too
 * (see agent-ui-tabs/tab-model.php's docblock for the same call).
 *
 * 2026-08 full top-to-bottom parity pass against production's design tab
 * (ai_agent_embed_render_section_design_appearance()/_flow()/_labels(),
 * admin-agent-settings.php ~2219-2525), on top of the screenshot-confirmed
 * items above:
 *   - ROOT CAUSE of "color rows still show the old swatch + 色を選択 button"
 *     (screenshot item 1): every neccospeak_render_color_field() call
 *     (includes/admin-tabs.php) marks its text input data-neccospeak-color.
 *     assets/admin.js's initColorPickers() — enqueued on this exact same
 *     admin page regardless of the agent-ui menu-callback swap, because
 *     neccospeak_is_admin_page() keys off $hook_suffix only, not which
 *     render callback is bound to it — upgrades every such field into the
 *     wp-color-picker (Iris) widget page-wide. That is production's actual
 *     absence of any such control turned into a NeccoSpeak-only regression.
 *     Since that helper (and admin.js) are outside this file's edit scope,
 *     neccospeak_agent_ui_widget_color_field() below reproduces the same
 *     native <input type="color"> + hex <input type="text"> pair inline,
 *     without that attribute, so this tab's fields are immune to the
 *     page-wide hijack instead of merely looking right until admin.js runs.
 *   - カラー設定: added 背景透かし画像/透かし不透明度 (production
 *     ~2265-2278) bound to theme_watermark_id/theme_watermark_opacity,
 *     which already existed — bound and working — a few hundred pixels
 *     lower on this same page, inside the legacy 「テーマ — 背景とカスタム
 *     CSS」 splice, just not where production puts them (inside カラー設定,
 *     right after フッター背景色) and not in production's URL+picker-button
 *     style. Re-rendering them here without also removing the old pair
 *     further down would leave two <input name="…[theme_watermark_id]">
 *     elements in one <form> — only the last in DOM order actually saves —
 *     so neccospeak_agent_ui_render_widget_sections() now cuts those two
 *     legacy rows out of the CSS segment first (same landmark-string,
 *     fail-closed technique already used for the フォント設定 split below:
 *     if the landmarks move, the cut is skipped and the legacy pair stays
 *     rather than risking a mangled table). theme_bubble_bot_border
 *     (NeccoSpeak-only) moved from "next to ボット吹き出しテキスト" to the
 *     very end of カラー設定 (after 透かし不透明度) per the "NS-only items
 *     trail behind the full production group" convention.
 *   - 角丸設定: production also has カード/入力欄 rows this tab had no
 *     grey stand-in for at all (NeccoSpeak has no matching key for either);
 *     inserted at their production position (between 吹き出し/ボタン, and
 *     after ボタン) via the same cut-and-splice technique. The 3 existing
 *     rows' labels (パネルの角丸/吹き出しの角丸/ボタンの角丸) are retargeted
 *     to production's shorter wording (ウィジェット/吹き出し/ボタン) — this
 *     table's own h4 already says 角丸設定, so 「◯◯の角丸」 was redundant.
 *   - フォント設定: production also has 小文字サイズ/ヘッダー文字サイズ,
 *     no NeccoSpeak key for either — appended as grey rows. The existing
 *     フォント row is relabeled フォントファミリー (matching production's
 *     label) but keeps NeccoSpeak's own constrained <select> of preset
 *     families rather than production's free-text CSS font-family string —
 *     theme_font_family's sanitizer (includes/settings.php) only accepts
 *     that fixed enum, so a free-text input would either silently fail to
 *     save or require a sanitizer change outside this file's scope.
 *   - カスタムCSS row's own <label> text (inside the legacy splice) is
 *     retargeted from 「カスタムCSS」 to production's 「追加CSS」 — the h4
 *     heading above it already correctly says 「カスタムCSS」 (unchanged).
 *   - ウィジェット動作設定: added a grey クイックアクション row (production
 *     ~2327-2337) — distinct from NeccoSpeak's own cta_list (the 'actions'
 *     tab bucket in neccospeak_tab_fields(), a different top-level tab in
 *     this UI), which is NeccoSpeak's equivalent of production's *separate*
 *     top-level 「CTA設定」 tab, not of this small welcome-screen shortcut
 *     list. No NeccoSpeak key covers this feature.
 *   - その他の外観設定: added a grey アバター絵文字 row (production
 *     ~2498-2510) as its own small trailing table rather than splicing it
 *     into the legacy ホーム画面 block — home_avatar_id/home_input_placeholder
 *     already cover production's アバター画像/入力プレースホルダー
 *     functionally (bound, working) inside that same legacy block, just
 *     organized under NeccoSpeak's own 「ホーム画面」 heading instead of
 *     「その他の外観設定」; left in place rather than risking that large
 *     splice for a heading-only relocation.
 *   - ランチャーデザイン設定/モバイル設定 were left as NeccoSpeak's existing
 *     enum-based controls (launcher_icon/size/position/shape/animation from
 *     a fixed list, vs. production's free-form hex/px/SVG-URL/rich-tooltip
 *     fields) — a pre-existing, deliberate design difference from before
 *     this pass, not something the screenshots flagged. Bringing every
 *     production launcher field (hover/shadow color, custom SVG/image icon,
 *     rich tooltip title+colors+item list, border, hover scale, z-index,
 *     first-time/returning-visitor icons, auto-connect, transparent
 *     background, label-hide, button width — none of which NeccoSpeak has a
 *     key for) up to full parity is a materially larger rebuild than this
 *     pass's other findings and was left for a dedicated follow-up instead
 *     of being rushed here.
 *
 * 2026-08 screenshot-confirmed 4-item follow-up (on top of both passes
 * above):
 *   1. 透かし不透明度: was a plain <input type="number" min="0" max="30">
 *      showing NeccoSpeak's raw whole-number-percent unit; now a
 *      <input type="range" min="0.01" max="0.3" step="0.01"> plus a live
 *      value readout and the "0.01（ほぼ見えない）〜 0.3（しっかり見える）"
 *      description, transcribed from production (admin-agent-settings.php
 *      ~2274-2278). The range control is display-only — it drives a hidden
 *      <input name="…[theme_watermark_opacity]"> (the one element carrying
 *      that name in the <form>) via inline oninput JS that converts
 *      decimal↔whole-number-percent, so neccospeak_sanitize_options() (int_
 *      clamp 0-30) and assets/admin-preview.js (which also expects that same
 *      whole-number-percent on that named field) both keep working
 *      unmodified.
 *   2. 背景透かし画像: the raw attachment-ID <input type="number"> that used
 *      to sit inline next to the read-only URL mirror is now type="hidden"
 *      (same id/name/data-neccospeak-media-* attributes, so assets/admin.js's
 *      initMediaPickers() — which only requires the [data-neccospeak-media-
 *      input] attribute, not a particular <input type=""> — keeps working
 *      unmodified); only the URL mirror, メディアから選択/削除 buttons, and
 *      conditional preview <img> remain visible, matching production
 *      (admin-agent-settings.php ~2265-2273).
 *   3. フォントファミリー: production is a free-text CSS font-family string
 *      (admin-agent-settings.php ~2292); NeccoSpeak's theme_font_family
 *      sanitizer (includes/settings.php, 'enum' type, fixed 'allowed' list)
 *      has no filter/extension hook for that value the way e.g.
 *      neccospeak_tab_fields() does via apply_filters( 'neccospeak_tab_
 *      fields', … ) — confirmed by reading every apply_filters() call in
 *      that file, none of which touch per-field sanitization. So this stays
 *      NeccoSpeak's existing constrained <select> (case (b) of the two
 *      options considered) rather than becoming free text that would either
 *      silently fail to save or require a settings.php sanitizer change,
 *      which is out of this file's edit scope. Only its visual footprint was
 *      widened to production's width and its <p class="description"> text
 *      was adjusted toward production's terser wording (still noting it's a
 *      preset list, since — unlike production — it actually still is one).
 *   4. AI吹き出し枠線色 (theme_bubble_bot_border, NeccoSpeak-only — no
 *      production row): previously the last row inside カラー設定, between
 *      透かし不透明度 and 角丸設定; moved to a new dedicated 「NeccoSpeak独自
 *      の設定」 section at the very end of the tab (after その他の外観設定,
 *      before the appearance_bottom hook — see neccospeak_agent_ui_render_
 *      widget_ns_only_section() and its call site) so every NeccoSpeak-only
 *      addition beyond production's own field set is grouped under one
 *      heading instead of interleaved into a production-matching table. Its
 *      existing description text is unchanged.
 *
 * 2026-08 5-item round (screenshot-confirmed items 5-9, on top of every pass
 * above) — a much larger reorg than the earlier rounds, so
 * neccospeak_agent_ui_cut_tr() (a small generic helper near the top of this
 * file) generalizes the $watermark_cut technique: cut one `<tr>` (or a
 * functionally-paired two-row unit) out of a legacy segment by unique
 * landmark string, fail-closed to '' on any miss:
 *   5. 埋め込みプレビュー(ボタン群+iframe枠) removed — production's design tab
 *      has no inline preview at all (moved to its own プレビュー tab); the
 *      panel is replaced with production's own single link line, pointed at
 *      NeccoSpeak's existing プレビュー top-tab.
 *   6. ウィジェット動作設定's 9 NeccoSpeak-only rows (Markdown表示/ローディング
 *      表示/会話を復元/復元有効期間/保存する最大メッセージ数・サイズ/表示
 *      モード/表示速度/スキップボタン — $seg['msgfmt']/$seg['continuity']/
 *      $progressive_table) moved verbatim to NeccoSpeak独自の設定; that
 *      section now only carries 初期メッセージ+クイックアクション, matching
 *      production.
 *   7. ウィジェット外観設定 reordered to production's プライマリカラー→ヘッダー
 *      タイトル→ウェルカムアイコン→ウェルカムアイコン画像→ウェルカムタイトル→
 *      ウェルカムサブタイトル→できること一覧: (a) プライマリカラー is now a
 *      色見本+HEX pair; (b) ウィジェットタイトル relabeled ヘッダータイトル;
 *      (c) the 5 ウェルカム* rows are cut out of the legacy ホーム画面 segment
 *      and re-rendered here (home_icon's enum <select> kept — same no-
 *      extension-hook reasoning as theme_font_family; できること一覧 gets a
 *      JS add/remove text-row overlay on home_capabilities, matching
 *      production's plain-text list editor — see
 *      neccospeak_agent_ui_render_widget_capabilities_row()); (d) ヘッダー
 *      ロゴ + 表示ルール/表示するURL/除外するURL moved to NeccoSpeak独自の設定.
 *   8. ランチャーデザイン設定 rebuilt to production's full 26-row order
 *      (admin-agent-settings.php ~2379-2467): 4 rows stay bound (ランチャー
 *      ラベル/ランチャーツールチップ/位置×2/アイコン種別), the other 22
 *      production rows render grey, and NeccoSpeak's own enum-only controls
 *      that don't map onto a single production row (size S/M/L, position
 *      select, shape, badge, attention-animation) relocate to NeccoSpeak
 *      独自の設定 instead — see neccospeak_agent_ui_render_widget_launcher_
 *      section()'s docblock for the full per-row bind table.
 *   9. モバイル設定 rebuilt around production's 「モバイル詳細」 grouping (3
 *      grey px rows + NeccoSpeak's own bound enum/offset rows, with no
 *      "モバイル個別設定" master checkbox — NeccoSpeak's per-field 'inherit'
 *      enums don't collapse into one boolean without changing behavior, the
 *      explicitly-allowed fallback here); 「モバイルで非表示」 moved to
 *      NeccoSpeak独自の設定; a 「ランチャープレビュー（概略）」 static (non-
 *      live) approximation added, per this round's "static is fine if live
 *      JS is heavy" allowance. その他の外観設定 reduced to production's exact
 *      3 rows (アバター絵文字[grey]/アバター画像/入力プレースホルダー — the
 *      latter two cut out of the legacy ホーム画面 segment); every other
 *      leftover NeccoSpeak-only ホーム画面/AI免責事項/フォールバック row moved
 *      to NeccoSpeak独自の設定 in full (see
 *      neccospeak_agent_ui_ns_only_misc_block()'s docblock for the one
 *      inference this made — home_capability_tags wasn't explicitly listed
 *      either way).
 *
 * 2026-08 screenshot-confirmed 3-item follow-up (dev.8 device screenshot, on
 * top of every round above), all inside
 * neccospeak_agent_ui_render_widget_launcher_section() — see that function's
 * own docblock for the per-row bind detail:
 *   1. ランチャーラベル: was still NeccoSpeak's legacy チェックボックス+ラベル
 *      のテキスト+「オフにすると…」description row; rebuilt to production's
 *      single text input + 「ウィジェット起動ボタンに表示するテキスト（例:
 *      試す、AIに相談）最大50文字」 description (admin-agent-settings.php
 *      ~2382). launcher_label_text binds the visible input directly;
 *      launcher_label_enabled (no visible control of its own now) is kept in
 *      a hidden input synced by inline oninput JS off launcher_label_text's
 *      emptiness ("非空→1 / 空→0"), reproducing the pre-existing "空欄=
 *      ラベルなし" semantics non-destructively (settings.php's sanitizer for
 *      both keys — 'text_optional'/'bool' — is unchanged).
 *   2. ランチャーツールチップ: rebuilt to production's 「ツールチップを表示
 *      する」チェック + テキスト入力 + 「ウィジェット起動ボタン横の吹き出し
 *      に表示するテキスト」 description (admin-agent-settings.php ~2383).
 *      NeccoSpeak has only the ツールチップ文言 key (launcher_tooltip, no
 *      separate enable/disable key), so the checkbox is UI sugar with no
 *      option key of its own: checked on load when launcher_tooltip is
 *      non-empty, and unchecking disables + clears the text input inline —
 *      a disabled input isn't submitted, so unchecked naturally saves as ''
 *      on submit, matching the pre-existing "空=非表示" semantics.
 *   3. ツールチップ拡張 (タイトル/背景色/文字色/項目リスト, admin-agent-
 *      settings.php ~2383-2395) and ボタンサイズ (~2397): NeccoSpeak has no
 *      key for either, so neither is rendered here at all.
 *
 * 2026-08 3-item follow-up (screenshot-confirmed diff vs. production's
 * admin-agent-settings.php, on top of every round above):
 *   1. モバイル設定: added production's 「モバイル個別設定」 master checkbox
 *      row (~2472: 「モバイルでは別サイズ・位置を使用する」) at the top of
 *      the section, and removed the long inline note that used to explain
 *      its absence. It is UI sugar only (no new option key) — see
 *      neccospeak_agent_ui_render_widget_mobile_section()'s own docblock for
 *      the full on/off behavior and why an unsubmitted disabled <select>
 *      still saves correctly. Row order inside モバイル詳細 now matches
 *      production's サイズ→幅→アイコンサイズ→位置（右）→位置（下）, with
 *      NeccoSpeak's own $mobile_size_row/$mobile_position_row enum selects
 *      trailing after (same "NS-only equivalents trail the full production
 *      group" convention as elsewhere in this file).
 *   2. その他の外観設定: アバター絵文字 rebuilt from a plain 「アイコンを
 *      選択」 button to production's icon-button + icon-name-text shape
 *      (~2498-2510) with production's own description text; アバター画像
 *      rebuilt from neccospeak_render_media_field()'s raw-ID-input shape to
 *      the same hidden-ID + read-only URL mirror + メディアから選択/削除
 *      pattern as 背景透かし画像, with production's description text; 入力
 *      プレースホルダー's description retargeted to production's 「チャット
 *      入力欄に表示するヒントテキスト」 (NeccoSpeak's own applicability note
 *      kept as a short trailing parenthetical). See
 *      neccospeak_agent_ui_render_widget_misc_extra_fields()'s own docblock.
 *   3. 「NeccoSpeak独自の設定」 section (heading + description + every field
 *      inside, including this round's own theme_bubble_bot_border row) is now
 *      wrapped in a `display:none` <div data-neccospeak-extras-hidden="1">
 *      instead of being visibly shown — hidden from the UI, but left in the
 *      DOM so every field's name="…[key]" attribute keeps submitting on save
 *      (a hidden checkbox still POSTs its checked state; only actually
 *      removing the markup would risk that). See
 *      neccospeak_agent_ui_render_widget_ns_only_section()'s own docblock.
 *
 * @package NeccoSpeak
 */

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

/**
 * Cuts one complete `<tr>...</tr>` out of a legacy-renderer HTML string and
 * returns it, removing it from $html in place. Same fail-closed technique as
 * the pre-existing 透かし画像/透かし不透明度 cut (see
 * neccospeak_agent_ui_render_widget_sections()'s $watermark_cut): on any
 * landmark miss this returns '' and leaves $html completely untouched, so a
 * future rename in includes/admin-tabs.php's legacy renderer degrades to
 * "this row stays wherever the legacy renderer already puts it" rather than
 * risking mangled markup.
 *
 * $end_landmark lets one call lift two (or more) physically adjacent rows as
 * a single unit when they're a functional pair — e.g. an enum <select> plus
 * its conditional "custom value" row right below it — by searching for the
 * closing `</tr>` only after $end_landmark's position rather than
 * $start_landmark's.
 *
 * @param string      $html          Legacy segment HTML, modified in place.
 * @param string      $start_landmark Substring unique to the row's opening (or
 *                                     the first row, for a multi-row lift).
 * @param string|null $end_landmark   Substring unique to the row whose </tr>
 *                                     should close the cut (defaults to
 *                                     $start_landmark, i.e. a single row).
 * @return string The cut `<tr>...</tr>` (or concatenated rows), or '' if any
 *                 landmark wasn't found.
 */
function neccospeak_agent_ui_cut_tr( &$html, $start_landmark, $end_landmark = null ) {
	if ( null === $end_landmark ) {
		$end_landmark = $start_landmark;
	}
	$start_at = strpos( $html, $start_landmark );
	if ( false === $start_at ) {
		return '';
	}
	$before      = substr( $html, 0, $start_at );
	$tr_open_at  = strrpos( $before, '<tr>' );
	$tr_open_at2 = strrpos( $before, '<tr ' );
	if ( false === $tr_open_at || ( false !== $tr_open_at2 && $tr_open_at2 > $tr_open_at ) ) {
		$tr_open_at = $tr_open_at2;
	}
	if ( false === $tr_open_at ) {
		return '';
	}
	$end_at = strpos( $html, $end_landmark, $start_at );
	if ( false === $end_at ) {
		return '';
	}
	$tr_close_at = strpos( $html, '</tr>', $end_at );
	if ( false === $tr_close_at ) {
		return '';
	}
	$cut_end   = $tr_close_at + strlen( '</tr>' );
	$extracted = substr( $html, $tr_open_at, $cut_end - $tr_open_at );
	$html      = substr( $html, 0, $tr_open_at ) . substr( $html, $cut_end );
	return $extracted;
}

/**
 * @param array $opts Editing-surface options.
 * @return void
 */
function neccospeak_agent_ui_tab_widget( $opts ) {
	$opts = is_array( $opts ) ? $opts : array();
	?>
	<div class="ai-agent-section" id="ai-agent-design">
		<?php neccospeak_agent_ui_form_open( 'appearance' ); ?>
		<div class="ai-agent-section-header">
			<h2><?php esc_html_e( 'Design & UI settings', 'neccospeak' ); ?></h2>
			<div class="ai-agent-section-actions">
				<button type="submit" class="button button-primary"><?php esc_html_e( 'Save as draft', 'neccospeak' ); ?></button>
			</div>
		</div>
		<p class="description"><?php esc_html_e( 'Customize the widget\'s appearance and behavior.', 'neccospeak' ); ?></p>

		<?php
		if ( function_exists( 'neccospeak_render_tab_appearance' ) ) {
			neccospeak_agent_ui_render_widget_sections( $opts );
		} else {
			echo '<p class="description">' . esc_html__( 'Could not load widget settings.', 'neccospeak' ) . '</p>';
		}
		?>
		<?php neccospeak_agent_ui_form_close(); ?>
	</div>
	<?php
}

/**
 * Renders the production-matching pieces (theme/color section, the
 * repositioned widget-title/logo/welcome-message/preview fields) directly,
 * and splices in neccospeak_render_tab_appearance()'s (includes/admin-tabs.php)
 * unmodified output for everything else, cut at that renderer's existing
 * heading boundaries.
 *
 * @param array $opts neccospeak_get_options().
 * @return void
 */
function neccospeak_agent_ui_render_widget_sections( $opts ) {
	ob_start();
	neccospeak_render_tab_appearance( $opts );
	$html = ob_get_clean();

	// Exact heading markup the legacy renderer emits today, in its current
	// on-screen order. Built with __()/_x() (same textdomain + args as
	// includes/admin-tabs.php) rather than hard-coded Japanese/English text,
	// so this keeps matching even if a translation is loaded later.
	$markers = array(
		'colors'      => '<h2 class="title">' . __( 'Theme — Color settings', 'neccospeak' ) . '</h2>',
		'radius_font' => '<h2 class="title">' . __( 'Theme — Corner radius (px)', 'neccospeak' ) . '</h2>',
		'css'         => '<h2 class="title">' . __( 'Theme — Background and custom CSS', 'neccospeak' ) . '</h2>',
		'launcher'    => '<h2 class="title">' . __( 'Launcher', 'neccospeak' ) . '</h2>',
		'launcher_m'  => '<h2 class="title">' . __( 'Launcher — Mobile', 'neccospeak' ) . '</h2>',
		'msgfmt'      => '<h2 class="title">' . __( 'Message display format', 'neccospeak' ) . '</h2>',
		'continuity'  => '<h2 class="title">' . __( 'Conversation continuity', 'neccospeak' ) . '</h2>',
		'disclaimer'  => '<h2 class="title">' . __( 'AI disclaimer', 'neccospeak' ) . '</h2>',
		'home'        => '<h2 class="title">' . __( 'Home screen', 'neccospeak' ) . '</h2>',
		'fallback'    => '<h2 class="title">' . _x( 'Fallback', 'settings section heading', 'neccospeak' ) . '</h2>',
		'progressive' => '<h2 class="title">' . __( 'Progressive replies', 'neccospeak' ) . '</h2>',
	);

	$pos      = array();
	$prev_pos = -1;
	$ok       = true;
	foreach ( $markers as $key => $marker ) {
		$found = strpos( $html, $marker );
		if ( false === $found || $found < $prev_pos ) {
			$ok = false;
			break;
		}
		$pos[ $key ] = $found;
		$prev_pos    = $found;
	}

	if ( ! $ok ) {
		echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- verbatim output of neccospeak_render_tab_appearance(), which already escapes every dynamic value it prints; a known heading moved/changed, so show the legacy output unmodified instead of risking mangled markup.
		return;
	}

	$after = function ( $key ) use ( $markers, $pos ) {
		return $pos[ $key ] + strlen( $markers[ $key ] );
	};

	$seg = array(
		'top'         => substr( $html, 0, $pos['colors'] ),
		'radius_font' => substr( $html, $after( 'radius_font' ), $pos['css'] - $after( 'radius_font' ) ),
		'css'         => substr( $html, $after( 'css' ), $pos['launcher'] - $after( 'css' ) ),
		'launcher'    => substr( $html, $after( 'launcher' ), $pos['launcher_m'] - $after( 'launcher' ) ),
		'launcher_m'  => substr( $html, $after( 'launcher_m' ), $pos['msgfmt'] - $after( 'launcher_m' ) ),
		'msgfmt'      => substr( $html, $after( 'msgfmt' ), $pos['continuity'] - $after( 'msgfmt' ) ),
		'continuity'  => substr( $html, $after( 'continuity' ), $pos['disclaimer'] - $after( 'continuity' ) ),
		'disclaimer'  => substr( $html, $after( 'disclaimer' ), $pos['home'] - $after( 'disclaimer' ) ),
		'home'        => substr( $html, $after( 'home' ), $pos['fallback'] - $after( 'home' ) ),
		'fallback'    => substr( $html, $after( 'fallback' ), $pos['progressive'] - $after( 'fallback' ) ),
		'progressive' => substr( $html, $after( 'progressive' ) ),
	);

	// `top` carries the appearance_top hook plus the legacy widget-title/
	// header-logo/welcome-message/brand-color/display-rule table. The whole
	// segment is discarded: the table is superseded by the production-position
	// fields rendered elsewhere below, and production's design tab has no
	// inline preview at all (screenshot-confirmed diff item 5 — it moved to
	// its own プレビュー tab, admin-agent-settings.php ~2309-2310: a single
	// 「プレビュータブでウィジェットの表示を確認できます。」 link), so the
	// embedded preview is replaced with that same link, pointed at
	// NeccoSpeak's own プレビュー top-tab — see its construction right before
	// this segment's old position is echoed, further down.
	//
	// 0.3.0-dev.42: the appearance_top hook now has NO listeners at all. It
	// used to fire includes/admin-preview.php's neccospeak_render_preview_panel()
	// (the G6 live preview panel), whose markup this segment already threw
	// away; that file was deleted with the rest of the legacy preview stack,
	// so the hook is a bare extension point again and this splice is
	// byte-for-byte unaffected.

	// `progressive` likewise carries the appearance_bottom hook (currently
	// unused, but a documented extension point — see
	// neccospeak_admin_tab_slot() in includes/admin-tabs.php) tacked on
	// after its own table. Keep that pinned last instead of wherever
	// ウィジェット動作設定 ends up.
	$progressive_table = $seg['progressive'];
	$hook_bottom        = '';
	$last_table_close    = strrpos( $seg['progressive'], '</table>' );
	if ( false !== $last_table_close ) {
		$cut               = $last_table_close + strlen( '</table>' );
		$progressive_table = substr( $seg['progressive'], 0, $cut );
		$hook_bottom       = substr( $seg['progressive'], $cut );
	}

	// The legacy renderer keeps 角丸設定 and フォント設定 fields in one
	// <table>; split it so フォント設定 can get its own heading, matching
	// production. If the split point can't be found, leave the whole table
	// under 角丸設定（px） rather than drop the フォント fields.
	$radius_html   = $seg['radius_font'];
	$font_html     = '';
	$font_label_at = strpos( $seg['radius_font'], '<label for="neccospeak_theme_font_family">' );
	if ( false !== $font_label_at ) {
		$font_tr_at = strrpos( substr( $seg['radius_font'], 0, $font_label_at ), '<tr>' );
		if ( false !== $font_tr_at ) {
			$radius_html = substr( $seg['radius_font'], 0, $font_tr_at ) . "\t\t</tbody>\n\t</table>\n";
			$font_html   = "<table class=\"form-table ai-agent-form-table\" role=\"presentation\">\n\t\t<tbody>\n" . substr( $seg['radius_font'], $font_tr_at );
		}
	}

	// 角丸設定: production also has カード/入力欄 rows (admin-agent-settings.php
	// ~2285/2287) NeccoSpeak has no key for at all — insert them as grey rows
	// at production's exact position (between 吹き出し/ボタン, and after
	// ボタン), and retarget the 3 existing rows' labels to production's
	// shorter wording (this table's h4 already says 角丸設定, so 「◯◯の
	// 角丸」 is redundant — production just says ウィジェット/吹き出し/ボタン).
	// Fails closed: if a landmark can't be found the corresponding insertion/
	// relabel is skipped rather than risking mangled markup.
	$radius_html = str_replace(
		array(
			'<label for="neccospeak_theme_panel_radius">' . __( 'Panel corner radius', 'neccospeak' ) . '</label>',
			'<label for="neccospeak_theme_bubble_radius">' . __( 'Bubble corner radius', 'neccospeak' ) . '</label>',
			'<label for="neccospeak_theme_button_radius">' . __( 'Button corner radius', 'neccospeak' ) . '</label>',
		),
		array(
			'<label for="neccospeak_theme_panel_radius">' . __( 'Widget', 'neccospeak' ) . '</label>',
			'<label for="neccospeak_theme_bubble_radius">' . __( 'Speech bubble', 'neccospeak' ) . '</label>',
			'<label for="neccospeak_theme_button_radius">' . __( 'Button', 'neccospeak' ) . '</label>',
		),
		$radius_html
	);

	// フォント設定: production also has 小文字サイズ/ヘッダー文字サイズ, no
	// NeccoSpeak key for either — appended as grey rows after 基本文字サイズ
	// (already last in this table, so "append before the closing tag" is
	// already production's exact position, no mid-table insertion needed).
	// フォント is relabeled フォントファミリー to match production's label;
	// the <select> itself stays NeccoSpeak's own constrained preset list
	// (theme_font_family's sanitizer — includes/settings.php, 'enum' type —
	// only accepts that fixed 'allowed' list, and unlike e.g.
	// neccospeak_tab_fields()/neccospeak_tabs() there is no apply_filters()
	// hook over that list to extend it) rather than switching to
	// production's free-text CSS font-family string (admin-agent-settings.php
	// ~2292); doing that would need a settings.php sanitizer change, which is
	// out of this file's scope. Only the <select>'s width and description
	// text are pulled toward production's look here.
	if ( '' !== $font_html ) {
		$font_html = str_replace(
			'<label for="neccospeak_theme_font_family">' . __( 'Font', 'neccospeak' ) . '</label>',
			'<label for="neccospeak_theme_font_family">' . __( 'Font family', 'neccospeak' ) . '</label>',
			$font_html
		);
		// Production's font-family field is a wide free-text <input
		// class="large-text">; widen NeccoSpeak's <select> to roughly match
		// that footprint (it stays a <select>, per the docblock above) and
		// swap its description toward production's terser "CSSフォント
		// ファミリー指定" wording, while still noting it's a preset list —
		// production's copy would otherwise misleadingly imply free CSS
		// input is accepted here.
		$font_html = str_replace(
			'<select id="neccospeak_theme_font_family" name="' . esc_attr( NECCOSPEAK_OPTION ) . '[theme_font_family]">',
			'<select id="neccospeak_theme_font_family" name="' . esc_attr( NECCOSPEAK_OPTION ) . '[theme_font_family]" class="large-text" style="width:100%;max-width:400px;">',
			$font_html
		);
		$font_html = str_replace(
			esc_html__( 'Only fonts already installed on the visitor\'s device are used (no web fonts are downloaded).', 'neccospeak' ),
			esc_html__( 'CSS font-family setting (choose from presets already installed on the visitor\'s device; no web fonts are downloaded).', 'neccospeak' ),
			$font_html
		);
	}

	// カスタムCSS segment: 背景の透かし画像/透かしの不透明度 rows here are
	// re-rendered production-style, in production's actual position, by
	// neccospeak_agent_ui_render_widget_color_rows() (right after フッター
	// 背景色) — but ONLY if the legacy pair below is actually cut out of this
	// segment, or theme_watermark_id/theme_watermark_opacity would each carry
	// two <input name="…"> in one <form> (only the last one in DOM order
	// actually saves on submit). $watermark_cut tracks whether that cut
	// succeeded; neccospeak_agent_ui_render_widget_color_rows() only renders
	// its own bound copy when it's true, so a future rename of one of the
	// landmarks below fails closed to "still just the one legacy control"
	// rather than ever risking two inputs for the same key. Also retarget the
	// legacy カスタムCSS row's own <label> text to production's 追加CSS — the
	// h4 heading above it already correctly says カスタムCSS (production's h4
	// for this exact block is also カスタムCSS; only the row label differs).
	$css_html              = $seg['css'];
	$watermark_cut         = false;
	$watermark_label_at    = strpos( $css_html, esc_html__( 'Background watermark image', 'neccospeak' ) );
	$custom_css_anchor_at = strpos( $css_html, '<label for="neccospeak_custom_css">' );
	if ( false !== $watermark_label_at && false !== $custom_css_anchor_at ) {
		$watermark_tr_at  = strrpos( substr( $css_html, 0, $watermark_label_at ), '<tr>' );
		$custom_css_tr_at = strrpos( substr( $css_html, 0, $custom_css_anchor_at ), '<tr>' );
		if ( false !== $watermark_tr_at && false !== $custom_css_tr_at && $custom_css_tr_at > $watermark_tr_at ) {
			$css_html      = substr( $css_html, 0, $watermark_tr_at ) . substr( $css_html, $custom_css_tr_at );
			$watermark_cut = true;
		}
	}
	$css_html = str_replace(
		'<label for="neccospeak_custom_css">' . __( 'Custom CSS', 'neccospeak' ) . '</label>',
		'<label for="neccospeak_custom_css">' . __( 'Additional CSS', 'neccospeak' ) . '</label>',
		$css_html
	);

	// 2026-08 5-item follow-up: cut every row that's either repositioned to a
	// production slot or relocated to NeccoSpeak独自の設定 out of its legacy
	// segment, via neccospeak_agent_ui_cut_tr() (same fail-closed technique as
	// $watermark_cut above — a missed landmark returns '' and leaves the
	// segment's copy of that row in place, untouched, rather than risking
	// mangled markup). See the per-row bind-policy notes on
	// neccospeak_agent_ui_render_widget_labels_fields()/_launcher_section()/
	// _mobile_section()/_misc_extra_fields() for what happens with each cut.
	$home_html                = $seg['home'];
	$welcome_icon_rows        = neccospeak_agent_ui_cut_tr( $home_html, '<label for="neccospeak_home_icon">', 'data-neccospeak-home-icon-custom-row' );
	$welcome_headline_row     = neccospeak_agent_ui_cut_tr( $home_html, '<label for="neccospeak_home_headline">' );
	$welcome_body_row         = neccospeak_agent_ui_cut_tr( $home_html, '<label for="neccospeak_home_body">' );
	$welcome_capabilities_row = neccospeak_agent_ui_cut_tr( $home_html, '<label for="neccospeak_home_capabilities">' );
	$avatar_row               = neccospeak_agent_ui_cut_tr( $home_html, '[home_avatar_id]' );
	$placeholder_row          = neccospeak_agent_ui_cut_tr( $home_html, '<label for="neccospeak_home_input_placeholder">' );

	// 2026-08 バグ修正: disclaimer_modal_enabled はマスタートグルのため、他のNS独自項目と
	// 一緒に非表示にすると新規インストールで「初回表示モーダル」を有効化する手段が管理画面
	// から完全に失われる（voice_enabledと同じ不具合）。「同意の記憶方法」行はこのモーダルに
	// のみ適用される（説明文が明記）ため一緒に切り出す。disclaimer_inline_enabled（常時表示の
	// 注記）は対象外のため $seg['disclaimer'] に残し、NeccoSpeak独自の設定側の扱いのまま。
	$disclaimer_modal_rows = neccospeak_agent_ui_cut_tr( $seg['disclaimer'], '[disclaimer_modal_enabled]', '[disclaimer_ack_scope]' );

	$launcher_html          = $seg['launcher'];
	$launcher_label_row     = neccospeak_agent_ui_cut_tr( $launcher_html, '<label for="neccospeak_launcher_label_text">' );
	$launcher_tooltip_row   = neccospeak_agent_ui_cut_tr( $launcher_html, 'id="neccospeak_launcher_tooltip"' );
	$launcher_offset_x_row  = neccospeak_agent_ui_cut_tr( $launcher_html, 'id="neccospeak_launcher_offset_x"' );
	$launcher_offset_y_row  = neccospeak_agent_ui_cut_tr( $launcher_html, 'id="neccospeak_launcher_offset_y"' );
	$launcher_icon_rows     = neccospeak_agent_ui_cut_tr( $launcher_html, 'id="neccospeak_launcher_icon"', 'data-neccospeak-launcher-icon-custom-row' );
	$launcher_size_row      = neccospeak_agent_ui_cut_tr( $launcher_html, 'id="neccospeak_launcher_size"' );
	$launcher_position_row  = neccospeak_agent_ui_cut_tr( $launcher_html, 'id="neccospeak_launcher_position"' );
	$launcher_shape_row     = neccospeak_agent_ui_cut_tr( $launcher_html, 'id="neccospeak_launcher_shape"' );
	$launcher_badge_row     = neccospeak_agent_ui_cut_tr( $launcher_html, '<label for="neccospeak_launcher_badge_text">' );
	$launcher_animation_row = neccospeak_agent_ui_cut_tr( $launcher_html, 'id="neccospeak_launcher_animation"' );

	$launcher_m_html              = $seg['launcher_m'];
	$launcher_mobile_hide_row     = neccospeak_agent_ui_cut_tr( $launcher_m_html, '[launcher_mobile_hide]' );
	$launcher_mobile_size_row     = neccospeak_agent_ui_cut_tr( $launcher_m_html, 'id="neccospeak_launcher_mobile_size"' );
	$launcher_mobile_position_row = neccospeak_agent_ui_cut_tr( $launcher_m_html, 'id="neccospeak_launcher_mobile_position"' );
	$launcher_mobile_offset_x_row = neccospeak_agent_ui_cut_tr( $launcher_m_html, 'id="neccospeak_launcher_mobile_offset_x"' );
	$launcher_mobile_offset_y_row = neccospeak_agent_ui_cut_tr( $launcher_m_html, 'id="neccospeak_launcher_mobile_offset_y"' );

	echo '<h3 class="title">' . esc_html__( 'Theme settings', 'neccospeak' ) . ' <span class="ai-agent-badge ai-agent-badge--draft">' . esc_html__( 'Draft editing', 'neccospeak' ) . '</span></h3>';
	echo '<p class="description">' . esc_html__( 'Customize colors and fonts based on a preset theme. Changes are saved to a draft.', 'neccospeak' ) . '</p>';
	echo '<h4 style="margin-top:24px;">' . esc_html__( 'Color settings', 'neccospeak' ) . '</h4>';
	neccospeak_agent_ui_render_widget_color_rows( $opts, $watermark_cut );
	echo '<h4 style="margin-top:24px;">' . esc_html__( 'Corner radius (px)', 'neccospeak' ) . '</h4>';
	echo $radius_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- legacy admin HTML plus this function's own escaped grey-row/label splices.
	if ( '' !== $font_html ) {
		echo '<h4 style="margin-top:24px;">' . esc_html__( 'Font settings', 'neccospeak' ) . '</h4>';
		echo $font_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- legacy admin HTML plus this function's own escaped grey-row/label splices.
	}
	echo '<h4 style="margin-top:24px;">' . esc_html__( 'Custom CSS', 'neccospeak' ) . '</h4>';
	echo $css_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- legacy admin HTML minus the watermark rows cut above, which already escapes every dynamic value it prints.

	// Screenshot-confirmed diff item 5: production's design tab has no
	// inline preview at all — the embedded G6 panel (buttons + iframe) is
	// replaced with production's own single link line, pointed at
	// NeccoSpeak's existing プレビュー top-tab (registered as the 'preview'
	// key in neccospeak_agent_ui_tabs(), includes/agent-ui.php; URL built the
	// same way that file's own tab nav does).
	$preview_tab_url = add_query_arg(
		array(
			'page' => defined( 'NECCOSPEAK_SETTINGS_SLUG' ) ? NECCOSPEAK_SETTINGS_SLUG : 'neccospeak',
			'tab'  => 'preview',
		),
		admin_url( 'admin.php' )
	);
	echo '<p class="description" style="margin-top:16px;"><a href="' . esc_url( $preview_tab_url ) . '">' . esc_html__( 'Preview tab', 'neccospeak' ) . '</a>' . esc_html__( ' lets you check how the widget is displayed.', 'neccospeak' ) . '</p>';

	echo '<h3 class="title">' . esc_html__( 'Widget behavior settings', 'neccospeak' ) . ' <span class="ai-agent-badge ai-agent-badge--draft">' . esc_html__( 'Draft editing', 'neccospeak' ) . '</span></h3>';
	// Screenshot-confirmed diff item 6: Markdown表示/ローディング表示/表示
	// モード/表示速度/スキップボタン ($seg['msgfmt']/$progressive_table) have
	// no production row in ウィジェット動作設定 at all — moved verbatim to
	// NeccoSpeak独自の設定 below instead of echoing here.
	//
	// 0.3.0-dev.68 (deliberate deviation from production parity): 会話の継続
	// (会話を復元/復元有効期間/保存する最大メッセージ数, $seg['continuity'])
	// is rendered VISIBLY here instead. The hidden NeccoSpeak独自の設定
	// section made these fields unreachable from any UI — the production
	// screen simply has no equivalent rows, but "history silently vanishes on
	// every page navigation and no setting can change that" is a real
	// operator-facing defect (field report 2026-08-21). Keep it out of the
	// hidden block below (duplicate field names would double-POST).
	echo '<h4 style="margin-top:24px;">' . esc_html__( 'Conversation continuity', 'neccospeak' ) . '</h4>';
	echo $seg['continuity']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- verbatim legacy admin HTML ($seg['continuity']), same discipline as the other spliced segments in this file.

	// 2026-08 ユーザー決定: ウェルカムメッセージの編集欄は会話フロータブ
	// (「会話の開始」セクション)へ一本化し、このタブ側は非表示にする。DOMからは
	// 削除しない — hidden化したtextareaの値はそのままPOSTされ続けるため、この
	// 'appearance' フォームの保存で welcome_message が空文字に巻き戻る事故
	// (dev.50の再発形)を防ぐ。name/値・保存経路は不変。会話の継続フィールドの
	// 「最初の非表示ラッパーより前に可視で描画」というテスト契約
	// (tests/test-agent-ui-widget-tab.php W-1/W-3)を守るため、会話の継続の
	// 後ろに配置している。
	echo '<div style="display:none" aria-hidden="true" data-neccospeak-extras-hidden="1">';
	neccospeak_agent_ui_render_widget_initial_message( $opts );
	echo '</div>';

	echo '<h3 class="title">' . esc_html__( 'Widget appearance settings', 'neccospeak' ) . ' <span class="ai-agent-badge ai-agent-badge--draft">' . esc_html__( 'Draft editing', 'neccospeak' ) . '</span></h3>';
	neccospeak_agent_ui_render_widget_labels_fields( $opts, $welcome_icon_rows, $welcome_headline_row, $welcome_body_row, $welcome_capabilities_row );

	echo '<h3 class="title">' . esc_html__( 'Launcher design settings', 'neccospeak' ) . ' <span class="ai-agent-badge ai-agent-badge--draft">' . esc_html__( 'Draft editing', 'neccospeak' ) . '</span></h3>';
	neccospeak_agent_ui_render_widget_launcher_section( $opts, $launcher_label_row, $launcher_tooltip_row, $launcher_offset_x_row, $launcher_offset_y_row, $launcher_icon_rows );

	echo '<h3 class="title">' . esc_html__( 'Mobile settings (independent settings for smartphones)', 'neccospeak' ) . ' <span class="ai-agent-badge ai-agent-badge--draft">' . esc_html__( 'Draft editing', 'neccospeak' ) . '</span></h3>';
	neccospeak_agent_ui_render_widget_mobile_section( $opts, $launcher_mobile_size_row, $launcher_mobile_position_row, $launcher_mobile_offset_x_row, $launcher_mobile_offset_y_row );

	echo '<h3 class="title">' . esc_html__( 'Other appearance settings', 'neccospeak' ) . '</h3>';
	neccospeak_agent_ui_render_widget_misc_extra_fields( $opts, $avatar_row, $placeholder_row );

	if ( '' !== $disclaimer_modal_rows ) {
		echo '<h3 class="title">' . esc_html__( 'AI disclaimer', 'neccospeak' ) . '</h3>';
		echo '<table class="form-table ai-agent-form-table" role="presentation"><tbody>' . $disclaimer_modal_rows . '</tbody></table>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- verbatim legacy admin HTML rows cut from $seg['disclaimer'] via neccospeak_agent_ui_cut_tr(), which already escapes every dynamic value it prints.
	}

	// タブ末尾の NeccoSpeak-only セクション — after every production-matching
	// section, before the appearance_bottom hook so that hook stays pinned
	// last per the note above $hook_bottom's assembly. Item 4's AI吹き出し
	// 枠線色 table is this function's own built-in content; every other
	// NS-only block collected above is passed in and printed after it, each
	// under its own short intro line.
	neccospeak_agent_ui_render_widget_ns_only_section(
		$opts,
		array(
			// dev.68: continuity は上の可視ブロックへ移動済み — '' を渡して
			// このブロック内での二重描画(=同名フィールドの二重POST)を防ぐ。
			neccospeak_agent_ui_ns_only_behavior_block( $seg['msgfmt'], '', $progressive_table ),
			neccospeak_agent_ui_ns_only_labels_block( $opts ),
			neccospeak_agent_ui_ns_only_launcher_block( $launcher_size_row, $launcher_position_row, $launcher_shape_row, $launcher_badge_row, $launcher_animation_row ),
			neccospeak_agent_ui_ns_only_mobile_block( $launcher_mobile_hide_row ),
			neccospeak_agent_ui_ns_only_misc_block( $seg['disclaimer'], $home_html, $seg['fallback'] ),
		)
	);

	echo $hook_bottom; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- verbatim hook admin HTML (currently empty; appearance_bottom has no registered listener yet).
}

/**
 * Production-matching 色見本+HEXテキスト入力 pair for this tab only —
 * deliberately NOT the shared neccospeak_render_color_field() helper
 * (includes/admin-tabs.php). That helper marks its text input
 * data-neccospeak-color, which assets/admin.js's initColorPickers() (run
 * page-wide on DOMContentLoaded, gated only by hook_suffix so it fires on
 * this agent-ui page too — see file docblock) upgrades into the
 * wp-color-picker (Iris) "swatch + 色を選択 button" widget. Production has
 * no such control anywhere in this tab (admin-agent-settings.php
 * ~2238-2264 is a plain native <input type="color"> + hex <input
 * type="text"> pair throughout), so reusing that shared helper here would
 * silently regress every row back to the old Iris look the moment
 * admin.js runs — exactly the screenshot-confirmed bug this function
 * exists to avoid. The color<->hex sync is the same inline oninput pair
 * the shared helper uses, just without the attribute that triggers Iris.
 *
 * @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.
 * @param string $text_width     CSS width for the hex text input — production uses 120px for
 *                                standalone rows and a narrower 90px inside gradient rows that
 *                                pack multiple fields on one line (admin-agent-settings.php
 *                                ~2247-2248) to avoid the row wrapping (screenshot item 2).
 * @return void
 */
function neccospeak_agent_ui_widget_color_field( $key, $opts, $default_swatch, $text_width = '120px' ) {
	$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" style="vertical-align:middle;" value="%2$s" 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" style="width:%5$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 ),
		esc_attr( $text_width )
	);
}

/**
 * カラー設定 rows (production admin-agent-settings.php ~2237-2278), in
 * production's row order/labels, each a 色見本+HEX pair via
 * neccospeak_agent_ui_widget_color_field() above where NeccoSpeak has a
 * matching theme_* key. Rows production has that NeccoSpeak has no key for
 * are not rendered at all: ユーザー吹き出しグラデーション/システム背景色/
 * システムテキスト色/フッター背景色 (no matching key at all — confirmed
 * against includes/settings.php's full theme_* key list). 背景透かし画像/
 * 透かし不透明度 ARE bound (theme_watermark_id/theme_watermark_opacity both
 * exist) and are placed here at production's exact position for them (right
 * after where production shows フッター背景色) — but
 * ONLY when $watermark_bindable is true, i.e. neccospeak_agent_ui_render_
 * widget_sections() actually managed to cut the equivalent legacy pair out
 * of the カスタムCSS segment; otherwise this function stays silent there and
 * that legacy pair remains the sole place to edit them, rather than ever
 * rendering two <input name="…[theme_watermark_id]"> in the same <form>.
 * theme_bubble_bot_border (NeccoSpeak-only, no production row) is NOT
 * rendered here (screenshot-confirmed diff item 4) — it used to trail this
 * table as the last row, interleaved with production's own カラー設定 rows;
 * it now renders in its own タブ末尾 NeccoSpeak独自の設定 section instead,
 * see neccospeak_agent_ui_render_widget_ns_only_section() below.
 *
 * @param array $opts                neccospeak_get_options().
 * @param bool  $watermark_bindable  Whether it's safe to render the bound
 *                                    背景透かし画像/透かし不透明度 rows here
 *                                    (see above).
 * @return void
 */
function neccospeak_agent_ui_render_widget_color_rows( $opts, $watermark_bindable = false ) {
	$header_style = isset( $opts['theme_header_style'] ) ? (string) $opts['theme_header_style'] : 'solid';

	$header_bg_default = isset( $opts['brand_color'] ) ? (string) $opts['brand_color'] : '#2f6fed';
	$header_bg_current  = isset( $opts['theme_header_bg'] ) ? (string) $opts['theme_header_bg'] : '';
	$header_bg_swatch   = ( '' !== $header_bg_current && preg_match( '/^#[0-9a-fA-F]{6}$/', $header_bg_current ) ) ? $header_bg_current : $header_bg_default;

	$watermark_id        = isset( $opts['theme_watermark_id'] ) && is_scalar( $opts['theme_watermark_id'] ) ? (int) $opts['theme_watermark_id'] : 0;
	$watermark_url       = ( $watermark_id > 0 && function_exists( 'wp_get_attachment_image_url' ) ) ? (string) wp_get_attachment_image_url( $watermark_id, 'large' ) : '';
	$watermark_thumb     = ( $watermark_id > 0 && function_exists( 'wp_get_attachment_image_src' ) ) ? wp_get_attachment_image_src( $watermark_id, 'thumbnail' ) : false;
	$watermark_thumb_url = is_array( $watermark_thumb ) ? (string) $watermark_thumb[0] : '';

	// theme_watermark_opacity is stored/sanitized (includes/settings.php's
	// 'int_clamp', min 0 max 30) and read on the front end (includes/
	// frontend-config.php, includes/widget-frame.php) as a whole-number
	// percent (0-30), NOT the 0.01-0.3 fraction production's range input
	// shows — confirmed both from the sanitizer spec and from
	// frontend-config.php's `number_format( $wm_op / 100, 2, ... )` (i.e.
	// production's 0.06 === NeccoSpeak's stored 6). The <input name="…
	// [theme_watermark_opacity]"> below therefore keeps submitting that same
	// whole-number percent (so neccospeak_sanitize_int_clamp()'s absint()
	// isn't fed a fractional string, which would just truncate to 0); only
	// the *visible* range control and its live value readout convert to/from
	// production's decimal display, in inline JS.
	$watermark_opacity     = isset( $opts['theme_watermark_opacity'] ) && is_scalar( $opts['theme_watermark_opacity'] ) ? (int) $opts['theme_watermark_opacity'] : 6;
	$watermark_opacity     = max( 0, min( 30, $watermark_opacity ) );
	$watermark_opacity_dec = number_format( $watermark_opacity / 100, 2, '.', '' );
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><label for="neccospeak_theme_panel_bg"><?php esc_html_e( 'Background color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_agent_ui_widget_color_field( 'theme_panel_bg', $opts, '#ffffff' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_thread_bg"><?php esc_html_e( 'Chat background color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_agent_ui_widget_color_field( 'theme_thread_bg', $opts, '#f4f6fb' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_panel_text"><?php esc_html_e( 'Text color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_agent_ui_widget_color_field( 'theme_panel_text', $opts, '#1f2533' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_panel_muted"><?php esc_html_e( 'Text color (muted)', 'neccospeak' ); ?></label></th>
				<td>
					<?php neccospeak_agent_ui_widget_color_field( 'theme_panel_muted', $opts, '#4b5563' ); ?>
					<p class="description"><?php esc_html_e( 'Used for the status line, notes, placeholders, and read dots.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_panel_border"><?php esc_html_e( 'Border color', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_agent_ui_widget_color_field( 'theme_panel_border', $opts, '#e2e6ef' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_header_bg"><?php esc_html_e( 'Header background color', 'neccospeak' ); ?></label></th>
				<td>
					<?php neccospeak_agent_ui_widget_color_field( 'theme_header_bg', $opts, $header_bg_default ); ?>
					<p class="description"><?php esc_html_e( 'Also used as the start color when the header gradient is enabled.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><?php esc_html_e( 'Header gradient', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" id="neccospeak_theme_header_gradient_toggle"
							<?php checked( $header_style, 'gradient' ); ?>
							onchange="var s=document.getElementById('neccospeak_theme_header_style'); if(s){s.value=this.checked?'gradient':'solid'; s.dispatchEvent(new Event('change',{bubbles:true}));}" />
						<?php esc_html_e( 'Enabled', 'neccospeak' ); ?>
					</label>
					<select id="neccospeak_theme_header_style" name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[theme_header_style]" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);" aria-hidden="true" tabindex="-1">
						<option value="solid" <?php selected( $header_style, 'solid' ); ?>><?php esc_html_e( 'Solid color', 'neccospeak' ); ?></option>
						<option value="gradient" <?php selected( $header_style, 'gradient' ); ?>><?php esc_html_e( 'Gradient', 'neccospeak' ); ?></option>
					</select>
					<span style="margin-left:12px;">
						<?php esc_html_e( 'Angle', 'neccospeak' ); ?>
						<input type="number" min="0" max="360" step="1" id="neccospeak_theme_header_angle" class="small-text" style="width:60px;"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[theme_header_angle]"
							value="<?php echo esc_attr( isset( $opts['theme_header_angle'] ) ? (string) $opts['theme_header_angle'] : '135' ); ?>" /> <?php esc_html_e( 'degrees', 'neccospeak' ); ?>
					</span>
					<span style="margin-left:12px;">
						<?php esc_html_e( 'Start color', 'neccospeak' ); ?>
						<span class="neccospeak-color-field ai-agent-color-field">
							<input type="color" class="neccospeak-color-swatch" style="vertical-align:middle;" value="<?php echo esc_attr( $header_bg_swatch ); ?>" disabled="disabled" aria-hidden="true" tabindex="-1" />
							<input type="text" id="neccospeak_theme_header_bg_start_mirror" class="neccospeak-color-hex" style="width:90px;" value="<?php echo esc_attr( $header_bg_current ); ?>" placeholder="<?php echo esc_attr( $header_bg_swatch ); ?>" readonly="readonly" title="<?php echo esc_attr__( 'Shared with the header background color. Edit "Header background color" above.', 'neccospeak' ); ?>" />
						</span>
					</span>
					<span style="margin-left:8px;">
						<?php esc_html_e( 'End color', 'neccospeak' ); ?>
						<?php neccospeak_agent_ui_widget_color_field( 'theme_header_bg2', $opts, '#2f6fed', '90px' ); ?>
					</span>
					<p class="description"><?php esc_html_e( 'When enabled, a gradient is applied to the header background.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_header_text"><?php esc_html_e( 'Header text color', 'neccospeak' ); ?></label></th>
				<td>
					<?php neccospeak_agent_ui_widget_color_field( 'theme_header_text', $opts, '#ffffff' ); ?>
					<p class="description"><?php esc_html_e( 'Color of the header title and icons.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_bubble_user_bg"><?php esc_html_e( 'User bubble background', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_agent_ui_widget_color_field( 'theme_bubble_user_bg', $opts, $header_bg_default ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_bubble_user_text"><?php esc_html_e( 'User bubble text', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_agent_ui_widget_color_field( 'theme_bubble_user_text', $opts, '#ffffff' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_bubble_bot_bg"><?php esc_html_e( 'Bot bubble background', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_agent_ui_widget_color_field( 'theme_bubble_bot_bg', $opts, '#ffffff' ); ?></td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_bubble_bot_text"><?php esc_html_e( 'Bot bubble text', 'neccospeak' ); ?></label></th>
				<td><?php neccospeak_agent_ui_widget_color_field( 'theme_bubble_bot_text', $opts, '#1f2533' ); ?></td>
			</tr>
			<?php // Production rows (admin-agent-settings.php ~2265-2278) — bound (theme_watermark_id/theme_watermark_opacity both exist), rendered only when the caller confirms the equivalent legacy pair further down this tab (under カスタムCSS) was actually cut, so this is never a *second* <input name="…[theme_watermark_id]"> on the page (see this function's docblock). ?>
			<?php if ( $watermark_bindable ) : ?>
			<tr>
				<th scope="row"><?php esc_html_e( 'Background watermark image', 'neccospeak' ); ?></th>
				<td>
					<div class="neccospeak-media" data-neccospeak-media style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
						<input type="hidden" id="neccospeak_theme_watermark_id" class="neccospeak-media-id"
							data-neccospeak-media-input
							data-neccospeak-media-url="<?php echo esc_url( $watermark_url ); ?>"
							data-neccospeak-media-size="large"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[theme_watermark_id]"
							value="<?php echo esc_attr( (string) $watermark_id ); ?>" />
						<input type="text" id="neccospeak_theme_watermark_url_mirror" class="regular-text" style="flex:1;max-width:360px;" readonly="readonly"
							value="<?php echo esc_url( $watermark_url ); ?>" placeholder="https://…" />
						<button type="button" class="button" data-neccospeak-media-select><?php esc_html_e( 'Select from media', 'neccospeak' ); ?></button>
						<button type="button" class="button" data-neccospeak-media-remove style="color:#b32d2e;"><?php esc_html_e( 'Remove', 'neccospeak' ); ?></button>
					</div>
					<img class="neccospeak-media-preview" data-neccospeak-media-preview alt=""
						src="<?php echo esc_url( $watermark_thumb_url ); ?>"
						style="margin-top:8px;max-width:200px;height:auto;"
						<?php echo $watermark_thumb_url ? '' : 'hidden'; ?> />
					<p class="description"><?php esc_html_e( 'Watermark image (e.g. a logo) shown faintly in the chat area background', 'neccospeak' ); ?></p>
					<script>
					( function () {
						'use strict';
						document.addEventListener( 'DOMContentLoaded', function () {
							// initMediaPickers() (assets/admin.js) fires an 'input' event on the
							// id field after every wp.media select/remove (see that file's
							// scheduleThemeChanged()) — reuse it to keep this readonly URL
							// mirror in sync instead of duplicating the media-frame wiring here.
							var idField = document.getElementById( 'neccospeak_theme_watermark_id' );
							var mirror  = document.getElementById( 'neccospeak_theme_watermark_url_mirror' );
							if ( idField && mirror ) {
								idField.addEventListener( 'input', function () {
									mirror.value = idField.getAttribute( 'data-neccospeak-media-url' ) || '';
								} );
							}
						} );
					} )();
					</script>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_theme_watermark_opacity_range"><?php esc_html_e( 'Watermark opacity', 'neccospeak' ); ?></label></th>
				<td>
					<?php /*
					Production (admin-agent-settings.php ~2274-2278) is a
					range input showing a live 0.01-0.3 decimal readout —
					transcribed verbatim below (same min/max/step, same
					current-value span, same "0.01（ほぼ見えない）〜 0.3
					（しっかり見える）" description). It is NOT the field
					that submits: NeccoSpeak's stored/sanitized unit is a
					whole-number percent (0-30, see $watermark_opacity above),
					so the range only drives a hidden input carrying that same
					whole-number percent — the one thing with a
					[theme_watermark_opacity] name attribute in this <form>,
					which is what both neccospeak_sanitize_options() and
					assets/admin-preview.js's readFieldEl()/rawFieldValue()
					(name-attribute-based, works on hidden inputs the same as
					visible ones) actually read.
					*/ ?>
					<input type="range" id="neccospeak_theme_watermark_opacity_range" min="0.01" max="0.3" step="0.01"
						style="vertical-align:middle;width:200px;"
						value="<?php echo esc_attr( $watermark_opacity_dec ); ?>"
						oninput="(function(range){
							var pct = Math.max( 0, Math.min( 30, Math.round( parseFloat( range.value ) * 100 ) ) );
							var hidden = document.getElementById( 'neccospeak_theme_watermark_opacity' );
							hidden.value = String( pct );
							hidden.dispatchEvent( new Event( 'input', { bubbles: true } ) );
							document.getElementById( 'neccospeak_theme_watermark_opacity_value' ).textContent = parseFloat( range.value ).toFixed( 2 );
						})(this)" />
					<span id="neccospeak_theme_watermark_opacity_value"><?php echo esc_html( $watermark_opacity_dec ); ?></span>
					<input type="hidden" id="neccospeak_theme_watermark_opacity"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[theme_watermark_opacity]"
						value="<?php echo esc_attr( (string) $watermark_opacity ); ?>" />
					<p class="description"><?php esc_html_e( '0.01 (barely visible) to 0.3 (clearly visible)', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<?php endif; ?>
			<?php // theme_bubble_bot_border (NeccoSpeak-only, no production row) previously lived here, last in カラー設定 (between 透かし不透明度 and 角丸設定) — moved to its own タブ末尾 NeccoSpeak独自の設定 section (see neccospeak_agent_ui_render_widget_ns_only_section() below and its call site) per screenshot-confirmed diff item 4. ?>
		</tbody>
	</table>
	<script>
	( function () {
		'use strict';
		document.addEventListener( 'DOMContentLoaded', function () {
			// Keep the read-only「開始色」display in the ヘッダーグラデーション
			// row mirroring the real ヘッダー背景色 field live — it is not a
			// second submittable copy of theme_header_bg (see file docblock).
			var primary = document.getElementById( 'neccospeak_theme_header_bg' );
			var mirror  = document.getElementById( 'neccospeak_theme_header_bg_start_mirror' );
			if ( primary && mirror ) {
				primary.addEventListener( 'input', function () {
					mirror.value = primary.value;
					var swatch = mirror.previousElementSibling;
					if ( swatch && /^#[0-9a-fA-F]{6}$/.test( primary.value ) ) {
						swatch.value = primary.value;
					}
				} );
			}
		} );
	} )();
	</script>
	<?php
}

/**
 * 初期メッセージ row (production ai_agent_embed_render_section_design_
 * flow(), admin-agent-settings.php ~2318-2325) — moved here from the
 * legacy top-of-tab table (see file docblock) so it sits under ウィジェット
 * 動作設定 like production, ahead of メッセージ表示形式/会話の継続/段階的
 * 表示.
 *
 * @param array $opts neccospeak_get_options().
 * @return void
 */
function neccospeak_agent_ui_render_widget_initial_message( $opts ) {
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><label for="neccospeak_welcome_message"><?php esc_html_e( '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( isset( $opts['welcome_message'] ) ? (string) $opts['welcome_message'] : '' ); ?></textarea>
					<p class="description"><?php esc_html_e( 'The first message shown when the widget is opened.', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>
	<?php
}

/**
 * The <script> behind neccospeak_agent_ui_render_widget_capabilities_row()'s
 * row-list overlay: parses the (now visually hidden) home_capabilities
 * textarea's newline-separated value into add/remove-able text rows on load,
 * and rebuilds that same textarea's value from the rows on every edit — the
 * textarea, not the rows, is what actually submits, so no sanitizer change
 * is needed. Any pre-existing "iconName|" line prefix (includes/frontend-
 * config.php's neccospeak_widget_home_chips(), a Phase-7b backward-
 * compatible extension with no production equivalent — see that function's
 * own docblock) is captured per-row on parse and silently reattached on
 * rebuild, without ever showing or letting the row editor touch it —
 * production's own できること一覧 row editor (assets/js/admin-agent-
 * settings.js's createListItem()) is confirmed to be plain text only, no
 * icon control, so this doesn't add one either.
 *
 * @return string
 */
function neccospeak_agent_ui_capabilities_editor_script() {
	ob_start();
	?>
	<script>
	( function () {
		'use strict';
		document.addEventListener( 'DOMContentLoaded', function () {
			var ta   = document.getElementById( 'neccospeak_home_capabilities' );
			var wrap = document.querySelector( '[data-neccospeak-capabilities-editor]' );
			if ( ! ta || ! wrap ) {
				return;
			}
			var rowsEl = wrap.querySelector( '[data-neccospeak-capabilities-rows]' );
			var addBtn = wrap.querySelector( '[data-neccospeak-capabilities-add]' );
			if ( ! rowsEl || ! addBtn ) {
				return;
			}

			function splitPrefix( line ) {
				var bar = line.indexOf( '|' );
				if ( -1 === bar ) {
					return { prefix: '', label: line };
				}
				return { prefix: line.slice( 0, bar + 1 ), label: line.slice( bar + 1 ) };
			}

			function sync() {
				var lines = [];
				var rows  = rowsEl.querySelectorAll( '.neccospeak-capabilities-row' );
				for ( var i = 0; i < rows.length; i++ ) {
					var input = rows[ i ].querySelector( 'input' );
					var label = input ? input.value.replace( /\r?\n/g, ' ' ).trim() : '';
					if ( '' === label ) {
						continue;
					}
					lines.push( ( rows[ i ].getAttribute( 'data-neccospeak-capabilities-prefix' ) || '' ) + label );
				}
				ta.value = lines.join( '\n' );
			}

			function addRow( label, prefix ) {
				var row = document.createElement( 'div' );
				row.className = 'neccospeak-capabilities-row';
				row.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:4px;';
				row.setAttribute( 'data-neccospeak-capabilities-prefix', prefix || '' );

				var input = document.createElement( 'input' );
				input.type = 'text';
				input.className = 'regular-text';
				input.value = label || '';
				input.addEventListener( 'input', sync );

				var del = document.createElement( 'button' );
				del.type = 'button';
				del.className = 'button-link';
				del.style.color = '#b32d2e';
				del.textContent = '×';
				del.addEventListener( 'click', function () {
					if ( row.parentNode ) {
						row.parentNode.removeChild( row );
					}
					if ( ! rowsEl.children.length ) {
						addRow( '', '' );
					}
					sync();
				} );

				row.appendChild( input );
				row.appendChild( del );
				rowsEl.appendChild( row );
				return row;
			}

			var initialLines = ta.value.split( /\r\n|\r|\n/ );
			var any = false;
			for ( var j = 0; j < initialLines.length; j++ ) {
				var line = initialLines[ j ].trim();
				if ( '' === line ) {
					continue;
				}
				var parts = splitPrefix( line );
				addRow( parts.label, parts.prefix );
				any = true;
			}
			if ( ! any ) {
				addRow( '', '' );
			}

			addBtn.addEventListener( 'click', function () {
				addRow( '', '' );
			} );
		} );
	} )();
	</script>
	<?php
	return ob_get_clean();
}

/**
 * できること一覧 (home_capabilities) — screenshot-confirmed diff item 7c.
 * Production's row (admin-agent-settings.php ~2368-2376) is a plain add/
 * remove text-row editor (".ai-agent-list-editor", confirmed plain-text-only
 * via assets/js/admin-agent-settings.js's createListItem() — no icon
 * control); this reproduces that shape as a thin JS overlay
 * (neccospeak_agent_ui_capabilities_editor_script()) on top of NeccoSpeak's
 * existing home_capabilities textarea, which keeps being the one thing that
 * actually submits — no sanitizer change.
 *
 * @param string $capabilities_row_html Cut home_capabilities `<tr>` (may be '').
 * @return string
 */
function neccospeak_agent_ui_render_widget_capabilities_row( $capabilities_row_html ) {
	if ( '' === $capabilities_row_html ) {
		return '';
	}
	$row = str_replace(
		'<label for="neccospeak_home_capabilities">' . __( 'Suggested questions (one per line)', 'neccospeak' ) . '</label>',
		'<label for="neccospeak_home_capabilities">' . __( 'Capability list', 'neccospeak' ) . '</label>',
		$capabilities_row_html
	);
	$row = str_replace(
		esc_html__( 'Shown as clickable chips on the home screen. Up to 6 lines; each line is truncated to 60 characters.', 'neccospeak' ),
		esc_html__( 'Shown as tags at the bottom of the welcome screen. Displayed only when content is entered (up to 6 lines; each line is truncated to 60 characters). e.g. "Pricing guide", "Onboarding support", "FAQ answers"', 'neccospeak' ),
		$row
	);
	$row = str_replace(
		'<textarea id="neccospeak_home_capabilities" class="large-text" rows="4"',
		'<textarea id="neccospeak_home_capabilities" class="large-text" rows="4" style="display:none;"',
		$row
	);
	$editor = '<div class="neccospeak-capabilities-editor" data-neccospeak-capabilities-editor>'
		. '<div data-neccospeak-capabilities-rows></div>'
		. '<button type="button" class="button" data-neccospeak-capabilities-add>' . esc_html__( '+ Add', 'neccospeak' ) . '</button>'
		. '</div>';
	$row = str_replace( '<textarea id="neccospeak_home_capabilities"', $editor . '<textarea id="neccospeak_home_capabilities"', $row );
	$row = str_replace( '</textarea>', '</textarea>' . neccospeak_agent_ui_capabilities_editor_script(), $row );
	return $row;
}

/**
 * ウィジェット外観設定 fields — full reorg to production's design_labels()
 * row order (admin-agent-settings.php ~2347-2377): プライマリカラー →
 * ヘッダータイトル → ウェルカムアイコン → ウェルカムアイコン画像 →
 * ウェルカムタイトル → ウェルカムサブタイトル → できること一覧. 8th/9th
 * follow-up round (on top of the earlier プライマリカラー→ヘッダータイトル
 * reorder):
 *   a. プライマリカラー — was a plain hex <input type="text">; now
 *      neccospeak_agent_ui_widget_color_field() like every other color row in
 *      this tab (色見本+HEX pair).
 *   b. ウィジェットタイトル relabeled ヘッダータイトル — same key
 *      (widget_title), production's wording only.
 *   c. ウェルカムアイコン/ウェルカムアイコン画像/ウェルカムタイトル/
 *      ウェルカムサブタイトル/できること一覧 are $welcome_icon_rows/
 *      $welcome_headline_row/$welcome_body_row/$welcome_capabilities_row —
 *      cut out of the legacy ホーム画面 segment by
 *      neccospeak_agent_ui_render_widget_sections() (fail-closed: a landmark
 *      miss leaves that argument '' here, so this table silently omits just
 *      that one row rather than risking two <input>/<textarea> sharing a
 *      name in this <form> — the same row then stays wherever the legacy
 *      renderer already puts it, inside NeccoSpeak独自の設定's ホーム画面
 *      leftover block instead). $welcome_icon_rows carries TWO legacy rows
 *      (home_icon + its conditional home_icon_id custom-image row) relabeled
 *      to match production's ウェルカムアイコン／ウェルカムアイコン画像 pair;
 *      theme_font_family's own precedent applies equally here — home_icon's
 *      sanitizer (includes/settings.php, 'enum' type, fixed 'allowed' list of
 *      7 built-in shapes) has no free-form-Bootstrap-Icons extension hook, so
 *      it stays NeccoSpeak's own icon-shape <select> rather than production's
 *      Bootstrap-Icons picker button. できること一覧 additionally gets a JS
 *      row-editor overlay — see neccospeak_agent_ui_render_widget_capabilities_row().
 *   d. ヘッダーロゴ (NeccoSpeak-only) and 表示ルール/表示するURL/除外するURL
 *      (WP-specific, no production position) moved out of this table
 *      entirely, to neccospeak_agent_ui_ns_only_labels_block() in
 *      NeccoSpeak独自の設定.
 *
 * @param array  $opts                     neccospeak_get_options().
 * @param string $welcome_icon_rows        Cut home_icon + home_icon_id `<tr>` pair (may be '').
 * @param string $welcome_headline_row     Cut home_headline `<tr>` (may be '').
 * @param string $welcome_body_row         Cut home_body `<tr>` (may be '').
 * @param string $welcome_capabilities_row Cut home_capabilities `<tr>` (may be '').
 * @return void
 */
function neccospeak_agent_ui_render_widget_labels_fields( $opts, $welcome_icon_rows = '', $welcome_headline_row = '', $welcome_body_row = '', $welcome_capabilities_row = '' ) {
	$welcome_icon_rows = str_replace(
		esc_html__( 'Custom welcome icon image', 'neccospeak' ),
		esc_html__( 'Welcome icon image', 'neccospeak' ),
		$welcome_icon_rows
	);
	$welcome_icon_rows = str_replace(
		esc_html__( 'Used only when "Welcome icon" above is set to "Custom image".', 'neccospeak' ),
		esc_html__( 'When set, the image is used instead of the welcome icon (set "Welcome icon" above to "Custom image").', 'neccospeak' ),
		$welcome_icon_rows
	);

	$welcome_headline_row = str_replace(
		'<label for="neccospeak_home_headline">' . __( 'Headline', 'neccospeak' ) . '</label>',
		'<label for="neccospeak_home_headline">' . __( 'Welcome title', 'neccospeak' ) . '</label>',
		$welcome_headline_row
	);
	$welcome_headline_row = str_replace(
		esc_html__( 'Optional. If left blank, the built-in text is used.', 'neccospeak' ),
		esc_html__( 'Title shown when the widget is opened (if left blank, the built-in text is used).', 'neccospeak' ),
		$welcome_headline_row
	);

	$welcome_body_row = str_replace(
		'<label for="neccospeak_home_body">' . __( 'Body text', 'neccospeak' ) . '</label>',
		'<label for="neccospeak_home_body">' . __( 'Welcome subtitle', 'neccospeak' ) . '</label>',
		$welcome_body_row
	);
	$welcome_body_row = str_replace(
		esc_html__( 'Optional. If left blank, the built-in text is used.', 'neccospeak' ),
		esc_html__( 'Description shown below the welcome title (if left blank, the built-in text is used).', 'neccospeak' ),
		$welcome_body_row
	);

	$capabilities_row = neccospeak_agent_ui_render_widget_capabilities_row( $welcome_capabilities_row );
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><label for="neccospeak_brand_color"><?php esc_html_e( 'Primary color', 'neccospeak' ); ?></label></th>
				<td>
					<?php neccospeak_agent_ui_widget_color_field( 'brand_color', $opts, '#2f6fed' ); ?>
					<p class="description"><?php esc_html_e( 'Color for the widget\'s buttons, header, and accents.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_widget_title"><?php esc_html_e( 'Header 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( isset( $opts['widget_title'] ) ? (string) $opts['widget_title'] : '' ); ?>" />
					<p class="description"><?php esc_html_e( 'The title shown at the top of the widget.', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<?php echo $welcome_icon_rows; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- legacy admin HTML (cut from $seg['home']) plus this function's own escaped label/description splices; '' when the cut failed. ?>
			<?php echo $welcome_headline_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto. ?>
			<?php echo $welcome_body_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto. ?>
			<?php echo $capabilities_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto, plus this function's own escaped list-editor markup/script. ?>
		</tbody>
	</table>
	<?php
}

/**
 * ヘッダーロゴ (NeccoSpeak-only) + 表示ルール/表示するURL/除外するURL
 * (WP-specific, no production position) — moved out of ウィジェット外観設定
 * into NeccoSpeak独自の設定 (screenshot-confirmed diff item 7d). Markup
 * unchanged from their previous position in
 * neccospeak_agent_ui_render_widget_labels_fields(), just relocated — this
 * was already this file's own freshly-rendered markup (not a legacy-HTML
 * cut), so there's no landmark-miss risk here.
 *
 * @param array $opts neccospeak_get_options().
 * @return string
 */
function neccospeak_agent_ui_ns_only_labels_block( $opts ) {
	$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.
	);
	ob_start();
	?>
	<h4 style="margin-top:24px;"><?php esc_html_e( 'Additional widget appearance settings', 'neccospeak' ); ?></h4>
	<p class="description"><?php esc_html_e( 'NeccoSpeak-specific options that are not present in the production (paid) version\'s screen.', 'neccospeak' ); ?></p>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php esc_html_e( 'Header logo', 'neccospeak' ); ?></th>
				<td>
					<?php neccospeak_render_media_field( 'header_logo_id', $opts, 'medium' ); ?>
					<p class="description"><?php esc_html_e( '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_display_rule"><?php esc_html_e( '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( isset( $opts['display_rule'] ) ? $opts['display_rule'] : 'all', $val ); ?>><?php echo esc_html( $label ); ?></option>
						<?php endforeach; ?>
					</select>
				</td>
			</tr>
			<tr>
				<th scope="row"><label for="neccospeak_included_urls"><?php esc_html_e( '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( isset( $opts['included_urls'] ) ? (string) $opts['included_urls'] : '' ); ?></textarea>
					<p class="description"><?php esc_html_e( '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 esc_html_e( '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( isset( $opts['excluded_urls'] ) ? (string) $opts['excluded_urls'] : '' ); ?></textarea>
					<p class="description"><?php esc_html_e( 'Used when the display rule is "Show on all pages except specified URLs".', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>
	<?php
	return ob_get_clean();
}

/**
 * ランチャーデザイン設定 — full production-order rebuild
 * (admin-agent-settings.php ~2379-2467, ai_agent_embed_render_section_
 * design_labels()). Production's rows that NeccoSpeak has a key for, at
 * production's exact position and in production's order; production rows
 * NeccoSpeak has no key for are not rendered here at all.
 * NeccoSpeak's OWN enum-based launcher controls that don't map 1:1 onto a
 * production row (size S/M/L, position left/right-bottom select, shape,
 * badge, attention-animation) are NOT rendered here — they relocate to
 * NeccoSpeak独自の設定 instead (neccospeak_agent_ui_ns_only_launcher_block()),
 * so production's own corresponding rows (ボタン形状/バッジ表示・色・
 * テキスト/アテンションアニメーション・遅延) render fully grey here rather
 * than partially bound.
 *
 * Bound rows and what backs them (screenshot-confirmed diff item 8's bind
 * policy; 2026-08 3-item follow-up revised the first two entries below to
 * production's exact structure rather than a reshaped legacy row):
 *   - ランチャーラベル: production is a single plain text input, no separate
 *     enable checkbox — rebuilt directly from $opts instead of reshaping
 *     NeccoSpeak's old combined checkbox+text row ($label_row is still
 *     accepted, for call-site symmetry with the other cut rows below, but
 *     its content is now unused). launcher_label_text (settings.php
 *     'text_optional' — empty is a valid, meaningful value) binds the
 *     visible text input 1:1; launcher_label_enabled (settings.php 'bool')
 *     has no visible control of its own — an inline oninput handler keeps a
 *     same-name hidden input in sync with launcher_label_text's emptiness
 *     ("非空→1 / 空→0"), reproducing production's "空欄=ラベルなし"
 *     semantics without inventing a second, non-existent checkbox.
 *   - ランチャーツールチップ: production's own 「ツールチップを表示する」
 *     checkbox + text row — rebuilt directly from $opts ($tooltip_row is
 *     likewise accepted but unused). launcher_tooltip has no separate
 *     enable/disable key (empty text already means "don't show" — see its
 *     own existing description), so the checkbox is UI sugar only, backed
 *     by no new option key: checked reflects launcher_tooltip's current
 *     non-emptiness on load, and unchecking disables + clears the text
 *     input inline — a disabled input isn't submitted at all, so unchecked
 *     naturally saves as '' on submit, matching the existing "空=非表示"
 *     semantics unchanged.
 *   - 位置（右端／下端からの距離）: $offset_x_row/$offset_y_row —
 *     launcher_offset_x/_y, relabeled only (already px number inputs).
 *   - アイコン種別: $icon_rows — launcher_icon (+ its conditional custom-
 *     image row launcher_icon_id), relabeled from 「アイコン」; NeccoSpeak's
 *     7-value enum stays (no free extension hook — same theme_font_family
 *     precedent), with a grey note that production's additional「画像URL」
 *     choice isn't selectable here.
 *
 * @param array  $opts         neccospeak_get_options().
 * @param string $label_row    Cut ラベル `<tr>` (launcher_label_enabled/_text). Accepted for call-site
 *                              symmetry with the cut rows below, but unused — see ランチャーラベル above.
 * @param string $tooltip_row  Cut ツールチップ文言 `<tr>` (launcher_tooltip). Unused, same reason.
 * @param string $offset_x_row Cut 水平方向のオフセット `<tr>` (launcher_offset_x; may be '').
 * @param string $offset_y_row Cut 下端からのオフセット `<tr>` (launcher_offset_y; may be '').
 * @param string $icon_rows    Cut アイコン + カスタムアイコン画像 `<tr>` pair (may be '').
 * @return void
 */
function neccospeak_agent_ui_render_widget_launcher_section( $opts, $label_row, $tooltip_row, $offset_x_row, $offset_y_row, $icon_rows ) {
	// $label_row/$tooltip_row (the cut legacy checkbox+text rows) are no
	// longer reshaped here — ランチャーラベル/ランチャーツールチップ are
	// rendered fresh below, bound straight to $opts, to match production's
	// structure exactly (see this function's docblock). unset() only to
	// keep static analysis quiet about the now-unused parameters; harmless
	// either way since PHP never warns on an unused argument.
	unset( $label_row, $tooltip_row );

	$launcher_label_text_val = isset( $opts['launcher_label_text'] ) ? (string) $opts['launcher_label_text'] : '';
	$launcher_tooltip_val    = isset( $opts['launcher_tooltip'] ) ? (string) $opts['launcher_tooltip'] : '';
	$launcher_tooltip_on     = ( '' !== trim( $launcher_tooltip_val ) );

	$offset_x_row = str_replace(
		'<label for="neccospeak_launcher_offset_x">' . __( 'Horizontal offset', 'neccospeak' ) . '</label>',
		'<label for="neccospeak_launcher_offset_x">' . __( 'Position (distance from right edge)', 'neccospeak' ) . '</label>',
		$offset_x_row
	);
	$offset_y_row = str_replace(
		'<label for="neccospeak_launcher_offset_y">' . __( 'Offset from bottom edge', 'neccospeak' ) . '</label>',
		'<label for="neccospeak_launcher_offset_y">' . __( 'Position (distance from bottom edge)', 'neccospeak' ) . '</label>',
		$offset_y_row
	);
	$icon_rows = str_replace(
		'<label for="neccospeak_launcher_icon">' . __( 'Icon', 'neccospeak' ) . '</label>',
		'<label for="neccospeak_launcher_icon">' . __( 'Icon type', 'neccospeak' ) . '</label>',
		$icon_rows
	);
	// 2026-08 ユーザー決定: 「画像URL」アイコン種別が選べない旨の注記段落は撤去。
	// 本番の可視要素に対応しない冗長な純粋説明テキストで、フォーム的な意味を
	// 持たず、POST経路にも影響しなかったため（本番の「アイコン画像URL」行自体、
	// NeccoSpeak に対応キーが無いためこのタブには存在しない）。
	?>
	<p class="description" style="margin-bottom:12px;"><?php esc_html_e( 'Customize the appearance of the launcher button before it expands. Blank fields use their default values.', 'neccospeak' ); ?></p>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><label for="neccospeak_launcher_label_text"><?php esc_html_e( 'Launcher label', 'neccospeak' ); ?></label></th>
				<td>
					<input type="hidden" id="neccospeak_launcher_label_enabled"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_label_enabled]"
						value="<?php echo ( '' !== trim( $launcher_label_text_val ) ) ? '1' : '0'; ?>" />
					<input type="text" id="neccospeak_launcher_label_text" class="regular-text" style="width:200px;" maxlength="50"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_label_text]"
						value="<?php echo esc_attr( $launcher_label_text_val ); ?>"
						oninput="document.getElementById('neccospeak_launcher_label_enabled').value = ( '' !== this.value.trim() ) ? '1' : '0';" />
					<p class="description"><?php esc_html_e( 'Text shown on the widget launch button (e.g. Try it, Ask the AI), up to 50 characters', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<tr>
				<th scope="row"><?php esc_html_e( 'Launcher tooltip', 'neccospeak' ); ?></th>
				<td>
					<label style="display:block;margin-bottom:6px;">
						<input type="checkbox" id="neccospeak_launcher_tooltip_enabled" <?php checked( $launcher_tooltip_on ); ?>
							onchange="var t=document.getElementById('neccospeak_launcher_tooltip'); if(this.checked){t.disabled=false;}else{t.disabled=true;t.value='';}" />
						<?php esc_html_e( 'Show tooltip', 'neccospeak' ); ?>
					</label>
					<input type="text" id="neccospeak_launcher_tooltip" class="regular-text" style="width:400px;" maxlength="200"
						name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[launcher_tooltip]"
						value="<?php echo esc_attr( $launcher_tooltip_val ); ?>"
						<?php disabled( ! $launcher_tooltip_on ); ?> />
					<p class="description"><?php esc_html_e( 'Text shown in the bubble next to the widget launch button', 'neccospeak' ); ?></p>
				</td>
			</tr>
			<?php echo $offset_x_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto. ?>
			<?php echo $offset_y_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto. ?>
			<?php echo $icon_rows; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto, plus this function's own escaped label/note splices. ?>
		</tbody>
	</table>
	<?php
}

/**
 * NeccoSpeak's own enum-based launcher controls that don't map 1:1 onto any
 * single production row — screenshot-confirmed diff item 8's "NS独自の行"
 * list: サイズ(S/M/L)/位置(左下・右下 select)/形状/バッジ/注目アニメーション.
 * Markup unchanged from their previous position inside ランチャー
 * (includes/admin-tabs.php's legacy table), just relocated; production's OWN
 * nearby rows (ボタン形状/バッジ表示・色・テキスト/アテンションアニメーション・
 * 遅延) render fully grey at production's position in
 * neccospeak_agent_ui_render_widget_launcher_section() instead of being
 * partially bound to these.
 *
 * @param string $size_row      Cut サイズ `<tr>` (launcher_size; may be '').
 * @param string $position_row  Cut 位置 `<tr>` (launcher_position; may be '').
 * @param string $shape_row     Cut 形状 `<tr>` (launcher_shape; may be '').
 * @param string $badge_row     Cut バッジ `<tr>` (launcher_badge/_text; may be '').
 * @param string $animation_row Cut 注目アニメーション `<tr>` (launcher_animation/_once; may be '').
 * @return string
 */
function neccospeak_agent_ui_ns_only_launcher_block( $size_row, $position_row, $shape_row, $badge_row, $animation_row ) {
	ob_start();
	?>
	<h4 style="margin-top:24px;"><?php esc_html_e( 'Additional launcher settings', 'neccospeak' ); ?></h4>
	<p class="description"><?php esc_html_e( 'NeccoSpeak-specific options that are not present in the production (paid) version\'s screen.', 'neccospeak' ); ?></p>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<?php
			echo $size_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- verbatim legacy admin HTML (cut from $seg['launcher']).
			echo $position_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto.
			echo $shape_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto.
			echo $badge_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto.
			echo $animation_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto.
			?>
		</tbody>
	</table>
	<?php
	return ob_get_clean();
}

/**
 * 「ランチャープレビュー（概略）」— production's own live JS-driven snapshot
 * (admin-agent-settings.php ~2485-2493) rebuilt here as a PHP-rendered STATIC
 * approximation instead: computed once from $opts at page load (brand_color/
 * launcher_label_enabled+text), not live-updated as other fields change
 * without a page reload/re-save. Chosen per this round's explicit "static
 * approximation is fine if live JS is heavy, report it" allowance — a live
 * version would mean either duplicating this tab's several relevant fields'
 * change events into a mini re-renderer, or reaching into assets/
 * admin-preview.js's field-sync plumbing from outside this file's edit
 * scope.
 *
 * @param array $opts neccospeak_get_options().
 * @return string
 */
function neccospeak_agent_ui_render_widget_launcher_preview( $opts ) {
	$color = ( isset( $opts['brand_color'] ) && preg_match( '/^#[0-9a-fA-F]{6}$/', (string) $opts['brand_color'] ) ) ? (string) $opts['brand_color'] : '#2f6fed';
	$label = ( ! empty( $opts['launcher_label_enabled'] ) && isset( $opts['launcher_label_text'] ) && '' !== trim( (string) $opts['launcher_label_text'] ) )
		? (string) $opts['launcher_label_text']
		: '';
	ob_start();
	?>
	<div style="margin-top:16px;padding:16px;background:#f0f4f8;border-radius:8px;border:1px solid #ddd;">
		<p style="margin:0 0 10px;font-size:13px;font-weight:600;color:#444;"><?php esc_html_e( 'Launcher preview (approximate)', 'neccospeak' ); ?></p>
		<div style="position:relative;height:100px;background:repeating-linear-gradient(45deg,#e8edf2 0,#e8edf2 8px,#f0f4f8 8px,#f0f4f8 16px);border-radius:6px;">
			<div style="position:absolute;right:16px;bottom:16px;height:56px;min-width:56px;border-radius:9999px;background:<?php echo esc_attr( $color ); ?>;color:#fff;display:flex;align-items:center;justify-content:center;gap:8px;padding:0 16px;font-size:14px;font-weight:600;box-shadow:0 4px 16px rgba(0,0,0,0.25);">
				<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m3 21 1.9-5.7a8.5 8.5 0 1 1 3.8 3.8z"/></svg>
				<?php if ( '' !== $label ) : ?><span><?php echo esc_html( $label ); ?></span><?php endif; ?>
			</div>
		</div>
		<p class="description" style="margin-top:8px;"><?php esc_html_e( 'An approximate preview based on the current settings (static display; unsaved changes are not reflected).', 'neccospeak' ); ?></p>
	</div>
	<?php
	return ob_get_clean();
}

/**
 * モバイル設定（スマートフォン向け独立設定） — production-order rebuild
 * (admin-agent-settings.php ~2470-2493), including production's 「モバイル
 * 個別設定」 master checkbox row (~2472: 「モバイルでは別サイズ・位置を使用
 * する」), previously omitted (see this file's earlier revision history
 * above for the original rationale) and now added back per direct
 * screenshot-confirmed instruction.
 *
 * NeccoSpeak has no dedicated boolean option backing that checkbox —
 * launcher_mobile_size/launcher_mobile_position are each their OWN
 * independent 'inherit'|explicit-value enum (includes/settings.php), so this
 * checkbox is UI sugar only (no new option key — it carries no name=
 * attribute at all), reproducing production's on/off *behavior* on top of
 * those two existing keys instead of inventing a third:
 *   - Initial checked state is computed from $opts: ON if EITHER select's
 *     current value isn't 'inherit' ("デスクトップと同じ").
 *   - Turning it OFF resets both selects' value to 'inherit' and disables
 *     them, so neither is present in the submitted POST data at all —
 *     confirmed safe against neccospeak_sanitize_field_value()'s 'enum' case
 *     (includes/settings.php), which already falls back to
 *     $defaults[ $key ] (== 'inherit' for both keys) when a key is missing
 *     from input, so an unsubmitted disabled <select> still saves the exact
 *     same "デスクトップと同じ" value as if it had been submitted — no
 *     separate hidden mirror field is needed. It also visually greys out the
 *     whole モバイル詳細 group (the two selects, the always-grey production-
 *     only px stand-ins, and the bound $offset_x_row/$offset_y_row) via a
 *     wrapping <div>'s opacity + pointer-events, matching production's
 *     checkbox-gates-detail-fields shape without hiding the row outright
 *     (team-lead instruction: grey, not display:none).
 *   - Turning it ON reverses both (re-enables the selects; clears the grey
 *     styling).
 *   The disabled attribute on the two selects, and the div's grey style, are
 *   also set server-side straight from the same $opts-derived state, so the
 *   very first (or no-JS) render already matches; the inline <script> below
 *   only keeps this in sync on user interaction with the checkbox.
 *
 * Row order inside モバイル詳細 now matches production's サイズ→幅→
 * アイコンサイズ→位置（右）→位置（下） (the three always-grey px stand-ins,
 * then $offset_x_row/$offset_y_row), with NeccoSpeak's own
 * $mobile_size_row/$mobile_position_row enum selects trailing after —
 * they're NeccoSpeak's nearest equivalent to production's サイズ/位置 px
 * fields but a different control shape (enum vs. free px), so they keep
 * following the "NS-only equivalents trail the full production group"
 * convention used throughout this file, rather than displacing production's
 * own row order.
 *
 * @param array  $opts                 neccospeak_get_options().
 * @param string $mobile_size_row      Cut モバイルでのサイズ `<tr>` (launcher_mobile_size; may be '').
 * @param string $mobile_position_row  Cut モバイルでの位置 `<tr>` (launcher_mobile_position; may be '').
 * @param string $offset_x_row         Cut モバイル水平方向のオフセット `<tr>` (launcher_mobile_offset_x; may be '').
 * @param string $offset_y_row         Cut モバイル下端からのオフセット `<tr>` (launcher_mobile_offset_y; may be '').
 * @return void
 */
function neccospeak_agent_ui_render_widget_mobile_section( $opts, $mobile_size_row, $mobile_position_row, $offset_x_row, $offset_y_row ) {
	$offset_x_row = str_replace(
		'<label for="neccospeak_launcher_mobile_offset_x">' . __( 'Horizontal offset on mobile', 'neccospeak' ) . '</label>',
		'<label for="neccospeak_launcher_mobile_offset_x">' . __( 'Position (right)', 'neccospeak' ) . '</label>',
		$offset_x_row
	);
	$offset_y_row = str_replace(
		'<label for="neccospeak_launcher_mobile_offset_y">' . __( 'Offset from bottom edge on mobile', 'neccospeak' ) . '</label>',
		'<label for="neccospeak_launcher_mobile_offset_y">' . __( 'Position (bottom)', 'neccospeak' ) . '</label>',
		$offset_y_row
	);

	$mobile_size_val     = isset( $opts['launcher_mobile_size'] ) ? (string) $opts['launcher_mobile_size'] : 'inherit';
	$mobile_position_val = isset( $opts['launcher_mobile_position'] ) ? (string) $opts['launcher_mobile_position'] : 'inherit';
	$mobile_enabled      = ( 'inherit' !== $mobile_size_val || 'inherit' !== $mobile_position_val );

	// Belt-and-braces: server-render the disabled attribute on the two real
	// selects too (on top of the inline <script> below), so an OFF state
	// can't submit a stray non-'inherit' value even with JS disabled.
	if ( ! $mobile_enabled ) {
		$mobile_size_row     = str_replace( 'id="neccospeak_launcher_mobile_size"', 'id="neccospeak_launcher_mobile_size" disabled="disabled"', $mobile_size_row );
		$mobile_position_row = str_replace( 'id="neccospeak_launcher_mobile_position"', 'id="neccospeak_launcher_mobile_position" disabled="disabled"', $mobile_position_row );
	}

	// 2026-08 ユーザー決定: 「モバイルでのサイズ」「モバイルでの位置」の2つの<select>
	// （NeccoSpeak独自のenumセレクト。本番のモバイル詳細はサイズ/幅/アイコンサイズ/
	// 位置（右）/位置（下）のpx項目のみで、この2つに対応する行が無い）は非表示にする。
	// DOMからは削除しない — hidden化してもname/valueはPOSTされ続け、上の「モバイル
	// 個別設定」チェックボックスのJS同期（値の書き込み）も引き続き動作する。
	// 注意: <table>/<tbody>直下は<div>を子に持てず、<div>で<tr>を包んでもHTMLパーサーが
	// divをtableの外へforeign-parenting(foster-parenting)し、中の<tr>はtbody直下に戻って
	// 可視のまま残ってしまう（実機検証済み）。そのため<tr>自体にstyle/aria/data属性を
	// 付与する（$mobile_size_row/$mobile_position_rowはneccospeak_agent_ui_cut_tr()が
	// 素の"<tr>"から切り出した文字列で、各1個ずつしか含まないためstr_replaceで安全）。
	$mobile_size_row     = str_replace( '<tr>', '<tr style="display:none" aria-hidden="true" data-neccospeak-extras-hidden="1">', $mobile_size_row );
	$mobile_position_row = str_replace( '<tr>', '<tr style="display:none" aria-hidden="true" data-neccospeak-extras-hidden="1">', $mobile_position_row );
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><?php esc_html_e( 'Mobile-specific settings', 'neccospeak' ); ?></th>
				<td>
					<label>
						<input type="checkbox" id="neccospeak_launcher_mobile_enabled" <?php checked( $mobile_enabled ); ?>
							onchange="neccospeakAgentUiToggleMobileDetail(this.checked);" />
						<?php esc_html_e( 'Use a different size and position on mobile', 'neccospeak' ); ?>
					</label>
				</td>
			</tr>
			<tr>
				<th scope="row"><?php esc_html_e( 'Mobile details', 'neccospeak' ); ?></th>
				<td>
					<div id="neccospeak-launcher-mobile-detail-group" style="<?php echo $mobile_enabled ? '' : 'opacity:.5;pointer-events:none;'; ?>">
						<table style="border-collapse:collapse;margin-top:8px;">
							<tbody>
								<?php echo $offset_x_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- legacy admin HTML (cut from $seg['launcher_m']); '' when the cut failed, plus this function's own escaped label splice. ?>
								<?php echo $offset_y_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto. ?>
								<?php echo $mobile_size_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto, plus this function's own escaped disabled-attribute/hidden-attribute splice (see top of function). ?>
								<?php echo $mobile_position_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- ditto. ?>
							</tbody>
						</table>
					</div>
				</td>
			</tr>
		</tbody>
	</table>
	<script>
	( function () {
		'use strict';
		function neccospeakAgentUiApplyMobileDetailState( checked ) {
			var group   = document.getElementById( 'neccospeak-launcher-mobile-detail-group' );
			var sizeSel = document.getElementById( 'neccospeak_launcher_mobile_size' );
			var posSel  = document.getElementById( 'neccospeak_launcher_mobile_position' );
			if ( group ) {
				group.style.opacity       = checked ? '' : '.5';
				group.style.pointerEvents = checked ? '' : 'none';
			}
			[ sizeSel, posSel ].forEach( function ( sel ) {
				if ( ! sel ) {
					return;
				}
				if ( checked ) {
					sel.disabled = false;
				} else {
					sel.value    = 'inherit';
					sel.disabled = true;
					sel.dispatchEvent( new Event( 'change', { bubbles: true } ) );
				}
			} );
		}
		window.neccospeakAgentUiToggleMobileDetail = neccospeakAgentUiApplyMobileDetailState;
		document.addEventListener( 'DOMContentLoaded', function () {
			var checkbox = document.getElementById( 'neccospeak_launcher_mobile_enabled' );
			if ( checkbox ) {
				neccospeakAgentUiApplyMobileDetailState( checkbox.checked );
			}
		} );
	} )();
	</script>
	<?php
	echo neccospeak_agent_ui_render_widget_launcher_preview( $opts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this function's own escaped output, see its docblock.
}

/**
 * 「モバイルで非表示」(launcher_mobile_hide) — no production row.
 *
 * @param string $hide_row Cut モバイルで非表示 `<tr>` (may be '').
 * @return string
 */
function neccospeak_agent_ui_ns_only_mobile_block( $hide_row ) {
	if ( '' === $hide_row ) {
		return '';
	}
	ob_start();
	?>
	<h4 style="margin-top:24px;"><?php esc_html_e( 'Additional mobile settings', 'neccospeak' ); ?></h4>
	<p class="description"><?php esc_html_e( 'NeccoSpeak-specific options that are not present in the production (paid) version\'s screen.', 'neccospeak' ); ?></p>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<?php echo $hide_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- verbatim legacy admin HTML (cut from $seg['launcher_m']). ?>
		</tbody>
	</table>
	<?php
	return ob_get_clean();
}

/**
 * その他の外観設定 — production's exact 3 rows
 * (admin-agent-settings.php ~2495-2524):
 *   - アバター絵文字: NeccoSpeak has no matching key at all — grey stand-in,
 *     rebuilt from a plain 「アイコンを選択」 button to production's own
 *     icon-display-button + icon-name-text shape (~2498-2510: a 40×30
 *     "選択" button beside a name <span>) with production's own description
 *     text.
 *   - アバター画像: home_avatar_id — confirmed against production source to
 *     exist in exactly ONE place there (this row; production's
 *     design_labels() ウェルカムアイコン画像 is a DIFFERENT field, the
 *     welcome-screen icon, not the chat avatar), so NeccoSpeak's bound copy
 *     moves here too (from its previous position inside the legacy ホーム
 *     画面 block) rather than existing in two places at once. Rebuilt from
 *     neccospeak_render_media_field()'s raw-attachment-ID-<input type=
 *     "number"> + 「画像を選択」/「画像を削除」link shape to the same
 *     hidden-ID + read-only URL mirror + 「メディアから選択」/「削除」button
 *     pattern already used for 背景透かし画像
 *     (neccospeak_agent_ui_render_widget_color_rows()) — same
 *     data-neccospeak-media convention (home_avatar_id bound via
 *     data-neccospeak-media-input etc.), so assets/admin.js's
 *     initMediaPickers() keeps working unmodified — with production's own
 *     description text ("設定すると絵文字の代わりに画像がアバターに使用され
 *     ます。メディアライブラリまたは外部URLを指定できます。"; NeccoSpeak's
 *     implementation only accepts a media-library attachment, not a raw
 *     external URL, same as its prior <input type="number"> shape did — the
 *     production copy is used verbatim per direct instruction). Because this
 *     is now freshly rendered from $opts rather than a str_replace() splice
 *     of the legacy markup (production's shape differs too much from
 *     neccospeak_render_media_field()'s for that to be practical), it's only
 *     rendered when $avatar_row is non-empty, i.e.
 *     neccospeak_agent_ui_render_widget_sections() actually cut the legacy
 *     copy out of $seg['home'] first — same fail-closed reasoning as every
 *     other cut-and-rebuild in this file: on a landmark miss this function
 *     stays silent here and the legacy row (now relocated into NeccoSpeak
 *     独自の設定's ホーム画面 leftover block) remains the sole place to edit
 *     it, rather than ever rendering two <input name="…[home_avatar_id]">
 *     elements in one <form>.
 *   - 入力プレースホルダー: $placeholder_row, home_input_placeholder, moved
 *     the same way, relabeled to production's shorter wording and
 *     description text ("チャット入力欄に表示するヒントテキスト", with
 *     NeccoSpeak's own applicability note kept as a short trailing
 *     parenthetical instead of a second full sentence).
 *
 * @param array  $opts            neccospeak_get_options().
 * @param string $avatar_row      Cut アバター画像 `<tr>` (home_avatar_id; may be ''). Only its
 *                                  emptiness is consulted now (see above) — unlike this file's
 *                                  other cut-row splices, its markup itself is no longer reused.
 * @param string $placeholder_row Cut 入力欄のプレースホルダー `<tr>` (home_input_placeholder; may be '').
 * @return void
 */
function neccospeak_agent_ui_render_widget_misc_extra_fields( $opts, $avatar_row = '', $placeholder_row = '' ) {
	$placeholder_row = str_replace(
		'<label for="neccospeak_home_input_placeholder">' . __( 'Input placeholder', 'neccospeak' ) . '</label>',
		'<label for="neccospeak_home_input_placeholder">' . __( 'Input placeholder', 'neccospeak' ) . '</label>',
		$placeholder_row
	);
	$placeholder_row = str_replace(
		esc_html__( 'Applied only while the home screen is shown. If left blank, the default placeholder is used.', 'neccospeak' ),
		esc_html__( 'Hint text shown in the chat input field (applies only while the home screen is shown; blank uses the default).', 'neccospeak' ),
		$placeholder_row
	);

	$avatar_id        = isset( $opts['home_avatar_id'] ) && is_scalar( $opts['home_avatar_id'] ) ? (int) $opts['home_avatar_id'] : 0;
	$avatar_url       = ( $avatar_id > 0 && function_exists( 'wp_get_attachment_image_url' ) ) ? (string) wp_get_attachment_image_url( $avatar_id, 'thumbnail' ) : '';
	$avatar_thumb     = ( $avatar_id > 0 && function_exists( 'wp_get_attachment_image_src' ) ) ? wp_get_attachment_image_src( $avatar_id, 'thumbnail' ) : false;
	$avatar_thumb_url = is_array( $avatar_thumb ) ? (string) $avatar_thumb[0] : '';
	?>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<?php if ( '' !== $avatar_row ) : ?>
			<tr>
				<th scope="row"><?php esc_html_e( 'Avatar image', 'neccospeak' ); ?></th>
				<td>
					<div class="neccospeak-media" data-neccospeak-media style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
						<input type="hidden" id="neccospeak_home_avatar_id" class="neccospeak-media-id"
							data-neccospeak-media-input
							data-neccospeak-media-url="<?php echo esc_url( $avatar_url ); ?>"
							data-neccospeak-media-size="thumbnail"
							name="<?php echo esc_attr( NECCOSPEAK_OPTION ); ?>[home_avatar_id]"
							value="<?php echo esc_attr( (string) $avatar_id ); ?>" />
						<input type="text" id="neccospeak_home_avatar_url_mirror" class="regular-text" style="flex:1;max-width:360px;" readonly="readonly"
							value="<?php echo esc_url( $avatar_url ); ?>" placeholder="https://…" />
						<button type="button" class="button" data-neccospeak-media-select><?php esc_html_e( 'Select from media', 'neccospeak' ); ?></button>
						<button type="button" class="button" data-neccospeak-media-remove style="color:#b32d2e;"><?php esc_html_e( 'Remove', 'neccospeak' ); ?></button>
					</div>
					<img class="neccospeak-media-preview" data-neccospeak-media-preview alt=""
						src="<?php echo esc_url( $avatar_thumb_url ); ?>"
						style="margin-top:8px;max-width:120px;height:auto;border-radius:50%;"
						<?php echo $avatar_thumb_url ? '' : 'hidden'; ?> />
					<p class="description"><?php esc_html_e( 'When set, the image is used as the avatar instead of the emoji. You can specify a media library item or an external URL.', 'neccospeak' ); ?></p>
					<script>
					( function () {
						'use strict';
						document.addEventListener( 'DOMContentLoaded', function () {
							// Same mirror-sync approach as 背景透かし画像
							// (neccospeak_agent_ui_render_widget_color_rows()) —
							// initMediaPickers() (assets/admin.js) fires an 'input'
							// event on the id field after every wp.media select/remove.
							var idField = document.getElementById( 'neccospeak_home_avatar_id' );
							var mirror  = document.getElementById( 'neccospeak_home_avatar_url_mirror' );
							if ( idField && mirror ) {
								idField.addEventListener( 'input', function () {
									mirror.value = idField.getAttribute( 'data-neccospeak-media-url' ) || '';
								} );
							}
						} );
					} )();
					</script>
				</td>
			</tr>
			<?php endif; ?>
			<?php echo $placeholder_row; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- legacy admin HTML (cut from $seg['home']) plus this function's own escaped label/description splices. ?>
		</tbody>
	</table>
	<?php
}

/**
 * ウィジェット動作設定 の NeccoSpeak固有9項目 — screenshot-confirmed diff
 * item 6. $msgfmt_html/$continuity_html/$progressive_html are
 * $seg['msgfmt']/$seg['continuity']/$progressive_table verbatim — each is
 * already just that section's `<table>` (its own `<h2>` heading was already
 * consumed as this file's split marker, not part of the segment, so this
 * function supplies its own lighter sub-headings instead), covering:
 * Markdown表示・ローディング表示・会話を復元・復元有効期間（分）・保存する
 * 最大メッセージ数・保存する最大サイズ（バイト）・表示モード・表示速度・
 * スキップボタン. Production's ウィジェット動作設定 has no row for any of
 * them.
 *
 * @param string $msgfmt_html      $seg['msgfmt'] verbatim.
 * @param string $continuity_html  $seg['continuity'] verbatim.
 * @param string $progressive_html $progressive_table verbatim.
 * @return string
 */
function neccospeak_agent_ui_ns_only_behavior_block( $msgfmt_html, $continuity_html, $progressive_html ) {
	ob_start();
	?>
	<h4 style="margin-top:24px;"><?php esc_html_e( 'Additional widget behavior settings', 'neccospeak' ); ?></h4>
	<p class="description"><?php esc_html_e( 'NeccoSpeak-specific options that are not present in the production (paid) version\'s screen.', 'neccospeak' ); ?></p>
	<h5 style="margin:16px 0 4px;"><?php esc_html_e( 'Message display format', 'neccospeak' ); ?></h5>
	<?php echo $msgfmt_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- verbatim legacy admin HTML ($seg['msgfmt']). ?>
	<?php if ( '' !== $continuity_html ) : // dev.68: 通常は可視側(ウィジェット動作設定)で描画済みのため '' が渡ってくる。 ?>
	<h5 style="margin:16px 0 4px;"><?php esc_html_e( 'Conversation continuity', 'neccospeak' ); ?></h5>
	<?php echo $continuity_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- verbatim legacy admin HTML ($seg['continuity']). ?>
	<?php endif; ?>
	<h5 style="margin:16px 0 4px;"><?php esc_html_e( 'Progressive replies', 'neccospeak' ); ?></h5>
	<?php echo $progressive_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- verbatim legacy admin HTML ($progressive_table). ?>
	<?php
	return ob_get_clean();
}

/**
 * AI免責事項 (常時表示の注記のみ — 初回表示モーダル／同意の記憶方法の2行は
 * disclaimer_modal_enabled がマスタートグルのため2026-08のバグ修正で
 * neccospeak_agent_ui_render_widget_sections() 側へ切り出し済み、可視化した)
 * ＋ ホーム画面の残り (有効化／開いたときの既定表示／
 * 新規会話ボタン／オンライン状態インジケーター／機能タグ — home_headline/
 * home_body/home_icon/home_icon_id/home_avatar_id/home_capabilities/
 * home_input_placeholder はすべて他所へ移設済みで、ここにはもう無い) ＋
 * フォールバック(お問い合わせリンクURL 等) — production has no equivalent
 * screen for any of these (confirmed against admin-agent-settings.php's
 * design tab, which has no disclaimer/fallback section at all), so
 * $seg['disclaimer']/$seg['fallback'] relocate verbatim in full, and what's
 * left of $seg['home'] after neccospeak_agent_ui_render_widget_sections()'s
 * cuts relocates alongside them.
 *
 * home_capability_tags specifically wasn't named in either the 「ウィジェット
 * 外観設定に移設」 or 「NeccoSpeak独自の設定に移設」 lists given for this
 * round — it's included here as NeccoSpeak-only by the same reasoning as
 * everything else in this block (no production counterpart at all, per its
 * own docblock in includes/frontend-config.php, and distinct from
 * home_capabilities/できること一覧, which now IS bound to a production row) —
 * flagged in the final report in case that inference is wrong.
 *
 * @param string $disclaimer_html     $seg['disclaimer'] verbatim.
 * @param string $home_remainder_html $seg['home'] after this round's cuts.
 * @param string $fallback_html       $seg['fallback'] verbatim.
 * @return string
 */
function neccospeak_agent_ui_ns_only_misc_block( $disclaimer_html, $home_remainder_html, $fallback_html ) {
	ob_start();
	?>
	<h4 style="margin-top:24px;"><?php esc_html_e( 'Additional disclaimer, home screen, and fallback settings', 'neccospeak' ); ?></h4>
	<p class="description"><?php esc_html_e( 'NeccoSpeak-specific options that are not present in the production (paid) version\'s screen.', 'neccospeak' ); ?></p>
	<h5 style="margin:16px 0 4px;"><?php esc_html_e( 'AI disclaimer', 'neccospeak' ); ?></h5>
	<?php echo $disclaimer_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- verbatim legacy admin HTML ($seg['disclaimer']). ?>
	<h5 style="margin:16px 0 4px;"><?php esc_html_e( 'Home screen', 'neccospeak' ); ?></h5>
	<?php echo $home_remainder_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- verbatim legacy admin HTML ($seg['home'] minus this round's cuts). ?>
	<h5 style="margin:16px 0 4px;"><?php echo esc_html_x( 'Fallback', 'settings section heading', 'neccospeak' ); ?></h5>
	<?php echo $fallback_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- verbatim legacy admin HTML ($seg['fallback']). ?>
	<?php
	return ob_get_clean();
}

/**
 * 「NeccoSpeak独自の設定」— a dedicated section at the very end of this tab
 * for fields that have no production counterpart at all, so they no longer
 * sit interleaved inside a production-matching table. Its own built-in
 * content is theme_bubble_bot_border (AI吹き出し枠線色, screenshot-confirmed
 * diff item 4); $extra_blocks are further pre-rendered blocks collected by
 * neccospeak_agent_ui_render_widget_sections() (items 6/7d/8/9) — each
 * already carries its own heading/description, so this just prints them in
 * order after the built-in table.
 *
 * The whole section (heading, description, every field inside) is wrapped in
 * a `display:none` <div data-neccospeak-extras-hidden="1"> per direct
 * instruction, rather than removed from the DOM: production has no
 * equivalent screen for any of this, so it's hidden from the visible UI, but
 * every input/select/textarea inside is left physically present and keeps
 * its real name="…[key]" attribute — a hidden but still-present checkbox
 * still POSTs its checked state on save, so this does NOT regress to the
 * "checkbox missing from the DOM → silently saves as unchecked/default" data
 * -loss failure mode that actually deleting this markup would risk.
 *
 * @param array $opts         neccospeak_get_options().
 * @param array $extra_blocks Pre-rendered HTML block strings (may contain '').
 * @return void
 */
function neccospeak_agent_ui_render_widget_ns_only_section( $opts, $extra_blocks = array() ) {
	?>
	<div style="display:none" aria-hidden="true" data-neccospeak-extras-hidden="1">
	<h3 class="title"><?php esc_html_e( 'NeccoSpeak-specific settings', 'neccospeak' ); ?></h3>
	<table class="form-table ai-agent-form-table" role="presentation">
		<tbody>
			<tr>
				<th scope="row"><label for="neccospeak_theme_bubble_bot_border"><?php esc_html_e( 'AI bubble border color', 'neccospeak' ); ?></label></th>
				<td>
					<?php neccospeak_agent_ui_widget_color_field( 'theme_bubble_bot_border', $opts, '' ); ?>
					<p class="description"><?php esc_html_e( 'If left blank, AI bubbles have no border by default. (This option does not exist in production; it is a NeccoSpeak-specific setting.)', 'neccospeak' ); ?></p>
				</td>
			</tr>
		</tbody>
	</table>
	<?php
	foreach ( $extra_blocks as $block ) {
		if ( '' === $block || null === $block ) {
			continue;
		}
		echo $block; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- each element is this file's own pre-escaped HTML (see the neccospeak_agent_ui_ns_only_*_block() functions above), or a cut-and-relabeled legacy admin HTML fragment; never raw user input.
	}
	?>
	</div>
	<?php
}
