<script lang="ts">
	/**
	 * GitOpsPanel
	 *
	 * Config-as-code for dockerfile/static workloads. The repo's
	 * `.tinyforge.yml` declares a small overlay (port / healthcheck /
	 * deploy_strategy); this panel shows whether the live source_config
	 * matches that file (drift), previews the file, and applies it on demand
	 * via "Sync now" (validate-then-commit on the server).
	 *
	 * Self-contained: it fetches its own GET /gitops on mount and on
	 * workloadId change, and owns the enable/disable toggle. On a successful
	 * sync it both re-fetches its own status AND calls `onSynced` so the parent
	 * page reloads the (now-changed) workload row.
	 *
	 * The `.panel` / `.reg` card chrome is declared locally — Svelte scopes the
	 * detail page's panel styles to that route, so a child component must carry
	 * its own copy to render the forge card frame.
	 */
	import * as api from '$lib/api';
	import { t } from '$lib/i18n';
	import { toasts } from '$lib/stores/toast';
	import { fmt } from '$lib/format/datetime';
	import ToggleSwitch from './ToggleSwitch.svelte';
	import ConfirmDialog from './ConfirmDialog.svelte';
	import { IconRefresh, IconCheck, IconCopy } from './icons';

	interface Props {
		workloadId: string;
		sourceKind: string;
		isAdmin?: boolean;
		/** Called after a successful sync so the parent reloads the workload. */
		onSynced?: () => void;
	}
	// Default false: the admin affordances (enable toggle, Sync) stay hidden
	// unless the parent explicitly proves the viewer is an admin. The server
	// also gates PUT/POST with AdminOnly, so this is defense-in-depth.
	let { workloadId, sourceKind, isAdmin = false, onSynced }: Props = $props();

	let gitops = $state<api.GitOpsStatus | null>(null);
	let loading = $state(true);
	let error = $state('');
	let togglePending = $state(false);
	let syncing = $state(false);
	let confirmSync = $state(false);
	let copied = $state(false);

	// Eligibility is decided client-side from the source kind so the panel can
	// render-nothing instantly. (The server also reports `eligible`; the two
	// always agree.) dockerfile + static are the git-backed sources.
	const ELIGIBLE_KINDS = ['dockerfile', 'static'];
	const eligibleByKind = $derived(ELIGIBLE_KINDS.includes(sourceKind));

	async function load(signal?: AbortSignal): Promise<void> {
		try {
			gitops = await api.fetchWorkloadGitOps(workloadId, signal);
			error = '';
		} catch (e) {
			if (e instanceof DOMException && e.name === 'AbortError') return;
			error = e instanceof Error ? e.message : String(e);
		} finally {
			loading = false;
		}
	}

	// Reload whenever workloadId changes — the parent reuses this instance
	// across /apps/A → /apps/B navigation.
	$effect(() => {
		void workloadId;
		loading = true;
		const controller = new AbortController();
		load(controller.signal);
		return () => controller.abort();
	});

	async function onToggle(next: boolean): Promise<void> {
		if (togglePending || !gitops) return;
		togglePending = true;
		const path = gitops.path || '.tinyforge.yml';
		try {
			await api.setWorkloadGitOps(workloadId, { enabled: next, path });
			toasts.success(
				next ? $t('apps.detail.gitops.enabledToast') : $t('apps.detail.gitops.disabledToast')
			);
			await load();
			onSynced?.();
		} catch (e) {
			toasts.error(e instanceof Error ? e.message : $t('apps.detail.gitops.toggleFailed'));
			// Reload so the switch reflects the persisted (unchanged) value.
			await load();
		} finally {
			togglePending = false;
		}
	}

	async function doSync(): Promise<void> {
		if (syncing) return;
		syncing = true;
		try {
			const res = await api.syncWorkloadGitOps(workloadId);
			toasts.success(
				$t('apps.detail.gitops.syncedToast', {
					count: String(res.applied_fields.length),
					sha: shortSha(res.commit_sha)
				})
			);
			await load();
			onSynced?.();
		} catch (e) {
			toasts.error(e instanceof Error ? e.message : $t('apps.detail.gitops.syncFailed'));
		} finally {
			syncing = false;
			confirmSync = false;
		}
	}

	async function copyRaw(): Promise<void> {
		if (!gitops?.raw) return;
		try {
			await navigator.clipboard.writeText(gitops.raw);
			copied = true;
			setTimeout(() => (copied = false), 1500);
		} catch {
			// Clipboard unavailable (insecure context) — silently no-op; the
			// preview is visible and selectable regardless.
		}
	}

	function shortSha(sha: string): string {
		if (!sha) return '—';
		return /^[0-9a-f]{8,}$/i.test(sha) ? sha.slice(0, 10) : sha;
	}

	// Pill descriptor: status tone + label drive the header pill. "ok" splits
	// into in-sync (0 drift) vs N-changes so the single most important signal —
	// "does live match the repo?" — reads at a glance.
	type PillTone = 'sync' | 'drift' | 'muted' | 'warn' | 'danger';
	const pill = $derived.by((): { tone: PillTone; label: string } => {
		if (!gitops || !gitops.enabled) {
			return { tone: 'muted', label: $t('apps.detail.gitops.pillDisabled') };
		}
		switch (gitops.status) {
			case 'ok':
				return gitops.drift_count === 0
					? { tone: 'sync', label: $t('apps.detail.gitops.pillSynced') }
					: {
							tone: 'drift',
							label:
								gitops.drift_count === 1
									? $t('apps.detail.gitops.pillChangesOne')
									: $t('apps.detail.gitops.pillChangesMany', {
											count: String(gitops.drift_count)
										})
						};
			case 'no_file':
				return { tone: 'warn', label: $t('apps.detail.gitops.pillNoFile') };
			case 'fetch_failed':
				return { tone: 'danger', label: $t('apps.detail.gitops.pillFetchFailed') };
			case 'invalid':
				return { tone: 'danger', label: $t('apps.detail.gitops.pillInvalid') };
			default:
				return { tone: 'muted', label: $t('apps.detail.gitops.pillDisabled') };
		}
	});

	const isOK = $derived(gitops?.status === 'ok');
	const inSync = $derived(isOK && (gitops?.drift_count ?? 0) === 0);
	const hasDrift = $derived(isOK && (gitops?.drift_count ?? 0) > 0);
	const lastSync = $derived(gitops?.last_sync_at ?? '');

	// Human label for a managed source_config key.
	function fieldLabel(key: string): string {
		const k = `apps.detail.gitops.field.${key}`;
		const label = $t(k);
		// $t returns the key verbatim when missing — fall back to the raw key.
		return label === k ? key : label;
	}
