<!--
  Image source form. Surfaces the most-used image-source fields as proper
  controls (ref, port, healthcheck, default tag, registry, resource limits).
  Env + volumes stay on the detail page where they have dedicated panels.

  The parent owns the `ImageFormState` (from `$lib/workload/sourceForms`) and
  binds it here; serialization to `source_config` is done by the parent via
  `imageToConfig` so the shape stays byte-identical.

  Two pieces of async UX live in this component but write their results back
  through bindable props so the parent's submit gate can read them:

    • Inspect — pulls port + healthcheck from the image metadata. Guarded by
      an AbortController + ref re-check so a late response can't relabel a
      newer ref. Touch sentinels stop Inspect overwriting fields the operator
      already edited.
    • Conflict lookup — debounced /api/discovery/image/conflicts call,
      guarded by a sequence token so a slow earlier response can't clobber a
      faster later one. The `conflicts` / `conflictAcknowledged` /
      `conflictBlocked` triplet is bound to the parent which runs the
      two-click "submit anyway" gate.
-->
<script lang="ts">
	import { onDestroy } from 'svelte';
	import type { ImageFormState } from '$lib/workload/sourceForms';
	import * as api from '$lib/api';
	import { IconSearch, IconLoader } from '$lib/components/icons';
	import RegistryImagePicker from '$lib/components/RegistryImagePicker.svelte';
	import DeployStrategyField from '$lib/components/workload/DeployStrategyField.svelte';
	import { t } from '$lib/i18n';

	interface Props {
		form: ImageFormState;
		/** Registry list for the picker; empty falls back to a text input. */
		registries?: { name: string; url: string }[];
		/** Bound submitting flag — gates the debounced conflict lookup. */
		submitting?: boolean;
		/** Bound conflict triplet — the parent's submit gate reads these. */
		conflicts?: api.ImageConflict[];
		conflictAcknowledged?: boolean;
		conflictBlocked?: boolean;
		/**
		 * Gate the debounced /api/discovery/image/conflicts lookup + the conflict
		 * warning panel. The create wizard wants it (default `true`) so an
		 * operator about to deploy a duplicate image is warned. The detail-page
		 * EDIT form must turn it OFF — there the workload would flag itself as a
		 * conflict with its own image. When off, the conflict triplet / submitting
		 * / registries props are unused and may be omitted by the parent.
		 */
		enableConflicts?: boolean;
		/** Flip to the raw-JSON editor (owned by the parent). */
		onAdvanced: () => void;
	}

	let {
		form = $bindable(),
		registries = [],
		submitting = $bindable(false),
		conflicts = $bindable([]),
		conflictAcknowledged = $bindable(false),
		conflictBlocked = $bindable(false),
		enableConflicts = true,
		onAdvanced
	}: Props = $props();

	// ── Inspect state ─────────────────────────────────────────────────
	type InspectStatus = 'idle' | 'pending' | 'ok' | 'error';
	let inspectStatus = $state<InspectStatus>('idle');
	// AbortController + sequence guard so a late inspect response cannot
	// mislabel the *current* image ref after the user typed a new one.
	let inspectAbort: AbortController | null = null;
	// Touch sentinels for fields with a "0 == empty" sentinel value (port,
	// healthcheck). Once the user interacts, Inspect leaves them alone — even
	// when still `0` / "" (some images really do listen on port 0 / have no
	// healthcheck).
	let portTouched = $state(false);
	let healthcheckTouched = $state(false);

	// The legacy inline seedImageFromJSON reset the touched sentinels on every
	// reseed (mount, kind switch, Advanced↔form toggle). The parent reseeds by
	// REASSIGNING the form object (a fresh seed* result), so the object's
	// identity changes on reseed but not on in-place field edits. Track that
	// identity to reset the sentinels exactly when a reseed happens — keeping
	// Inspect's "only fill untouched fields" behaviour identical to before.
	let lastSeenForm: ImageFormState | null = null;
	$effect(() => {
		if (form !== lastSeenForm) {
			lastSeenForm = form;
			portTouched = false;
			healthcheckTouched = false;
		}
	});

	// ── Conflict-lookup state ─────────────────────────────────────────
	let conflictLoading = $state(false);
	let conflictDebounce: ReturnType<typeof setTimeout> | null = null;
	// Race token so a slow earlier response cannot overwrite a faster later one.
	let conflictReqSeq = 0;

	// Query the backend for workloads already using this image. Failures are
	// silent (the existing list stays) — a transient network blip should never
	// clear a real warning. The caller guards against empty / too-short refs.
	async function fetchImageConflicts(ref: string): Promise<void> {
		const mine = ++conflictReqSeq;
		conflictLoading = true;
		try {
			const result = await api.listImageConflicts(ref);
			if (mine === conflictReqSeq) {
				conflicts = result;
			}
		} catch (e) {
			if (mine === conflictReqSeq) {
				// no-op; intentionally preserve prior conflicts
				void e;
			}
		} finally {
			if (mine === conflictReqSeq) {
				conflictLoading = false;
			}
		}
	}

	function scheduleImageConflictLookup(ref: string) {
		if (conflictDebounce) {
			clearTimeout(conflictDebounce);
			conflictDebounce = null;
		}
		// Conflict detection disabled (edit form) — never probe the backend.
		if (!enableConflicts) return;
		const trimmed = ref.trim();
		if (trimmed.length < 3 || submitting) return;
		conflictDebounce = setTimeout(() => {
			conflictDebounce = null;
			void fetchImageConflicts(trimmed);
		}, 300);
	}

	function onImageRefInput() {
		// Typing invalidates any prior acknowledgement and clears the stale
		// list so the panel doesn't lie about the current ref; the debounced
		// lookup will repopulate it.
		conflictAcknowledged = false;
		conflictBlocked = false;
		conflicts = [];
		// Also reset the inspect pill — its OK/error status belongs to the
		// *previous* ref and would mislead the user otherwise.
		inspectStatus = 'idle';
		inspectAbort?.abort();
		scheduleImageConflictLookup(form.ref);
	}

	function onImageRefBlur() {
		if (!enableConflicts) return;
		const trimmed = form.ref.trim();
		if (trimmed.length < 3 || submitting) return;
		if (conflictDebounce) {
			clearTimeout(conflictDebounce);
			conflictDebounce = null;
		}
		void fetchImageConflicts(trimmed);
	}

	// Tear down the pending debounce timer + cancel any in-flight inspect
	// request if the user navigates away mid-window — otherwise the late
	// resolve mutates dead state.
	onDestroy(() => {
		if (conflictDebounce) {
			clearTimeout(conflictDebounce);
			conflictDebounce = null;
		}
		inspectAbort?.abort();
	});

	// Pull port + healthcheck from the image's exposed metadata. Only
	// overwrites untouched fields. A new call aborts any in-flight one, and we
	// re-check the ref after the await so a late response can't relabel the
	// *new* image ref the user just typed.
	async function inspectImageRef() {
		const ref = form.ref.trim();
		if (!ref) return;
		if (inspectStatus === 'pending') return;
		inspectAbort?.abort();
		const controller = new AbortController();
		inspectAbort = controller;
		inspectStatus = 'pending';
		try {
			const result = await api.inspectImage(ref, controller.signal);
			// Late-arrival guard: if the user edited the ref during the flight,
			// our success belongs to a stale value — discard.
			if (form.ref.trim() !== ref) return;
			// Only fill fields the operator hasn't touched. The sentinel is the
			// touched flag, not the value — a user who deliberately types `0`
			// or clears the healthcheck still owns the field.
			if (!portTouched && typeof result.port === 'number') form.port = result.port;
			if (!healthcheckTouched && typeof result.healthcheck === 'string') {
				form.healthcheck = result.healthcheck;
			}
			inspectStatus = 'ok';
		} catch (e) {
			if (controller.signal.aborted) return;
			if (form.ref.trim() !== ref) return;
			// Show a friendly, localized message — never the raw backend
			// string (the discovery handlers were just hardened to drop
			// leaky daemon errors, so there is nothing useful to surface).
			void e;
			inspectStatus = 'error';
		}
	}
