<!--
  Git-discovery wizard shared by the Static and Dockerfile source forms.

  Both sources clone a repo via the same provider/owner/repo/branch/token
  path, so this component owns the discovery UX once: provider auto-detect,
  test-connection probe, repo / branch pickers (EntityPicker mounts), and —
  for the static source only — a collapsible folder-tree browser plus Deno
  auto-detect.

  The shared git slice is bound in via `git` (a `GitSourceState` from
  `$lib/workload/sourceForms`). The static-only folder/mode controls are
  opt-in: pass `showFolderTree` + `folderPath`/`mode` bindings to render
  them. The dockerfile form omits those, so its render collapses to the
  provider+repo+branch+token+test block.

  Every discovery call is best-effort: failures show inline status and never
  block the operator from filling fields by hand. AbortController is not used
  here because the legacy inline handlers didn't use it for discovery — they
  guard on `pending` flags + invalidate downstream caches instead, which we
  preserve verbatim so behaviour is identical.
-->
<script lang="ts">
	import type { GitSourceState } from '$lib/workload/sourceForms';
	import type {
		RepoInfo,
		FolderEntry,
		GitProviderKind,
		DiscoveryGitRequest
	} from '$lib/api';
	import type { EntityPickerItem } from '$lib/types';
	import * as api from '$lib/api';
	import EntityPicker from '$lib/components/EntityPicker.svelte';
	import { IconSearch, IconLoader, IconCheck, IconChevronRight, IconX } from '$lib/components/icons';
	import { t } from '$lib/i18n';

	interface Props {
		/** Shared git slice (provider/baseURL/repoOwner/repoName/branch/accessToken). */
		git: GitSourceState;
		/**
		 * Layout variant. `static` renders the wide labelled buttons + the
		 * folder tree; `dockerfile` renders the compact two-column layout the
		 * dockerfile form used. Defaults to `static`.
		 */
		variant?: 'static' | 'dockerfile';
		/** Render the folder-tree browser + folder input (static only). */
		showFolderTree?: boolean;
		/** Bound static folder path (only when showFolderTree). */
		folderPath?: string;
		/** Bound static mode — drives Deno auto-detect (only when showFolderTree). */
		mode?: 'static' | 'deno';
		/** Set true once the user manually flips the mode radio (only static). */
		modeUserOverride?: boolean;
		/**
		 * Out-param: reflects whether a folder tree has been loaded. The static
		 * form gates its "auto-detected Deno" hint on this so the hint only
		 * appears after a real tree probe (matching the legacy
		 * `staticTree.length > 0` guard).
		 */
		treeLoaded?: boolean;
		/**
		 * Transient discovery status. These are normally owned by the wizard's
		 * own internal `$state` (the defaults below), but the create wizard
		 * (`apps/new`) binds them up to the PAGE so the loaded tree + detect/
		 * test pills SURVIVE the source form unmounting under the Advanced-JSON
		 * / source-kind toggles. Optional + internal-fallback by design: the
		 * detail/edit page (`apps/[id]`) binds none of them and keeps the
		 * transient-resets-on-remount behaviour unchanged.
		 */
		tree?: FolderEntry[];
		detectStatus?: 'idle' | 'pending' | 'ok' | 'error';
		detectError?: string;
		testStatus?: 'idle' | 'pending' | 'ok' | 'error';
		testError?: string;
		/** id prefix so labels stay unique if two ever mount. */
		idPrefix?: string;
	}

	let {
		git = $bindable(),
		variant = 'static',
		showFolderTree = false,
		folderPath = $bindable(''),
		mode = $bindable('static'),
		modeUserOverride = $bindable(false),
		treeLoaded = $bindable(false),
		tree = $bindable([]),
		detectStatus = $bindable('idle'),
		detectError = $bindable(''),
		testStatus = $bindable('idle'),
		testError = $bindable(''),
		idPrefix = 'disc'
	}: Props = $props();

	// ── Discovery state ───────────────────────────────────────────────
	// Note: detectStatus/detectError/testStatus/testError/tree are bindable
	// props (declared above) so a parent can hoist them to survive unmount;
	// they default to the wizard's own internal state when unbound.
	let detectedProvider = $state<GitProviderKind>('');

	let showRepoPicker = $state(false);
	let repoItems = $state<EntityPickerItem[]>([]);
	let repoLoading = $state(false);
	let repoError = $state('');

	let showBranchPicker = $state(false);
	let branchItems = $state<EntityPickerItem[]>([]);
	let branchLoading = $state(false);
	let branchError = $state('');

	let treeOpen = $state(false);
	let treeLoading = $state(false);
	let treeError = $state('');
	let expandedDirs = $state<Set<string>>(new Set());

	// Mirror tree-loaded state out to the parent so the static form can gate
	// its "auto-detected Deno" hint on a real tree probe (matching the legacy
	// `staticTree.length > 0` guard).
	$effect(() => {
		treeLoaded = tree.length > 0;
	});

	// Min fields required before discovery calls are safe to fire.
	const discoveryReady = $derived(
		git.baseURL.trim() !== '' && git.repoOwner.trim() !== '' && git.repoName.trim() !== ''
	);

	function buildDiscoveryRequest(): DiscoveryGitRequest {
		return {
			provider: git.provider,
			base_url: git.baseURL.trim(),
			access_token: git.accessToken || undefined,
			repo_owner: git.repoOwner.trim(),
			repo_name: git.repoName.trim(),
			branch: git.branch || undefined
		};
	}

	async function detectProvider() {
		if (!git.baseURL.trim()) return;
		// Guard against rapid double-clicks: the disabled state lags the
		// click handler by a microtask, so check explicitly.
		if (detectStatus === 'pending') return;
		detectStatus = 'pending';
		detectError = '';
		try {
			// The backend returns DetectedGitProvider which is the narrow
			// 'gitea' | 'github' | 'gitlab' union — assignment is direct.
			const res = await api.detectGitProvider(git.baseURL.trim());
			detectedProvider = res.provider;
			git.provider = res.provider;
			detectStatus = 'ok';
		} catch {
			// Discovery errors are surfaced as a friendly, localized message —
			// the backend deliberately returns a generic, non-localized string
			// (it must not echo the upstream provider's body, which can reflect
			// the access token). The real cause is logged server-side.
			detectError = $t('apps.new.errors.detectionFailed');
			detectStatus = 'error';
		}
	}

	async function testConnection() {
		if (!discoveryReady) return;
		if (testStatus === 'pending') return;
		testStatus = 'pending';
		testError = '';
		try {
			await api.testGitConnection(buildDiscoveryRequest());
			testStatus = 'ok';
		} catch {
			testError = $t('apps.new.errors.connectionFailed');
			testStatus = 'error';
		}
	}

	async function openRepoPicker() {
		if (!git.baseURL.trim()) return;
		if (repoLoading) return;
		showRepoPicker = true;
		repoLoading = true;
		repoError = '';
		try {
			const repos: RepoInfo[] = await api.listGitRepos(buildDiscoveryRequest());
			repoItems = repos.map((r) => ({
				value: JSON.stringify({ owner: r.owner, name: r.name }),
				label: r.full_name,
				description: r.description || undefined,
				icon: r.private ? 'lock' : undefined
			}));
		} catch (e) {
			// Surface the failure so an empty picker is not mistaken
			// for "no repos found" — the operator typically needs to
			// fix the base URL or access token to recover.
			repoItems = [];
			repoError = $t('apps.new.errors.reposFailed');
		} finally {
			repoLoading = false;
		}
	}

	function selectRepo(value: string) {
		try {
			const parsed = JSON.parse(value) as { owner: string; name: string };
			git.repoOwner = parsed.owner;
			git.repoName = parsed.name;
		} catch {
			// best-effort; leave fields untouched if value isn't JSON
		}
		showRepoPicker = false;
		// Repo changed → invalidate downstream caches.
		branchItems = [];
		tree = [];
		expandedDirs = new Set();
		testStatus = 'idle';
	}

	async function openBranchPicker() {
		if (!discoveryReady) return;
		if (branchLoading) return;
		showBranchPicker = true;
		branchLoading = true;
		branchError = '';
		try {
			const branches: string[] = await api.listGitBranches(buildDiscoveryRequest());
			branchItems = branches.map((b) => ({
				value: b,
				label: b
			}));
			// Auto-pick main/master if no branch is set yet.
			if (!git.branch || git.branch === 'main') {
				const preferred = branches.find((b) => b === 'main') ?? branches.find((b) => b === 'master');
				if (preferred) git.branch = preferred;
			}
		} catch (e) {
			branchItems = [];
			branchError = $t('apps.new.errors.branchesFailed');
		} finally {
			branchLoading = false;
		}
	}

	function selectBranch(value: string) {
		git.branch = value;
		showBranchPicker = false;
		// Branch changed → invalidate tree cache.
		tree = [];
		expandedDirs = new Set();
	}

	async function toggleTree() {
		treeOpen = !treeOpen;
		if (treeOpen && tree.length === 0 && discoveryReady) {
			await loadTree();
		}
	}

	async function loadTree() {
		treeLoading = true;
		treeError = '';
		try {
			tree = await api.listGitTree(buildDiscoveryRequest());
			autoDetectDenoMode();
		} catch {
			treeError = $t('apps.new.errors.treeFailed');
			tree = [];
		} finally {
			treeLoading = false;
		}
	}

	// Detect a Deno-style site by looking for an `api/` folder under
	// the currently selected folder root. Skipped once the user has
	// manually flipped the mode radio.
	function autoDetectDenoMode() {
		if (modeUserOverride) return;
		const prefix = folderPath ? folderPath + '/api' : 'api';
		const hasApi = tree.some(
			(e) => e.is_dir && (e.path === prefix || e.path.startsWith(prefix + '/'))
		);
		if (hasApi) mode = 'deno';
	}

	// Tree helpers — ported from the legacy /sites/new wizard.
	const folders = $derived(
		tree.filter((e) => e.is_dir).sort((a, b) => a.path.localeCompare(b.path))
	);

	function getTopLevelFolders(): FolderEntry[] {
		return folders.filter((f) => !f.path.includes('/'));
	}

	function getChildFolders(parentPath: string): FolderEntry[] {
		return folders.filter((f) => {
			if (!f.path.startsWith(parentPath + '/')) return false;
			const rest = f.path.slice(parentPath.length + 1);
			return !rest.includes('/');
		});
	}

	function toggleDir(path: string) {
		const next = new Set(expandedDirs);
		if (next.has(path)) {
			next.delete(path);
		} else {
			next.add(path);
		}
		expandedDirs = next;
	}

	function selectFolder(path: string) {
		folderPath = path;
		// Re-run auto-detect against the new selection.
		autoDetectDenoMode();
	}