</script>

{#if eligibleByKind}
	<section class="panel gp-panel" aria-labelledby="gp-heading">
		<span class="reg reg-tl" aria-hidden="true"></span>
		<span class="reg reg-tr" aria-hidden="true"></span>
		<span class="reg reg-bl" aria-hidden="true"></span>
		<span class="reg reg-br" aria-hidden="true"></span>

		<header class="panel-head">
			<div class="gp-titlewrap">
				<h2 class="panel-title" id="gp-heading">
					{$t('apps.detail.gitops.title')}<span class="title-accent">.</span>
				</h2>
				<span class="panel-sub">{$t('apps.detail.gitops.sub')}</span>
			</div>

			<span class="gp-pill gp-pill-{pill.tone}">
				{#if pill.tone === 'sync'}
					<IconCheck size={11} />
				{:else}
					<span class="gp-pill-dot" aria-hidden="true"></span>
				{/if}
				{pill.label}
			</span>

			{#if isAdmin}
				<div class="gp-toggle" title={$t('apps.detail.gitops.toggleHint')}>
					<span class="gp-toggle-lbl">{$t('apps.detail.gitops.toggleLabel')}</span>
					<ToggleSwitch
						checked={gitops?.enabled ?? false}
						disabled={togglePending || loading}
						ariaLabel={$t('apps.detail.gitops.toggleAria')}
						onchange={onToggle}
					/>
				</div>
			{/if}
		</header>

		{#if error}
			<div class="alert inline-alert" role="alert">
				<span class="alert-tag">ERR</span><span>{error}</span>
			</div>
		{/if}

		{#if loading && !gitops}
			<p class="gp-hint">{$t('apps.detail.gitops.loading')}</p>
		{:else if gitops}
			{#if !gitops.enabled}
				<!-- Disabled: calm one-liner + path the file is expected at. -->
				<div class="gp-empty">
					<p class="gp-empty-lead">{$t('apps.detail.gitops.disabledLead')}</p>
					<p class="gp-empty-sub">
						{$t('apps.detail.gitops.disabledSub')}
						<code class="gp-path">{gitops.path || '.tinyforge.yml'}</code>
					</p>
				</div>
			{:else}
				<!-- Meta row: path · short sha · last sync. -->
				<div class="gp-meta">
					<span class="gp-meta-item">
						<span class="gp-meta-k">{$t('apps.detail.gitops.metaPath')}</span>
						<code class="gp-path">{gitops.path || '.tinyforge.yml'}</code>
					</span>
					{#if gitops.commit_sha}
						<span class="gp-meta-sep" aria-hidden="true">·</span>
						<span class="gp-meta-item">
							<span class="gp-meta-k">{$t('apps.detail.gitops.metaCommit')}</span>
							<code class="gp-sha" title={gitops.commit_sha}>{shortSha(gitops.commit_sha)}</code>
						</span>
					{/if}
					<span class="gp-meta-sep" aria-hidden="true">·</span>
					<span class="gp-meta-item">
						<span class="gp-meta-k">{$t('apps.detail.gitops.metaLastSync')}</span>
						{#if lastSync}
							<span class="gp-meta-v" title={$fmt.dateTime(lastSync)}>{$fmt.relative(lastSync)}</span>
						{:else}
							<span class="gp-meta-v gp-muted">{$t('apps.detail.gitops.metaNeverSynced')}</span>
						{/if}
					</span>
				</div>

				<!-- ── THE DRIFT VIEW ─────────────────────────────────
				     Field-level repo-vs-live diff. Purpose-built: one row per
				     declared field, repo value on the left, live value on the
				     right, with a connective glyph that turns amber when the
				     two differ. In-sync collapses to a single confident gitops. -->
				{#if isOK}
					{#if inSync}
						<div class="gp-insync" role="status">
							<span class="gp-insync-icon" aria-hidden="true"><IconCheck size={16} /></span>
							<div class="gp-insync-text">
								<strong>{$t('apps.detail.gitops.inSyncTitle')}</strong>
								<span>{$t('apps.detail.gitops.inSyncSub')}</span>
							</div>
						</div>
					{:else}
						<div class="gp-drift" aria-label={$t('apps.detail.gitops.driftAria')}>
							<div class="gp-drift-head" aria-hidden="true">
								<span class="gp-col-field">{$t('apps.detail.gitops.driftColField')}</span>
								<span class="gp-col gp-col-repo">{$t('apps.detail.gitops.driftColRepo')}</span>
								<span class="gp-col-arrow"></span>
								<span class="gp-col gp-col-live">{$t('apps.detail.gitops.driftColLive')}</span>
							</div>
							{#each gitops.drift as d (d.field)}
								<div class="gp-drift-row">
									<span class="gp-field">{fieldLabel(d.field)}</span>
									<span class="gp-val gp-val-repo" title={d.repo_value}>{d.repo_value || '—'}</span>
									<span class="gp-arrow" aria-hidden="true">→</span>
									<span class="gp-val gp-val-live" title={d.live_value}>{d.live_value || '—'}</span>
								</div>
							{/each}
							<p class="gp-drift-foot">{$t('apps.detail.gitops.driftFoot')}</p>
						</div>
					{/if}
				{:else if gitops.status === 'no_file'}
					<div class="gp-status-note gp-note-warn">
						<p class="gp-note-lead">{$t('apps.detail.gitops.noFileLead')}</p>
						<p class="gp-note-sub">
							{$t('apps.detail.gitops.noFileSub')}
							<code class="gp-path">{gitops.path || '.tinyforge.yml'}</code>
						</p>
					</div>
				{:else if gitops.status === 'fetch_failed'}
					<div class="gp-status-note gp-note-danger">
						<p class="gp-note-lead">{$t('apps.detail.gitops.fetchFailedLead')}</p>
						{#if gitops.message}<p class="gp-note-sub">{gitops.message}</p>{/if}
					</div>
				{:else if gitops.status === 'invalid'}
					<div class="gp-status-note gp-note-danger">
						<p class="gp-note-lead">{$t('apps.detail.gitops.invalidLead')}</p>
						{#if gitops.message}<p class="gp-note-sub gp-mono">{gitops.message}</p>{/if}
					</div>
				{/if}

				<!-- ── Rendered .tinyforge.yml preview (when present) ──── -->
				{#if gitops.raw}
					<div class="gp-editor">
						<div class="gp-editor-head">
							<span class="gp-dot"></span><span class="gp-dot"></span><span class="gp-dot"></span>
							<span class="gp-editor-title">{gitops.path || '.tinyforge.yml'}</span>
							<span class="gp-spacer"></span>
							<button
								type="button"
								class="gp-editor-chip"
								onclick={copyRaw}
								title={$t('apps.detail.gitops.copyAria')}
							>
								{#if copied}
									<IconCheck size={12} />{$t('apps.detail.gitops.copied')}
								{:else}
									<IconCopy size={12} />{$t('apps.detail.gitops.copy')}
								{/if}
							</button>
						</div>
						<pre class="gp-code" aria-label={gitops.path || '.tinyforge.yml'}>{gitops.raw}</pre>
					</div>
				{/if}

				<!-- ── Sync action (admin) ─────────────────────────────── -->
				{#if isAdmin && (isOK || gitops.status === 'no_file')}
					<div class="gp-actions">
						<p class="gp-actions-hint">
							{hasDrift
								? $t('apps.detail.gitops.syncHintDrift')
								: $t('apps.detail.gitops.syncHintClean')}
						</p>
						<button
							class="forge-btn"
							onclick={() => (confirmSync = true)}
							disabled={syncing || !isOK}
						>
							<IconRefresh size={13} />
							<span>{syncing ? $t('apps.detail.gitops.syncing') : $t('apps.detail.gitops.syncNow')}</span>
						</button>
					</div>
				{/if}
			{/if}
		{/if}
	</section>
{/if}

{#if confirmSync}
	<ConfirmDialog
		open={true}
		title={$t('apps.detail.gitops.confirmTitle')}
		message={$t('apps.detail.gitops.confirmMessage')}
		confirmLabel={$t('apps.detail.gitops.syncNow')}
		confirmVariant="primary"
		onconfirm={doSync}
		oncancel={() => (confirmSync = false)}
	/>
{/if}

<style>
	/* ── Card chrome (page-scoped on the detail route; carried locally so
	     this child renders the forge frame on its own). ───────────────── */
	.gp-panel {
		--accent: var(--forge-accent);
		--accent-soft: var(--forge-accent-soft);
		position: relative;
		background: var(--surface-card);
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-2xl);
		padding: 1.25rem 1.5rem 1.4rem;
		display: flex;
		flex-direction: column;
		gap: 1rem;
		margin-top: 1rem;
	}
	.reg {
		position: absolute;
		width: 10px;
		height: 10px;
		border-color: var(--color-brand-500);
		border-style: solid;
		border-width: 0;
		pointer-events: none;
	}
	.reg-tl {
		top: -1px;
		left: -1px;
		border-top-width: 2px;
		border-left-width: 2px;
		border-top-left-radius: var(--radius-2xl);
	}
	.reg-tr {
		top: -1px;
		right: -1px;
		border-top-width: 2px;
		border-right-width: 2px;
		border-top-right-radius: var(--radius-2xl);
	}
	.reg-bl {
		bottom: -1px;
		left: -1px;
		border-bottom-width: 2px;
		border-left-width: 2px;
		border-bottom-left-radius: var(--radius-2xl);
	}
	.reg-br {
		bottom: -1px;
		right: -1px;
		border-bottom-width: 2px;
		border-right-width: 2px;
		border-bottom-right-radius: var(--radius-2xl);
	}

	.panel-head {
		display: flex;
		align-items: center;
		gap: 0.7rem;
		flex-wrap: wrap;
	}
	.gp-titlewrap {
		display: flex;
		flex-direction: column;
		gap: 0.15rem;
		min-width: 0;
	}
	.panel-title {
		font-family: var(--font-family-sans);
		font-size: 0.98rem;
		font-weight: 700;
		letter-spacing: -0.005em;
		color: var(--text-primary);
		margin: 0;
	}
	.title-accent {
		color: var(--accent);
		font-weight: 700;
	}
	.panel-sub {
		font-size: 0.78rem;
		color: var(--text-tertiary);
	}

	/* ── Status pill ─────────────────────────────────────────────── */
	.gp-pill {
		display: inline-flex;
		align-items: center;
		gap: 0.32rem;
		margin-left: auto;
		padding: 0.22rem 0.6rem;
		border-radius: var(--radius-full);
		font-family: var(--forge-mono);
		font-size: 0.66rem;
		font-weight: 600;
		letter-spacing: 0.04em;
		text-transform: uppercase;
		border: 1px solid transparent;
		white-space: nowrap;
	}
	.gp-pill-dot {
		width: 6px;
		height: 6px;
		border-radius: 50%;
		background: currentColor;
	}
	.gp-pill-sync {
		color: var(--color-success-dark);
		background: color-mix(in srgb, var(--color-success) 12%, transparent);
		border-color: color-mix(in srgb, var(--color-success) 35%, transparent);
	}
	.gp-pill-drift {
		color: var(--color-brand-700);
		background: var(--forge-accent-soft);
		border-color: color-mix(in srgb, var(--color-brand-500) 38%, transparent);
	}
	.gp-pill-warn {
		color: var(--color-warning-dark);
		background: color-mix(in srgb, var(--color-warning) 12%, transparent);
		border-color: color-mix(in srgb, var(--color-warning) 32%, transparent);
	}
	.gp-pill-danger {
		color: var(--color-danger);
		background: color-mix(in srgb, var(--color-danger) 10%, transparent);
		border-color: color-mix(in srgb, var(--color-danger) 32%, transparent);
	}
	.gp-pill-muted {
		color: var(--text-tertiary);
		background: var(--surface-card-hover);
		border-color: var(--border-primary);
	}

	/* ── Enable toggle ───────────────────────────────────────────── */
	.gp-toggle {
		display: inline-flex;
		align-items: center;
		gap: 0.5rem;
	}
	.gp-toggle-lbl {
		font-family: var(--forge-mono);
		font-size: 0.62rem;
		letter-spacing: 0.08em;
		text-transform: uppercase;
		color: var(--text-tertiary);
	}

	.gp-hint {
		font-size: 0.74rem;
		color: var(--text-tertiary);
		margin: 0.2rem 0;
	}

	/* ── Disabled / empty ────────────────────────────────────────── */
	.gp-empty {
		padding: 0.4rem 0 0.1rem;
	}
	.gp-empty-lead {
		font-size: 0.82rem;
		color: var(--text-secondary);
		margin: 0 0 0.25rem;
	}
	.gp-empty-sub {
		font-size: 0.74rem;
		color: var(--text-tertiary);
		margin: 0;
		display: flex;
		align-items: center;
		gap: 0.4rem;
		flex-wrap: wrap;
	}

	/* ── Meta row ────────────────────────────────────────────────── */
	.gp-meta {
		display: flex;
		align-items: center;
		gap: 0.5rem;
		flex-wrap: wrap;
		font-size: 0.73rem;
		color: var(--text-secondary);
	}
	.gp-meta-item {
		display: inline-flex;
		align-items: center;
		gap: 0.35rem;
	}
	.gp-meta-k {
		color: var(--text-tertiary);
		font-size: 0.66rem;
		letter-spacing: 0.04em;
		text-transform: uppercase;
		font-family: var(--forge-mono);
	}
	.gp-meta-sep {
		color: var(--text-tertiary-soft);
	}
	.gp-muted,
	.gp-meta-v.gp-muted {
		color: var(--text-tertiary);
	}
	.gp-path,
	.gp-sha {
		font-family: var(--forge-mono);
		font-size: 0.7rem;
		padding: 0.08rem 0.38rem;
		border-radius: var(--radius-sm);
		background: var(--surface-card-hover);
		border: 1px solid var(--border-primary);
		color: var(--text-secondary);
	}

	/* ── In-sync confident gitops ─────────────────────────────────── */
	.gp-insync {
		display: flex;
		align-items: center;
		gap: 0.7rem;
		padding: 0.85rem 1rem;
		border-radius: var(--radius-lg);
		background: color-mix(in srgb, var(--color-success) 7%, var(--surface-card));
		border: 1px solid color-mix(in srgb, var(--color-success) 28%, transparent);
	}
	.gp-insync-icon {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		width: 28px;
		height: 28px;
		flex-shrink: 0;
		border-radius: 50%;
		color: #fff;
		background: var(--color-success);
	}
	.gp-insync-text {
		display: flex;
		flex-direction: column;
		gap: 0.1rem;
	}
	.gp-insync-text strong {
		font-size: 0.82rem;
		color: var(--text-primary);
	}
	.gp-insync-text span {
		font-size: 0.72rem;
		color: var(--text-secondary);
	}

	/* ── THE DRIFT VIEW ──────────────────────────────────────────── */
	.gp-drift {
		border: 1px solid color-mix(in srgb, var(--color-brand-500) 28%, var(--border-primary));
		border-radius: var(--radius-lg);
		overflow: hidden;
		background: var(--surface-card);
	}
	.gp-drift-head {
		display: grid;
		grid-template-columns: minmax(7rem, 0.9fr) 1fr 1.6rem 1fr;
		align-items: center;
		gap: 0.5rem;
		padding: 0.45rem 0.85rem;
		background: var(--forge-accent-soft);
		border-bottom: 1px solid color-mix(in srgb, var(--color-brand-500) 22%, transparent);
		font-family: var(--forge-mono);
		font-size: 0.6rem;
		letter-spacing: 0.07em;
		text-transform: uppercase;
		color: var(--text-tertiary);
	}
	.gp-col-repo {
		color: var(--color-brand-700);
	}
	.gp-col-live {
		color: var(--text-secondary);
	}
	.gp-drift-row {
		display: grid;
		grid-template-columns: minmax(7rem, 0.9fr) 1fr 1.6rem 1fr;
		align-items: center;
		gap: 0.5rem;
		padding: 0.6rem 0.85rem;
		border-bottom: 1px solid var(--border-secondary);
	}
	.gp-drift-row:last-of-type {
		border-bottom: 0;
	}
	.gp-field {
		font-family: var(--forge-mono);
		font-size: 0.74rem;
		font-weight: 600;
		color: var(--text-primary);
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}
	.gp-val {
		font-family: var(--forge-mono);
		font-size: 0.74rem;
		padding: 0.22rem 0.5rem;
		border-radius: var(--radius-sm);
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}
	/* Repo = the desired/incoming value: ember-tinted, the "source of truth". */
	.gp-val-repo {
		color: var(--color-brand-700);
		background: var(--forge-accent-soft);
		border: 1px solid color-mix(in srgb, var(--color-brand-500) 30%, transparent);
		font-weight: 600;
	}
	/* Live = the current value being replaced: muted, struck feel via dashed. */
	.gp-val-live {
		color: var(--text-tertiary);
		background: var(--surface-card-hover);
		border: 1px dashed var(--border-input);
	}
	.gp-arrow {
		justify-self: center;
		color: var(--accent);
		font-weight: 700;
		font-size: 0.85rem;
	}
	.gp-drift-foot {
		margin: 0;
		padding: 0.5rem 0.85rem;
		font-size: 0.68rem;
		color: var(--text-tertiary);
		background: var(--surface-card-hover);
		border-top: 1px solid var(--border-secondary);
	}

	/* ── Status notes (no_file / fetch_failed / invalid) ─────────── */
	.gp-status-note {
		padding: 0.7rem 0.9rem;
		border-radius: var(--radius-lg);
		border: 1px solid var(--border-primary);
	}
	.gp-note-warn {
		background: color-mix(in srgb, var(--color-warning) 7%, var(--surface-card));
		border-color: color-mix(in srgb, var(--color-warning) 26%, transparent);
	}
	.gp-note-danger {
		background: color-mix(in srgb, var(--color-danger) 6%, var(--surface-card));
		border-color: color-mix(in srgb, var(--color-danger) 26%, transparent);
	}
	.gp-note-lead {
		margin: 0;
		font-size: 0.8rem;
		font-weight: 600;
		color: var(--text-primary);
	}
	.gp-note-sub {
		margin: 0.3rem 0 0;
		font-size: 0.73rem;
		color: var(--text-secondary);
		display: flex;
		align-items: center;
		gap: 0.4rem;
		flex-wrap: wrap;
	}
	.gp-mono {
		font-family: var(--forge-mono);
		font-size: 0.7rem;
		word-break: break-word;
	}

	/* ── Rendered .tinyforge.yml preview ─────────────────────────── */
	.gp-editor {
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-lg);
		overflow: hidden;
		background: var(--surface-card);
	}
	.gp-editor-head {
		display: flex;
		align-items: center;
		gap: 0.4rem;
		padding: 0.45rem 0.7rem;
		background: var(--surface-card-hover);
		border-bottom: 1px solid var(--border-primary);
	}
	.gp-dot {
		width: 8px;
		height: 8px;
		border-radius: 50%;
		background: var(--border-input);
	}
	.gp-editor-title {
		margin-left: 0.3rem;
		font-family: var(--forge-mono);
		font-size: 0.68rem;
		color: var(--text-secondary);
	}
	.gp-spacer {
		flex: 1;
	}
	.gp-editor-chip {
		display: inline-flex;
		align-items: center;
		gap: 0.3rem;
		padding: 0.18rem 0.5rem;
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-sm);
		background: var(--surface-card);
		color: var(--text-secondary);
		font-family: var(--forge-mono);
		font-size: 0.62rem;
		letter-spacing: 0.04em;
		text-transform: uppercase;
		cursor: pointer;
		transition: color 120ms ease, border-color 120ms ease;
	}
	.gp-editor-chip:hover {
		color: var(--accent);
		border-color: var(--accent);
	}
	.gp-code {
		margin: 0;
		padding: 0.85rem 1rem;
		font-family: var(--forge-mono);
		font-size: 0.76rem;
		line-height: 1.55;
		color: var(--text-primary);
		white-space: pre;
		overflow-x: auto;
		tab-size: 2;
		max-height: 22rem;
	}

	/* ── Sync action ─────────────────────────────────────────────── */
	.gp-actions {
		display: flex;
		align-items: center;
		gap: 0.8rem;
		flex-wrap: wrap;
	}
	.gp-actions-hint {
		margin: 0;
		font-size: 0.72rem;
		color: var(--text-tertiary);
		flex: 1;
		min-width: 12rem;
	}

	@media (prefers-reduced-motion: reduce) {
		.gp-editor-chip {
			transition: none;
		}
	}
</style>