</script>

<div class="image-form">
	<div class="image-form-head">
		<span class="editor-title">{$t('apps.new.imageHeader')}</span>
		<button type="button" class="json-escape" onclick={onAdvanced} title={$t('apps.new.switchToJsonTitle')}>
			{$t('apps.new.advancedJson')}
		</button>
	</div>
	<label class="sub" for="app-image-ref">
		<span class="sub-label"
			>{$t('apps.new.imageRefLabel')}<span class="req-star" aria-label={$t('apps.new.fieldRequired')}>*</span></span
		>
		<div class="input-with-button">
			<div class="input-wrap">
				<input
					id="app-image-ref"
					type="text"
					class="input mono"
					bind:value={form.ref}
					oninput={onImageRefInput}
					onblur={onImageRefBlur}
					placeholder={$t('apps.new.imageRefPlaceholder')}
					autocomplete="off"
					spellcheck="false"
					required
				/>
				<!--
					Conflict-lookup affordance lives INSIDE the field as an
					absolutely-positioned overlay, so a blur → check → clear
					cycle never reflows the rows below it. (The old inline hint
					sat in normal flow and flashed in/out, shifting the whole
					form.) A left fade masks ref text behind it; the aria-live
					region still announces the lookup to assistive tech.
				-->
				{#if enableConflicts && conflictLoading}
					<span class="conflict-checking" role="status" aria-live="polite">
						<IconLoader size={12} />
						<span>{$t('apps.new.imageConflictChecking')}</span>
					</span>
				{/if}
			</div>
			<button
				type="button"
				class="discover-btn"
				onclick={inspectImageRef}
				disabled={!form.ref.trim() || inspectStatus === 'pending'}
				title={$t('apps.new.imageInspectHint')}
			>
				{#if inspectStatus === 'pending'}
					<IconLoader size={14} />
				{:else}
					<IconSearch size={14} />
				{/if}
				<span>{$t('apps.new.imageInspect')}</span>
			</button>
			<RegistryImagePicker
				current={form.ref}
				onpick={(ref, registryName) => {
					form.ref = ref;
					// Auto-select the registry the image came from so private
					// images pull with the right credentials without a second
					// manual step. Only adopt it when the picker surfaced a
					// non-empty name (public images carry '') so we never wipe a
					// registry the operator already chose.
					if (registryName) form.registryName = registryName;
					onImageRefInput();
				}}
			/>
		</div>
		<p class="hint">{$t('apps.new.imageRefHint')}</p>
		{#if inspectStatus === 'ok'}
			<span class="discover-pill discover-pill-ok inline">{$t('apps.new.imageInspectOk')}</span>
		{:else if inspectStatus === 'error'}
			<span class="discover-pill discover-pill-bad inline">{$t('apps.new.errors.inspectFailed')}</span>
		{/if}
	</label>
	{#if enableConflicts && conflicts.length > 0}
		<div class="conflict-panel" role="status" aria-live="polite">
			<div class="conflict-panel-head">
				<span class="conflict-tag">{$t('apps.new.imageConflictTag')}</span>
			</div>
			<p class="conflict-heading">
				{$t('apps.new.imageConflictHeading', { count: String(conflicts.length) })}
				<code class="conflict-ref mono">{form.ref.trim()}</code>
			</p>
			<ul class="conflict-list">
				{#each conflicts as conflict (conflict.id)}
					<li class="conflict-row">
						<div class="conflict-row-text">
							<span class="conflict-name">{conflict.name}</span>
							<span class="conflict-image mono">{conflict.image}</span>
						</div>
						<a
							href={`/apps/${conflict.id}`}
							class="editor-chip conflict-open"
							title={$t('apps.new.imageConflictOpenBtn')}
						>
							{$t('apps.new.imageConflictOpenBtn')}
						</a>
					</li>
				{/each}
			</ul>
			<p class="conflict-foot">{$t('apps.new.imageConflictAcknowledgeNote')}</p>
		</div>
	{/if}
	<div class="row three">
		<label class="sub" for="app-image-port">
			<span class="sub-label">{$t('apps.new.imagePort')}</span>
			<input
				id="app-image-port"
				type="number"
				min="0"
				max="65535"
				class="input"
				bind:value={form.port}
				oninput={() => (portTouched = true)}
			/>
		</label>
		<label class="sub" for="app-image-healthcheck">
			<span class="sub-label">{$t('apps.new.imageHealthcheck')}</span>
			<input
				id="app-image-healthcheck"
				type="text"
				class="input mono"
				bind:value={form.healthcheck}
				oninput={() => (healthcheckTouched = true)}
				placeholder="/healthz"
				autocomplete="off"
				spellcheck="false"
			/>
		</label>
		<label class="sub" for="app-image-default-tag">
			<span class="sub-label">{$t('apps.new.imageDefaultTag')}</span>
			<input
				id="app-image-default-tag"
				type="text"
				class="input mono"
				bind:value={form.defaultTag}
				placeholder="latest"
				autocomplete="off"
				spellcheck="false"
			/>
		</label>
	</div>
	<label class="sub" for="app-image-registry">
		<span class="sub-label">{$t('apps.new.imageRegistryLabel')}</span>
		{#if registries.length > 0}
			<select id="app-image-registry" class="input" bind:value={form.registryName}>
				<option value="">{$t('apps.new.imageRegistryPublic')}</option>
				{#each registries as r}
					<option value={r.name}>{r.name} — {r.url}</option>
				{/each}
			</select>
		{:else}
			<input
				id="app-image-registry"
				type="text"
				class="input"
				bind:value={form.registryName}
				placeholder={$t('apps.new.imageRegistryPublic')}
				autocomplete="off"
			/>
		{/if}
		<p class="hint">{$t('apps.new.imageRegistryHint')}</p>
	</label>
	<div class="row three">
		<label class="sub" for="app-image-cpu">
			<span class="sub-label">{$t('apps.new.imageCpu')}</span>
			<input id="app-image-cpu" type="number" min="0" step="0.1" class="input" bind:value={form.cpuLimit} />
			<p class="hint">{$t('apps.new.imageCpuHint')}</p>
		</label>
		<label class="sub" for="app-image-memory">
			<span class="sub-label">{$t('apps.new.imageMemory')}</span>
			<input id="app-image-memory" type="number" min="0" class="input" bind:value={form.memoryLimit} />
			<p class="hint">{$t('apps.new.imageMemoryHint')}</p>
		</label>
		<label class="sub" for="app-image-max">
			<span class="sub-label">{$t('apps.new.imageMax')}</span>
			<input id="app-image-max" type="number" min="1" class="input" bind:value={form.maxInstances} />
			<p class="hint">{$t('apps.new.imageMaxHint')}</p>
		</label>
	</div>
	<DeployStrategyField bind:strategy={form.deployStrategy} defaultStrategy="blue-green" idPrefix="app-image-strategy" />
	<p class="hint image-form-foot">{$t('apps.new.imageFoot')}</p>
</div>

<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;
	}
	.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 so it doesn't bloat the mono
	   sub-label. The aria-label carries the meaning for assistive tech. */
	.req-star {
		margin-left: 0.2rem;
		color: var(--color-danger);
		font-weight: 700;
	}
	.editor-title {
		margin-left: 0.4rem;
		color: var(--text-secondary);
		font-weight: 600;
		letter-spacing: 0.02em;
	}
	.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:focus-visible {
		outline: 2px solid var(--border-focus);
		outline-offset: 2px;
	}

	/* ── "Edit as JSON" escape hatch ─────────────────────────
	   A quiet secondary text-link, not a prominent chip — it must not
	   compete with the form title. Mono, muted, underline-on-hover so it
	   reads as the rarely-used power-user door it is. */
	.json-escape {
		background: none;
		border: 0;
		padding: 0;
		font-family: var(--forge-mono);
		font-size: 0.62rem;
		font-weight: 600;
		letter-spacing: 0.08em;
		text-transform: uppercase;
		color: var(--text-tertiary);
		cursor: pointer;
		text-decoration: none;
		text-underline-offset: 3px;
		transition: color 120ms ease;
	}
	.json-escape:hover {
		color: var(--forge-accent);
		text-decoration: underline;
	}
	.json-escape:focus-visible {
		outline: 2px solid var(--border-focus);
		outline-offset: 2px;
		border-radius: var(--radius-sm);
	}

	/* ── Image source form ──────────────────────────────────
	   Same overall shell as the editor box (border + radius) but the
	   contents are a stack of labelled form rows. */
	.image-form {
		display: flex;
		flex-direction: column;
		gap: 0.9rem;
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-lg);
		background: var(--surface-input);
		padding: 0.85rem 1rem;
	}
	.image-form-head {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 0.5rem;
		padding-bottom: 0.55rem;
		border-bottom: 1px solid var(--border-primary);
		margin-bottom: 0.2rem;
	}
	.image-form-foot {
		margin-top: 0.2rem;
		padding-top: 0.55rem;
		border-top: 1px dashed var(--border-primary);
	}
	.row.three {
		display: grid;
		grid-template-columns: 1fr 1fr 1fr;
		gap: 0.9rem;
	}
	@media (max-width: 720px) {
		.row.three {
			grid-template-columns: 1fr 1fr;
		}
	}
	@media (max-width: 480px) {
		.row.three {
			grid-template-columns: 1fr;
		}
	}
	@media (max-width: 600px) {
		.row {
			grid-template-columns: 1fr;
		}
	}

	/* ── Discovery-style input+button row + status pills ─── */
	.input-with-button {
		display: flex;
		align-items: stretch;
		gap: 0.4rem;
	}
	.input-with-button > .input-wrap {
		flex: 1;
		min-width: 0;
	}
	/* Wrapper exists only to anchor the absolute conflict-checking overlay to
	   the field's box (inputs can't host positioned children themselves). */
	.input-wrap {
		position: relative;
		display: flex;
		min-width: 0;
	}
	.input-wrap > .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-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;
	}

	/* ── Image-source conflict panel ──────────────────
	   Sibling of .image-form-foot. Reuses the dashed border + soft card
	   surface treatment lifted into a loud amber-leaning warning tag. */
	.conflict-panel {
		display: flex;
		flex-direction: column;
		gap: 0.55rem;
		margin-top: 0.2rem;
		padding: 0.75rem 0.9rem;
		background: var(--surface-card-hover);
		border: 1px dashed var(--color-warning, var(--forge-accent));
		border-radius: var(--radius-lg);
	}
	.conflict-panel-head {
		display: flex;
		align-items: center;
		gap: 0.5rem;
	}
	.conflict-tag {
		display: inline-flex;
		padding: 0.18rem 0.5rem;
		background: var(--color-warning, var(--forge-accent));
		color: var(--surface-card);
		font-family: var(--forge-mono);
		font-size: 0.58rem;
		font-weight: 700;
		letter-spacing: 0.18em;
		border-radius: var(--radius-sm);
		line-height: 1;
	}
	/* Quiet "checking…" affordance shown while a conflict lookup is in flight.
	   Pinned as an absolute overlay inside the image-ref field's right edge so
	   it sits ENTIRELY out of document flow — toggling it on a blur → check →
	   clear cycle can no longer reflow the form rows beneath it (the old
	   in-flow hint flashed in/out and shifted the whole form). The left fade
	   lets a long ref scroll cleanly under the pill instead of hard-cutting,
	   and pointer-events:none keeps the field fully clickable underneath. */
	.conflict-checking {
		position: absolute;
		top: 50%;
		right: 0.5rem;
		transform: translateY(-50%);
		display: inline-flex;
		align-items: center;
		gap: 0.35rem;
		padding: 0.28rem 0.55rem 0.28rem 1.6rem;
		border-radius: var(--radius-md);
		background: linear-gradient(90deg, transparent, var(--surface-input) 1.1rem);
		font-family: var(--forge-mono);
		font-size: 0.62rem;
		font-weight: 600;
		letter-spacing: 0.06em;
		color: var(--text-tertiary);
		white-space: nowrap;
		pointer-events: none;
		animation: cc-fade-in 140ms ease-out;
	}
	.conflict-checking :global(svg) {
		animation: spin 0.9s linear infinite;
	}
	@keyframes spin {
		to {
			transform: rotate(360deg);
		}
	}
	@keyframes cc-fade-in {
		from {
			opacity: 0;
		}
		to {
			opacity: 1;
		}
	}
	/* Respect users who opt out of motion: no spin, no fade. */
	@media (prefers-reduced-motion: reduce) {
		.conflict-checking,
		.conflict-checking :global(svg) {
			animation: none;
		}
	}
	.conflict-heading {
		margin: 0;
		font-size: 0.84rem;
		color: var(--text-secondary);
		line-height: 1.5;
		display: flex;
		flex-wrap: wrap;
		align-items: baseline;
		gap: 0.4rem;
	}
	.conflict-ref {
		padding: 0.1rem 0.35rem;
		font-size: 0.78rem;
		background: var(--surface-input);
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-sm);
		color: var(--text-primary);
	}
	.conflict-list {
		list-style: none;
		margin: 0;
		padding: 0;
		display: flex;
		flex-direction: column;
		gap: 0.35rem;
	}
	.conflict-row {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 0.75rem;
		padding: 0.5rem 0.65rem;
		background: var(--surface-card);
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-md);
	}
	.conflict-row-text {
		display: flex;
		flex-direction: column;
		gap: 0.15rem;
		min-width: 0;
	}
	.conflict-name {
		font-weight: 600;
		font-size: 0.88rem;
		color: var(--text-primary);
		letter-spacing: -0.01em;
	}
	.conflict-image {
		font-size: 0.74rem;
		color: var(--text-tertiary);
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
		max-width: 100%;
	}
	.conflict-open {
		flex: 0 0 auto;
		text-decoration: none;
	}
	.conflict-foot {
		margin: 0;
		font-size: 0.74rem;
		color: var(--text-tertiary);
		line-height: 1.45;
	}
</style>