</script>

{#if variant === 'static'}
	<div class="row">
		<label class="sub" for="{idPrefix}-provider">
			<span class="sub-label">{$t('apps.new.staticProvider')}</span>
			<div class="input-with-button">
				<select id="{idPrefix}-provider" class="input" bind:value={git.provider}>
					<option value="gitea">gitea</option>
					<option value="github">github</option>
					<option value="gitlab">gitlab</option>
				</select>
				<button
					type="button"
					class="discover-btn"
					onclick={detectProvider}
					disabled={!git.baseURL.trim() || detectStatus === 'pending'}
					title={$t('apps.new.staticDetectProvider')}
				>
					{#if detectStatus === 'pending'}
						<IconLoader size={14} />
					{:else}
						<IconSearch size={14} />
					{/if}
					<span class="discover-btn-label">{$t('apps.new.staticDetectProvider')}</span>
				</button>
			</div>
		</label>
		<label class="sub" for="{idPrefix}-base-url">
			<span class="sub-label">{$t('apps.new.staticBaseUrl')}</span>
			<input
				id="{idPrefix}-base-url"
				type="url"
				class="input mono"
				bind:value={git.baseURL}
				placeholder={$t('apps.new.staticBaseUrlPlaceholder')}
				autocomplete="off"
				spellcheck="false"
			/>
		</label>
	</div>
	{#if detectStatus === 'ok'}
		<div class="discover-pill discover-pill-ok">
			<IconCheck size={12} />
			<span>{$t('apps.new.staticDetectedOk', { provider: detectedProvider || git.provider })}</span>
		</div>
	{:else if detectStatus === 'error'}
		<div class="discover-pill discover-pill-bad">
			<IconX size={12} />
			<span>{$t('apps.new.staticDetectedFailed', { error: detectError })}</span>
		</div>
	{/if}
	<div class="row">
		<label class="sub" for="{idPrefix}-owner">
			<span class="sub-label"
				>{$t('apps.new.staticRepoOwner')}<span class="req-star" aria-label={$t('apps.new.fieldRequired')}
					>*</span
				></span
			>
			<input
				id="{idPrefix}-owner"
				type="text"
				class="input mono"
				bind:value={git.repoOwner}
				placeholder={$t('apps.new.staticRepoOwnerPlaceholder')}
				autocomplete="off"
				spellcheck="false"
				required
			/>
		</label>
		<label class="sub" for="{idPrefix}-name">
			<span class="sub-label"
				>{$t('apps.new.staticRepoName')}<span class="req-star" aria-label={$t('apps.new.fieldRequired')}
					>*</span
				></span
			>
			<div class="input-with-button">
				<input
					id="{idPrefix}-name"
					type="text"
					class="input mono"
					bind:value={git.repoName}
					placeholder={$t('apps.new.staticRepoNamePlaceholder')}
					autocomplete="off"
					spellcheck="false"
					required
				/>
				<button
					type="button"
					class="discover-icon-btn"
					onclick={openRepoPicker}
					disabled={!git.baseURL.trim() || repoLoading}
					title={$t('apps.new.staticBrowseRepos')}
					aria-label={$t('apps.new.staticBrowseRepos')}
				>
					{#if repoLoading}
						<IconLoader size={14} />
					{:else}
						<IconSearch size={14} />
					{/if}
				</button>
			</div>
			{#if repoError}
				<span class="discover-pill discover-pill-bad inline">{repoError}</span>
			{/if}
		</label>
	</div>
	<div class="row">
		<label class="sub" for="{idPrefix}-branch">
			<span class="sub-label">{$t('apps.new.staticBranch')}</span>
			<div class="input-with-button">
				<input
					id="{idPrefix}-branch"
					type="text"
					class="input mono"
					bind:value={git.branch}
					placeholder={$t('apps.new.staticBranchPlaceholder')}
					autocomplete="off"
					spellcheck="false"
				/>
				<button
					type="button"
					class="discover-icon-btn"
					onclick={openBranchPicker}
					disabled={!discoveryReady || branchLoading}
					title={$t('apps.new.staticBrowseBranches')}
					aria-label={$t('apps.new.staticBrowseBranches')}
				>
					{#if branchLoading}
						<IconLoader size={14} />
					{:else}
						<IconSearch size={14} />
					{/if}
				</button>
			</div>
			{#if branchError}
				<span class="discover-pill discover-pill-bad inline">{branchError}</span>
			{/if}
		</label>
		{#if showFolderTree}
			<label class="sub" for="{idPrefix}-folder">
				<span class="sub-label">{$t('apps.new.staticFolder')}</span>
				<input
					id="{idPrefix}-folder"
					type="text"
					class="input mono"
					bind:value={folderPath}
					placeholder={$t('apps.new.staticFolderPlaceholder')}
					autocomplete="off"
					spellcheck="false"
				/>
			</label>
		{/if}
	</div>
	{#if showFolderTree}
		<div class="tree-toggle-row">
			<button
				type="button"
				class="editor-chip"
				class:active={treeOpen}
				onclick={toggleTree}
				disabled={!discoveryReady}
				title={$t('apps.new.staticBrowseFolders')}
				aria-expanded={treeOpen}
				aria-controls="{idPrefix}-tree-panel"
			>
				<IconChevronRight size={12} class={treeOpen ? 'tree-chev open' : 'tree-chev'} />
				<span>{$t('apps.new.staticBrowseFolders')}</span>
			</button>
			{#if folderPath}
				<span class="tree-selected mono">
					{$t('apps.new.staticFolderSelectedPrefix')} <strong>{folderPath}</strong>
				</span>
			{/if}
		</div>
		{#if treeOpen}
			<div class="tree-panel" id="{idPrefix}-tree-panel">
				{#if treeLoading}
					<div class="tree-loading">
						<IconLoader size={14} />
						<span>{$t('apps.new.staticTreeLoading')}</span>
					</div>
				{:else if treeError}
					<div class="discover-pill discover-pill-bad">
						<IconX size={12} />
						<span>{treeError}</span>
					</div>
				{:else if folders.length === 0}
					<div class="tree-empty">{$t('apps.new.staticTreeEmpty')}</div>
				{:else}
					<button
						type="button"
						class="tree-row tree-root"
						class:tree-row-selected={folderPath === ''}
						onclick={() => selectFolder('')}
					>
						<span class="tree-indent"></span>
						<span class="tree-label mono">{$t('apps.new.staticFolderRoot')}</span>
					</button>
					{#each getTopLevelFolders() as folder (folder.path)}
						{@const isSelected = folderPath === folder.path}
						{@const isExpanded = expandedDirs.has(folder.path)}
						{@const children = getChildFolders(folder.path)}
						<div class="tree-branch">
							<div class="tree-row-wrap">
								{#if children.length > 0}
									<button
										type="button"
										class="tree-chev-btn"
										onclick={() => toggleDir(folder.path)}
										aria-label={$t('common.toggle')}
									>
										<IconChevronRight size={12} class={isExpanded ? 'tree-chev open' : 'tree-chev'} />
									</button>
								{:else}
									<span class="tree-chev-btn tree-chev-empty"></span>
								{/if}
								<button
									type="button"
									class="tree-row tree-row-leaf"
									class:tree-row-selected={isSelected}
									onclick={() => selectFolder(folder.path)}
								>
									<span class="tree-label mono">{folder.path}</span>
								</button>
							</div>
							{#if isExpanded}
								<div class="tree-children">
									{#each children as child (child.path)}
										{@const childSelected = folderPath === child.path}
										<button
											type="button"
											class="tree-row tree-row-child"
											class:tree-row-selected={childSelected}
											onclick={() => selectFolder(child.path)}
										>
											<span class="tree-label mono">{child.path.split('/').pop()}</span>
										</button>
									{/each}
								</div>
							{/if}
						</div>
					{/each}
				{/if}
			</div>
		{/if}
		<label class="sub" for="{idPrefix}-token">
			<span class="sub-label">{$t('apps.new.staticToken')}</span>
			<input
				id="{idPrefix}-token"
				type="password"
				class="input"
				bind:value={git.accessToken}
				placeholder={$t('apps.new.staticTokenPlaceholder')}
				autocomplete="new-password"
			/>
			<p class="hint">{$t('apps.new.staticTokenHint')}</p>
			<div class="test-row">
				<button
					type="button"
					class="discover-btn"
					onclick={testConnection}
					disabled={!discoveryReady || testStatus === 'pending'}
					title={$t('apps.new.staticTestConnection')}
				>
					{#if testStatus === 'pending'}
						<IconLoader size={14} />
					{:else if testStatus === 'ok'}
						<IconCheck size={14} />
					{:else}
						<IconSearch size={14} />
					{/if}
					<span class="discover-btn-label">{$t('apps.new.staticTestConnection')}</span>
				</button>
				{#if testStatus === 'ok'}
					<span class="discover-pill discover-pill-ok inline">
						<IconCheck size={12} />
						<span>{$t('apps.new.staticConnectionOk')}</span>
					</span>
				{:else if testStatus === 'error'}
					<span class="discover-pill discover-pill-bad inline">
						<IconX size={12} />
						<span>{$t('apps.new.staticConnectionFailed', { error: testError })}</span>
					</span>
				{/if}
			</div>
		</label>
	{/if}
{:else}
	<!-- dockerfile variant: compact two-column layout, no folder tree. -->
	<div class="row">
		<label class="sub" for="{idPrefix}-provider">
			<span class="sub-label">{$t('apps.new.staticProvider')}</span>
			<div class="input-with-button">
				<select id="{idPrefix}-provider" class="input" bind:value={git.provider}>
					<option value="gitea">gitea</option>
					<option value="github">github</option>
					<option value="gitlab">gitlab</option>
				</select>
				<button
					type="button"
					class="discover-btn"
					onclick={detectProvider}
					disabled={!git.baseURL.trim() || detectStatus === 'pending'}
					title={$t('apps.new.staticDetectProvider')}
				>
					{#if detectStatus === 'pending'}
						<IconLoader size={14} />
					{:else}
						<IconSearch size={14} />
					{/if}
					<span class="discover-btn-label">{$t('apps.new.staticDetectProvider')}</span>
				</button>
			</div>
		</label>
		<label class="sub" for="{idPrefix}-base-url">
			<span class="sub-label">{$t('apps.new.staticBaseUrl')}</span>
			<input
				id="{idPrefix}-base-url"
				type="url"
				class="input mono"
				bind:value={git.baseURL}
				placeholder={$t('apps.new.staticBaseUrlPlaceholder')}
				autocomplete="off"
				spellcheck="false"
			/>
		</label>
	</div>
	{#if detectStatus === 'ok'}
		<div class="discover-pill discover-pill-ok">
			<IconCheck size={12} />
			<span>{$t('apps.new.staticDetectedOk', { provider: detectedProvider || git.provider })}</span>
		</div>
	{:else if detectStatus === 'error'}
		<div class="discover-pill discover-pill-bad">
			<IconX size={12} />
			<span>{$t('apps.new.staticDetectedFailed', { error: detectError })}</span>
		</div>
	{/if}
	<div class="row">
		<label class="sub" for="{idPrefix}-owner">
			<span class="sub-label"
				>{$t('apps.new.staticRepoOwner')}<span class="req-star" aria-label={$t('apps.new.fieldRequired')}
					>*</span
				></span
			>
			<input
				id="{idPrefix}-owner"
				type="text"
				class="input mono"
				bind:value={git.repoOwner}
				placeholder={$t('apps.new.staticRepoOwnerPlaceholder')}
				autocomplete="off"
				spellcheck="false"
				required
			/>
		</label>
		<label class="sub" for="{idPrefix}-name">
			<span class="sub-label"
				>{$t('apps.new.staticRepoName')}<span class="req-star" aria-label={$t('apps.new.fieldRequired')}
					>*</span
				></span
			>
			<div class="input-with-button">
				<input
					id="{idPrefix}-name"
					type="text"
					class="input mono"
					bind:value={git.repoName}
					placeholder={$t('apps.new.staticRepoNamePlaceholder')}
					autocomplete="off"
					spellcheck="false"
					required
				/>
				<button
					type="button"
					class="discover-btn"
					onclick={openRepoPicker}
					disabled={!git.baseURL.trim() || repoLoading}
					title={$t('apps.new.staticPickerRepoTitle')}
				>
					{#if repoLoading}
						<IconLoader size={14} />
					{:else}
						<IconSearch size={14} />
					{/if}
					<span class="discover-btn-label">{$t('apps.new.staticBrowseRepos')}</span>
				</button>
			</div>
		</label>
	</div>
	<div class="row">
		<label class="sub" for="{idPrefix}-branch">
			<span class="sub-label">{$t('apps.new.staticBranch')}</span>
			<div class="input-with-button">
				<input
					id="{idPrefix}-branch"
					type="text"
					class="input mono"
					bind:value={git.branch}
					placeholder="main"
					autocomplete="off"
					spellcheck="false"
				/>
				<button
					type="button"
					class="discover-btn"
					onclick={openBranchPicker}
					disabled={!discoveryReady || branchLoading}
					title={$t('apps.new.staticPickerBranchTitle')}
				>
					{#if branchLoading}
						<IconLoader size={14} />
					{:else}
						<IconChevronRight size={14} />
					{/if}
					<span class="discover-btn-label">{$t('apps.new.staticBrowseBranches')}</span>
				</button>
			</div>
		</label>
		<label class="sub" for="{idPrefix}-token">
			<span class="sub-label">{$t('apps.new.staticToken')}</span>
			<input
				id="{idPrefix}-token"
				type="password"
				class="input mono"
				bind:value={git.accessToken}
				placeholder={$t('apps.new.staticTokenPlaceholder')}
				autocomplete="off"
				spellcheck="false"
			/>
		</label>
	</div>
	<div class="row">
		<button
			type="button"
			class="discover-btn"
			onclick={testConnection}
			disabled={!discoveryReady || testStatus === 'pending'}
			title={$t('apps.new.staticTestConnection')}
		>
			{#if testStatus === 'pending'}
				<IconLoader size={14} />
			{:else}
				<IconCheck size={14} />
			{/if}
			<span class="discover-btn-label">{$t('apps.new.staticTestConnection')}</span>
		</button>
		{#if testStatus === 'ok'}
			<div class="discover-pill discover-pill-ok">
				<IconCheck size={12} />
				<span>{$t('apps.new.staticConnectionOk')}</span>
			</div>
		{:else if testStatus === 'error'}
			<div class="discover-pill discover-pill-bad">
				<IconX size={12} />
				<span>{$t('apps.new.staticConnectionFailed', { error: testError })}</span>
			</div>
		{/if}
	</div>
{/if}

<!-- Shared repo + branch pickers — one mount each regardless of variant.
     The picker bodies only render when their `open` flag is true, so this
     is invisible at rest. -->
<EntityPicker
	bind:open={showRepoPicker}
	items={repoItems}
	current={git.repoOwner && git.repoName
		? JSON.stringify({ owner: git.repoOwner, name: git.repoName })
		: ''}
	title={$t('apps.new.staticPickerRepoTitle')}
	placeholder={$t('apps.new.staticPickerRepoPlaceholder')}
	onselect={selectRepo}
	onclose={() => (showRepoPicker = false)}
/>
<EntityPicker
	bind:open={showBranchPicker}
	items={branchItems}
	current={git.branch}
	title={$t('apps.new.staticPickerBranchTitle')}
	placeholder={$t('apps.new.staticPickerBranchPlaceholder')}
	onselect={selectBranch}
	onclose={() => (showBranchPicker = false)}
/>

<style>
	.input {
		width: 100%;
		background: var(--surface-input);
		border: 1px solid var(--border-input);
		border-radius: var(--radius-lg);
		padding: 0.6rem 0.8rem;
		font-size: 0.92rem;
		color: var(--text-primary);
		font-family: inherit;
		outline: none;
		transition: border-color 120ms ease, box-shadow 120ms ease;
	}
	.input:focus {
		border-color: var(--border-focus);
		box-shadow: 0 0 0 3px var(--accent-soft);
	}
	.input:focus-visible {
		outline: none;
	}
	.hint {
		font-size: 0.78rem;
		color: var(--text-tertiary);
		margin: 0;
		line-height: 1.45;
	}
	.row {
		display: grid;
		grid-template-columns: 1fr 1fr;
		gap: 0.9rem;
	}
	@media (max-width: 600px) {
		.row {
			grid-template-columns: 1fr;
		}
	}
	.sub {
		display: flex;
		flex-direction: column;
		gap: 0.35rem;
	}
	.sub-label {
		font-family: var(--forge-mono);
		font-size: 0.62rem;
		font-weight: 600;
		letter-spacing: 0.14em;
		text-transform: uppercase;
		color: var(--text-secondary);
	}
	/* Required-field marker — same danger hue as the page-level `.req`
	   badge, rendered as a compact asterisk. */
	.req-star {
		margin-left: 0.2rem;
		color: var(--color-danger);
		font-weight: 700;
	}
	.editor-chip {
		background: var(--surface-card);
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-md);
		padding: 0.22rem 0.55rem;
		font-family: var(--forge-mono);
		font-size: 0.6rem;
		font-weight: 600;
		letter-spacing: 0.1em;
		text-transform: uppercase;
		color: var(--text-secondary);
		cursor: pointer;
		transition: all 120ms ease;
	}
	.editor-chip:hover {
		border-color: var(--color-brand-400);
		color: var(--text-primary);
		background: var(--surface-card-hover);
	}
	.editor-chip.active {
		border-color: var(--forge-accent);
		background: var(--forge-accent);
		color: var(--surface-card);
	}
	.editor-chip.active:hover {
		background: var(--forge-accent);
		color: var(--surface-card);
	}
	.editor-chip:focus-visible {
		outline: 2px solid var(--border-focus);
		outline-offset: 2px;
	}

	/* ── Static discovery controls ───────────────────── */
	.input-with-button {
		display: flex;
		align-items: stretch;
		gap: 0.4rem;
	}
	.input-with-button > .input,
	.input-with-button > select.input {
		flex: 1;
		min-width: 0;
	}
	.discover-btn {
		display: inline-flex;
		align-items: center;
		gap: 0.4rem;
		padding: 0 0.7rem;
		background: var(--surface-card);
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-md);
		font-family: var(--forge-mono);
		font-size: 0.6rem;
		font-weight: 600;
		letter-spacing: 0.1em;
		text-transform: uppercase;
		color: var(--text-secondary);
		cursor: pointer;
		transition: all 120ms ease;
		white-space: nowrap;
	}
	.discover-btn:hover:not(:disabled) {
		border-color: var(--color-brand-400);
		color: var(--text-primary);
		background: var(--surface-card-hover);
	}
	.discover-btn:disabled {
		opacity: 0.5;
		cursor: not-allowed;
	}
	.discover-btn:focus-visible {
		outline: 2px solid var(--border-focus);
		outline-offset: 2px;
	}
	.discover-btn-label {
		line-height: 1;
	}
	.discover-icon-btn {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		width: 36px;
		background: var(--surface-card);
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-md);
		color: var(--text-secondary);
		cursor: pointer;
		transition: all 120ms ease;
	}
	.discover-icon-btn:hover:not(:disabled) {
		border-color: var(--color-brand-400);
		color: var(--text-primary);
		background: var(--surface-card-hover);
	}
	.discover-icon-btn:disabled {
		opacity: 0.5;
		cursor: not-allowed;
	}
	.discover-icon-btn:focus-visible {
		outline: 2px solid var(--border-focus);
		outline-offset: 2px;
	}

	/* Status pills — green/red mirroring .foot-status. */
	.discover-pill {
		display: inline-flex;
		align-items: center;
		gap: 0.35rem;
		padding: 0.28rem 0.55rem;
		border-radius: var(--radius-md);
		font-family: var(--forge-mono);
		font-size: 0.62rem;
		font-weight: 600;
		letter-spacing: 0.06em;
		line-height: 1;
		align-self: flex-start;
	}
	.discover-pill.inline {
		align-self: center;
	}
	.discover-pill-ok {
		background: color-mix(in srgb, var(--color-success) 14%, transparent);
		color: var(--color-success-dark);
		border: 1px solid color-mix(in srgb, var(--color-success) 40%, transparent);
	}
	.discover-pill-bad {
		background: color-mix(in srgb, var(--color-danger) 14%, transparent);
		color: var(--color-danger-dark);
		border: 1px solid color-mix(in srgb, var(--color-danger) 40%, transparent);
	}
	:global([data-theme='dark']) .discover-pill-ok {
		color: #86efac;
	}
	:global([data-theme='dark']) .discover-pill-bad {
		color: #fca5a5;
	}

	.test-row {
		display: flex;
		align-items: center;
		gap: 0.55rem;
		flex-wrap: wrap;
		margin-top: 0.25rem;
	}

	/* Folder tree picker — inline collapsible panel below the folder_path
	   input. Compact monospaced rows; chevron rotates 90deg when expanded. */
	.tree-toggle-row {
		display: flex;
		align-items: center;
		gap: 0.55rem;
		flex-wrap: wrap;
	}
	.tree-toggle-row .editor-chip {
		display: inline-flex;
		align-items: center;
		gap: 0.35rem;
	}
	.tree-selected {
		font-size: 0.7rem;
		color: var(--text-tertiary);
		letter-spacing: 0.02em;
	}
	.tree-selected strong {
		color: var(--text-primary);
	}
	:global(.tree-chev) {
		transition: transform 120ms ease;
	}
	:global(.tree-chev.open) {
		transform: rotate(90deg);
	}
	.tree-panel {
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-md);
		background: var(--surface-card);
		padding: 0.45rem;
		max-height: 260px;
		overflow-y: auto;
		display: flex;
		flex-direction: column;
		gap: 0.1rem;
	}
	.tree-loading,
	.tree-empty {
		display: inline-flex;
		align-items: center;
		gap: 0.4rem;
		padding: 0.6rem 0.5rem;
		font-family: var(--forge-mono);
		font-size: 0.7rem;
		color: var(--text-tertiary);
		letter-spacing: 0.04em;
	}
	.tree-row {
		display: flex;
		align-items: center;
		gap: 0.4rem;
		width: 100%;
		padding: 0.32rem 0.5rem;
		background: transparent;
		border: 1px solid transparent;
		border-radius: var(--radius-sm);
		text-align: left;
		font-size: 0.78rem;
		color: var(--text-secondary);
		cursor: pointer;
		transition: background 100ms ease, border-color 100ms ease, color 100ms ease;
	}
	.tree-row:hover {
		background: var(--surface-card-hover);
		color: var(--text-primary);
	}
	.tree-row-selected {
		background: var(--forge-accent-soft);
		border-color: var(--forge-accent);
		color: var(--text-primary);
	}
	.tree-row-leaf {
		flex: 1;
	}
	.tree-row-child {
		margin-left: 1.4rem;
	}
	.tree-root {
		border-bottom: 1px dashed var(--border-primary);
		border-radius: 0;
		margin-bottom: 0.25rem;
	}
	.tree-root.tree-row-selected {
		border-bottom-color: var(--forge-accent);
	}
	.tree-indent {
		display: inline-block;
		width: 18px;
	}
	.tree-row-wrap {
		display: flex;
		align-items: center;
		gap: 0.25rem;
	}
	.tree-chev-btn {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		width: 18px;
		height: 18px;
		background: transparent;
		border: 0;
		color: var(--text-tertiary);
		cursor: pointer;
		border-radius: var(--radius-sm);
	}
	.tree-chev-btn:hover {
		color: var(--text-primary);
		background: var(--surface-card-hover);
	}
	.tree-chev-empty {
		cursor: default;
		pointer-events: none;
	}
	.tree-branch {
		display: flex;
		flex-direction: column;
		gap: 0.05rem;
	}
	.tree-children {
		display: flex;
		flex-direction: column;
		gap: 0.05rem;
		padding-left: 0;
	}
	.tree-label {
		flex: 1;
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}
</style>
