<script lang="ts">
	import { onDestroy } from 'svelte';
	import { goto } from '$app/navigation';
	import { page } from '$app/stores';
	import { get } from 'svelte/store';
	import * as api from '$lib/api';
	import type {
		RedeployTrigger,
		TriggerBinding,
		TriggerInput,
		TriggerWebhook
	} from '$lib/api';
	import { triggerDetailCache } from '$lib/stores/caches';
	import ForgeHero from '$lib/components/ForgeHero.svelte';
	import ToggleSwitch from '$lib/components/ToggleSwitch.svelte';
	import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
	import TriggerKindForm, {
		createTriggerKindFormState,
		seedTriggerKindFormState,
		isTriggerFormValid,
		buildTriggerInput
	} from '$lib/components/TriggerKindForm.svelte';
	import { IconCopy, IconRefresh, IconTrash, IconExternalLink } from '$lib/components/icons';
	import { t } from '$lib/i18n';

	// Trigger ids are server-generated (string). The route guarantees
	// `id` is set when this page renders, but SvelteKit types it as
	// `string | undefined`; the empty-string fallback simply quiets
	// the type checker — server validation rejects empty ids anyway.
	const id = $derived($page.params.id ?? '');

	function formatLastFired(ts: string): string {
		if (!ts) return $t('redeployTriggers.detail.lastFiredNever');
		const d = new Date(ts);
		// Defensive: a malformed timestamp from a future writer should
		// not leak raw bytes into the UI. Fall back to the never-fired
		// label rather than render an unparseable string.
		if (Number.isNaN(d.getTime())) return $t('redeployTriggers.detail.lastFiredNever');
		return d.toLocaleString();
	}

	// Warm-seed the trigger from the per-id cache so the hero + config form
	// render instantly on revisit; `loading` clears as soon as the trigger is
	// ready. The webhook/bindings panels gate on `detailsLoaded` instead, since
	// the webhook carries a rotated secret and is never cached.
	const _seed = triggerDetailCache.peek(get(page).params.id ?? '').value;
	let trigger = $state<RedeployTrigger | null>(_seed);
	let webhook = $state<TriggerWebhook | null>(null);
	let bindings = $state<TriggerBinding[]>([]);
	let loading = $state(_seed === null);
	let detailsLoaded = $state(false);
	let saving = $state(false);
	let error = $state('');

	// Confirmation gates for destructive actions.
	let confirmDelete = $state(false);
	let confirmRotate = $state(false);
	let confirmUnbindId = $state<string | null>(null);
	let deleting = $state(false);
	let rotating = $state(false);

	let copied = $state(false);

	// All kind-aware form state lives in the shared TriggerKindForm
	// component. seedTriggerKindFormState() primes it from the loaded
	// trigger; buildTriggerInput() reads it back on save. Kind stays
	// read-only after creation — TriggerKindForm renders a static
	// kind tag when showKindPicker=false.
	let formState = $state(createTriggerKindFormState());

	// Seed the editable form ONCE per id (plain, non-reactive guard so the load
	// effect tracks only `id`). On a warm fresh mount, seed from the cached
	// trigger right away; load() handles cold + reused-component (A→B) nav.
	let seededKey: string | null = null;
	function seedFormFrom(tr: RedeployTrigger): void {
		seedTriggerKindFormState(
			formState,
			tr.kind,
			tr.name,
			tr.config,
			tr.webhook_enabled,
			tr.webhook_require_signature
		);
	}
	if (_seed) {
		seedFormFrom(_seed);
		seededKey = get(page).params.id ?? '';
	}

	// Monotonic load token — each invocation claims the latest seq, so an
	// earlier in-flight load (even for the same id on A→B→A nav, where the
	// id-only guard can't tell two same-id loads apart) is stale and cannot
	// write trigger / bindings / webhook / detailsLoaded.
	let loadSeq = 0;

	async function load(): Promise<void> {
		const k = id;
		const my = ++loadSeq;
		// New id: its secondary panels (bindings/webhook) aren't loaded yet.
		detailsLoaded = false;
		const cached = triggerDetailCache.peek(k);
		if (cached.value) {
			trigger = cached.value;
			if (seededKey !== k) {
				seedFormFrom(cached.value);
				seededKey = k;
			}
			loading = false; // warm: render the hero + config form immediately
		} else {
			loading = true;
		}
		error = '';
		try {
			const tr = await api.getTrigger(id);
			if (my !== loadSeq) return; // a newer load owns the state now
			trigger = tr;
			triggerDetailCache.set(k, tr);
			if (seededKey !== k) {
				seedFormFrom(tr);
				seededKey = k;
			}
			loading = false;
			// Secondary panels. The webhook carries the rotated secret, so it is
			// fetched fresh every time and NEVER cached. Reset both first so a
			// reused-component warm nav (A→B) can't show A's data under B.
			webhook = null;
			bindings = [];
			const [b, w] = await Promise.all([
				api.listBindingsForTrigger(id),
				tr.webhook_enabled ? api.getTriggerWebhook(id) : Promise.resolve(null)
			]);
			if (my !== loadSeq) return;
			bindings = b;
			webhook = w;
			detailsLoaded = true;
		} catch (e) {
			if (my !== loadSeq) return;
			// Clear so a 404 (e.g. a deleted trigger revisited from cache) shows
			// the error state instead of a stale, interactive phantom.
			trigger = null;
			error = e instanceof Error ? e.message : 'Failed to load trigger';
			loading = false;
		}
	}

	const canSave = $derived(!!trigger && !saving && isTriggerFormValid(formState));

	async function save(e?: Event): Promise<void> {
		e?.preventDefault();
		if (!trigger || saving || !canSave) return;
		// Pin the id for the round-trip so a mid-save nav can't write another
		// trigger's state or poison its cache entry (the component is reused).
		const k = id;
		saving = true;
		error = '';
		try {
			// Kind is immutable post-create. The form is rendered with
			// showKindPicker=false so the UI can't mutate it, but pinning
			// to the loaded value here keeps the contract explicit and
			// guards against future regressions if someone re-enables
			// the picker on this page.
			const body: TriggerInput = {
				...buildTriggerInput(formState),
				kind: trigger.kind
			};
			const updated = await api.updateTrigger(k, body);
			if (id !== k) return;
			trigger = updated;
			triggerDetailCache.set(k, updated);
			// Webhook info comes/goes with the toggle. Keep state in
			// sync so the panel doesn't show stale secrets after
			// turning ingress off-then-on.
			if (updated.webhook_enabled) {
				const w = await api.getTriggerWebhook(k);
				if (id !== k) return;
				webhook = w;
			} else {
				webhook = null;
			}
		} catch (e) {
			if (id !== k) return;
			error = e instanceof Error ? e.message : 'Save failed';
		} finally {
			saving = false;
		}
	}

	async function doDelete(): Promise<void> {
		deleting = true;
		error = '';
		try {
			await api.deleteTrigger(id);
			// Drop the cached entry so a back-nav to this deleted id doesn't
			// warm-seed a phantom detail page.
			triggerDetailCache.remove(id);
			goto('/triggers');
		} catch (e) {
			error = e instanceof Error ? e.message : 'Delete failed';
			deleting = false;
			confirmDelete = false;
		}
	}

	let confirmFire = $state(false);
	let firing = $state(false);
	let fireResult = $state<{ deployed: number; errored: number; bindings: number } | null>(null);

	// Auto-clear the fire-result flash after a few seconds. Tracked so
	// a rapid second fire (or component unmount) cancels the prior
	// timer instead of having two writers race to null/replace state.
	const FIRE_FLASH_MS = 5000;
	let fireFlashTimer: ReturnType<typeof setTimeout> | null = null;

	function scheduleFireFlashClear(): void {
		if (fireFlashTimer !== null) clearTimeout(fireFlashTimer);
		fireFlashTimer = setTimeout(() => {
			fireResult = null;
			fireFlashTimer = null;
		}, FIRE_FLASH_MS);
	}

	onDestroy(() => {
		if (fireFlashTimer !== null) clearTimeout(fireFlashTimer);
	});

	async function doFireNow(): Promise<void> {
		if (!trigger) return;
		const k = id;
		const tid = trigger.id;
		firing = true;
		error = '';
		try {
			const res = await api.fireTriggerNow(tid);
			if (id !== k) return;
			fireResult = { deployed: res.deployed, errored: res.errored, bindings: res.bindings };
			scheduleFireFlashClear();
			// Refresh the trigger so the "last fired" row reflects the new ts.
			const fresh = await api.getTrigger(tid);
			if (id !== k) return;
			trigger = fresh;
			triggerDetailCache.set(k, fresh);
		} catch (e) {
			if (id !== k) return;
			error = e instanceof Error ? e.message : 'Fire failed';
		} finally {
			firing = false;
			confirmFire = false;
		}
	}

	async function doRotate(): Promise<void> {
		const k = id;
		rotating = true;
		error = '';
		try {
			const res = await api.regenerateTriggerWebhook(k);
			// Don't surface this trigger's new secret if we've navigated to
			// another trigger while the rotate was in flight.
			if (id !== k) return;
			// regenerate returns the new url+secret but no signing
			// flag — preserve the current toggle state.
			webhook = {
				url: res.url,
				secret: res.secret,
				webhook_require_signature:
					webhook?.webhook_require_signature ?? formState.webhookRequireSig
			};
		} catch (e) {
			if (id !== k) return;
			error = e instanceof Error ? e.message : 'Rotate failed';
		} finally {
			rotating = false;
			confirmRotate = false;
		}
	}

	async function toggleBinding(b: TriggerBinding, next: boolean): Promise<void> {
		const k = id;
		try {
			const updated = await api.updateBinding(b.id, { enabled: next });
			if (id !== k) return;
			bindings = bindings.map((x) => (x.id === b.id ? { ...x, enabled: updated.enabled } : x));
		} catch (e) {
			if (id !== k) return;
			error = e instanceof Error ? e.message : 'Update failed';
		}
	}

	async function doUnbind(): Promise<void> {
		if (!confirmUnbindId) return;
		const bid = confirmUnbindId;
		const k = id;
		try {
			await api.deleteBinding(bid);
			if (id !== k) return;
			bindings = bindings.filter((b) => b.id !== bid);
			// Reflect the new binding count in the hero.
			if (trigger) {
				trigger = { ...trigger, binding_count: Math.max(0, trigger.binding_count - 1) };
				triggerDetailCache.set(k, trigger);
			}
		} catch (e) {
			if (id !== k) return;
			error = e instanceof Error ? e.message : 'Unbind failed';
		} finally {
			confirmUnbindId = null;
		}
	}

	function fullWebhookUrl(): string {
		if (!webhook) return '';
		if (typeof window === 'undefined') return webhook.url;
		// Backend returns a relative path; prefix with the current
		// origin so the operator can copy a paste-ready value.
		return webhook.url.startsWith('http') ? webhook.url : window.location.origin + webhook.url;
	}

	async function copyWebhook(): Promise<void> {
		const url = fullWebhookUrl();
		if (!url || typeof navigator === 'undefined' || !navigator.clipboard) return;
		try {
			await navigator.clipboard.writeText(url);
			copied = true;
			setTimeout(() => (copied = false), 1500);
		} catch {
			// best-effort — silent failure is fine
		}
	}

	const unbindTarget = $derived(
		confirmUnbindId ? bindings.find((b) => b.id === confirmUnbindId) ?? null : null
	);

	function kindLabel(k: string): string {
		const key = `redeployTriggers.kind.${k}`;
		const v = $t(key);
		return v === key ? k : v;
	}

	// SvelteKit reuses this component instance across /triggers/A → /triggers/B,
	// so onMount(load) would only fire once. The id-keyed effect reloads on
	// param change.
	$effect(() => {
		const _ = id;
		load();
	});
</script>

<svelte:head>
	<title>{trigger?.name ?? $t('redeployTriggers.titleSingular')} · Tinyforge</title>
</svelte:head>

<div class="forge" aria-busy={loading || !detailsLoaded}>
	{#snippet stats()}
		<div>
			<dt>KIND</dt>
			<dd>{trigger ? kindLabel(trigger.kind).toUpperCase() : '—'}</dd>
		</div>
		<div>
			<dt>{$t('redeployTriggers.stat.boundWorkloads')}</dt>
			<dd class="accent">{trigger ? String(trigger.binding_count).padStart(2, '0') : '—'}</dd>
		</div>
		<div>
			<dt>WEBHOOK</dt>
			<dd>{trigger ? (trigger.webhook_enabled ? 'ON' : '—') : '—'}</dd>
		</div>
	{/snippet}

	<ForgeHero
		backHref="/triggers"
		backLabel={$t('redeployTriggers.toolbar.backToList')}
		eyebrowSuffix={$t('redeployTriggers.titleSingular').toUpperCase()}
		title={trigger?.name ?? $t('observability.loading')}
		size="md"
		stats={stats}
	/>

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

	{#if loading || !trigger}
		<div class="skeleton-rows" aria-busy="true" aria-live="polite" aria-label={$t('observability.loading')}>
			{#each Array(3) as _, i}
				<div class="skeleton-row" style:--i={i}></div>
			{/each}
		</div>
	{:else}
		<!-- ── Config panel ────────────────────────────── -->
		<form class="panel" onsubmit={save} aria-busy={saving}>
			<header class="panel-head">
				<h2 class="panel-title">
					{$t('redeployTriggers.detail.config')}<span class="title-accent">.</span>
				</h2>
				<span class="panel-sub">
					{$t('redeployTriggers.detail.configSub', {
						kind: trigger.kind,
						id: trigger.id,
						updatedAt: trigger.updated_at
					})}
				</span>
			</header>

			<!-- Kind picker is hidden on edit — kind is immutable post-
			     create. Everything else (name, kind-aware config form,
			     advanced JSON, webhook toggles) lives in the shared
			     TriggerKindForm so /triggers/new and /triggers/[id]
			     stay in lockstep. -->
			<TriggerKindForm bind:state={formState} idPrefix="t" showKindPicker={false} />

			<div class="actions">
				<button
					type="submit"
					class="forge-btn"
					disabled={!canSave}
					aria-busy={saving}
				>
					{saving ? $t('observability.saving') : $t('observability.save')}
				</button>
			</div>
		</form>

		{#if trigger.kind === 'schedule'}
			<!-- ── Schedule status panel ─────────────────── -->
			<section class="panel" aria-labelledby="sched-heading">
				<header class="panel-head">
					<h2 class="panel-title" id="sched-heading">
						{$t('redeployTriggers.detail.scheduleStatus')}<span class="title-accent">.</span>
					</h2>
					<span class="panel-sub">{$t('redeployTriggers.detail.scheduleStatusSub')}</span>
				</header>

				<div class="sched-row">
					<div class="sched-block">
						<span class="sub-label">{$t('redeployTriggers.detail.lastFired')}</span>
						<span class="mono">{formatLastFired(trigger.last_fired_at)}</span>
					</div>
					<button
						type="button"
						class="forge-btn-ghost xs"
						onclick={() => (confirmFire = true)}
						disabled={firing || trigger.binding_count === 0}
						title={trigger.binding_count === 0
							? $t('redeployTriggers.detail.fireNowDisabledTitle')
							: $t('redeployTriggers.detail.fireNowTitle')}
					>
						{firing ? $t('redeployTriggers.detail.firing') : $t('redeployTriggers.detail.fireNow')}
					</button>
				</div>
				{#if fireResult}
					<div class="fire-flash" role="status">
						{$t('redeployTriggers.detail.fireResult', {
							deployed: String(fireResult.deployed),
							bindings: String(fireResult.bindings),
							errored: String(fireResult.errored)
						})}
					</div>
				{/if}
			</section>
		{/if}

		<!-- ── Webhook ingress panel ───────────────────── -->
		<section class="panel" aria-labelledby="webhook-heading">
			<header class="panel-head">
				<h2 class="panel-title" id="webhook-heading">
					{$t('redeployTriggers.detail.webhook')}<span class="title-accent">.</span>
				</h2>
				<span class="panel-sub">{$t('redeployTriggers.detail.webhookSub')}</span>
			</header>

			{#if !detailsLoaded}
				<div class="skeleton-rows" aria-busy="true"><div class="skeleton-row"></div></div>
			{:else if webhook && trigger.webhook_enabled}
				<div class="field">
					<span class="sub-label">{$t('redeployTriggers.detail.webhookUrlLabel')}</span>
					<div class="url-box">
						<code class="url-text mono">{fullWebhookUrl()}</code>
						<div class="url-actions">
							<button
								type="button"
								class="forge-btn-icon"
								onclick={copyWebhook}
								aria-label={$t('redeployTriggers.detail.webhookCopy')}
								title={copied
									? $t('redeployTriggers.detail.webhookCopied')
									: $t('redeployTriggers.detail.webhookCopy')}
							>
								<IconCopy size={14} />
							</button>
							<button
								type="button"
								class="forge-btn-ghost xs"
								onclick={() => (confirmRotate = true)}
								disabled={rotating}
							>
								<IconRefresh size={12} />
								<span>
									{rotating
										? $t('redeployTriggers.detail.webhookRotating')
										: $t('redeployTriggers.detail.webhookRotate')}
								</span>
							</button>
						</div>
					</div>
					<span class="hint">{$t('redeployTriggers.detail.webhookUrlNote')}</span>
					{#if copied}
						<span class="copied-flag" role="status">
							{$t('redeployTriggers.detail.webhookCopied')}
						</span>
					{/if}
				</div>

				<div class="signing-line">
					<span
						class="sig-pill"
						class:on={trigger.webhook_require_signature}
						class:off={!trigger.webhook_require_signature}
					>
						<span class="status-dot" aria-hidden="true"></span>
						<span class="mono">{$t('redeployTriggers.detail.webhookRequireSig')}</span>
						<span class="mono small">
							{trigger.webhook_require_signature ? 'ON' : 'OFF'}
						</span>
					</span>
				</div>
			{:else}
				<div class="note muted-note">
					<span class="note-tag">OFF</span>
					<p>{$t('redeployTriggers.detail.webhookDisabledNote')}</p>
				</div>
			{/if}
		</section>

		<!-- ── Bindings panel ──────────────────────────── -->
		<section class="panel" aria-labelledby="bindings-heading">
			<header class="panel-head">
				<h2 class="panel-title" id="bindings-heading">
					{$t('redeployTriggers.detail.bindings')}<span class="title-accent">.</span>
				</h2>
				<span class="panel-sub">{$t('redeployTriggers.detail.bindingsSub')}</span>
			</header>

			{#if !detailsLoaded}
				<div class="skeleton-rows" aria-busy="true"><div class="skeleton-row"></div></div>
			{:else if bindings.length === 0}
				<div class="note muted-note">
					<span class="note-tag">∅</span>
					<p>{$t('redeployTriggers.detail.bindingsEmpty')}</p>
				</div>
			{:else}
				<ul class="bindings-list">
					{#each bindings as b, i (b.id)}
						<li class="binding">
							<div class="b-main">
								<span class="b-ref mono">{String(i + 1).padStart(2, '0')}</span>
								<span class="b-name">{b.workload_name || b.workload_id}</span>
								<span
									class="b-state mono"
									class:on={b.enabled}
									class:off={!b.enabled}
								>
									{b.enabled
										? $t('redeployTriggers.binding.enabled')
										: $t('redeployTriggers.binding.disabled')}
								</span>
							</div>
							<div class="b-actions">
								<ToggleSwitch
									checked={b.enabled}
									onchange={(next: boolean) => toggleBinding(b, next)}
									label={$t('redeployTriggers.binding.enabled')}
								/>
								<a
									class="forge-btn-icon"
									href={`/apps?workload=${b.workload_id}`}
									aria-label={$t('redeployTriggers.detail.bindingsListItem.openWorkload')}
									title={$t('redeployTriggers.detail.bindingsListItem.openWorkload')}
								>
									<IconExternalLink size={14} />
								</a>
								<button
									type="button"
									class="forge-btn-icon danger"
									onclick={() => (confirmUnbindId = b.id)}
									aria-label={$t('redeployTriggers.detail.bindingsListItem.unbind')}
									title={$t('redeployTriggers.detail.bindingsListItem.unbind')}
								>
									<IconTrash size={14} />
								</button>
							</div>
						</li>
					{/each}
				</ul>
				<p class="hint foot">{$t('redeployTriggers.detail.bindingEnabledHint')}</p>
			{/if}
		</section>

		<!-- ── Danger zone ────────────────────────────── -->
		<section class="panel danger-panel" aria-labelledby="danger-heading">
			<header class="panel-head">
				<h2 class="panel-title" id="danger-heading">
					{$t('redeployTriggers.detail.dangerZone')}<span class="title-accent">.</span>
				</h2>
				<span class="panel-sub">{$t('redeployTriggers.detail.dangerZoneSub')}</span>
			</header>
			<div class="danger-actions">
				<button
					type="button"
					class="forge-btn-ghost forge-btn-danger"
					onclick={() => (confirmDelete = true)}
				>
					{$t('redeployTriggers.detail.deleteButton')}
				</button>
			</div>
		</section>

		<ConfirmDialog
			open={confirmDelete}
			title={$t('redeployTriggers.detail.deleteTitle')}
			message={$t('redeployTriggers.detail.deleteMessage', {
				name: formState.name.trim() || trigger.name,
				count: String(trigger.binding_count)
			})}
			confirmLabel={deleting ? $t('observability.deleting') : $t('observability.delete')}
			confirmVariant="danger"
			onconfirm={doDelete}
			oncancel={() => (confirmDelete = false)}
		/>

		<ConfirmDialog
			open={confirmRotate}
			title={$t('redeployTriggers.detail.rotateTitle')}
			message={$t('redeployTriggers.detail.rotateMessage')}
			confirmLabel={rotating
				? $t('redeployTriggers.detail.webhookRotating')
				: $t('redeployTriggers.detail.rotateConfirm')}
			confirmVariant="danger"
			onconfirm={doRotate}
			oncancel={() => (confirmRotate = false)}
		/>

		<ConfirmDialog
			open={confirmFire}
			title={$t('redeployTriggers.detail.fireConfirmTitle')}
			message={$t('redeployTriggers.detail.fireConfirmMessage', {
				name: trigger.name,
				count: String(trigger.binding_count)
			})}
			confirmLabel={firing
				? $t('redeployTriggers.detail.firing')
				: $t('redeployTriggers.detail.fireConfirm')}
			onconfirm={doFireNow}
			oncancel={() => (confirmFire = false)}
		/>

		<ConfirmDialog
			open={confirmUnbindId !== null}
			title={$t('redeployTriggers.detail.unbindTitle')}
			message={$t('redeployTriggers.detail.unbindMessage', {
				name: unbindTarget?.workload_name || unbindTarget?.workload_id || ''
			})}
			confirmLabel={$t('redeployTriggers.detail.unbindConfirm')}
			confirmVariant="danger"
			onconfirm={doUnbind}
			oncancel={() => (confirmUnbindId = null)}
		/>
	{/if}
</div>

<style>
	.forge {
		display: flex;
		flex-direction: column;
		gap: 1.25rem;
		max-width: 820px;
		margin: 0 auto;
	}

	/* ── Alert ─────────────────────────────────────── */
	.alert {
		display: flex;
		gap: 0.7rem;
		align-items: center;
		padding: 0.7rem 0.9rem;
		background: var(--color-danger-light);
		color: var(--color-danger-dark);
		border: 1px solid var(--color-danger);
		border-left-width: 4px;
		border-radius: var(--radius-lg);
		font-size: 0.875rem;
	}
	.alert-tag {
		font-family: var(--forge-mono);
		font-weight: 700;
		font-size: 0.65rem;
		letter-spacing: 0.16em;
		padding: 0.15rem 0.4rem;
		background: var(--color-danger);
		color: var(--surface-card);
		border-radius: var(--radius-sm);
	}
	:global([data-theme='dark']) .alert {
		background: color-mix(in srgb, var(--color-danger) 14%, transparent);
		color: color-mix(in srgb, var(--color-danger) 60%, var(--text-primary));
	}

	/* ── Skeleton ──────────────────────────────────── */
	.skeleton-rows {
		display: flex;
		flex-direction: column;
		gap: 0.55rem;
	}
	.skeleton-row {
		height: 64px;
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-2xl);
		background: linear-gradient(
			110deg,
			var(--surface-card) 20%,
			var(--surface-card-hover) 50%,
			var(--surface-card) 80%
		);
		background-size: 200% 100%;
		animation: shimmer 1.6s linear infinite;
		animation-delay: calc(var(--i) * 120ms);
	}
	@keyframes shimmer {
		0% { background-position: 200% 0; }
		100% { background-position: -200% 0; }
	}

	/* ── Panel ─────────────────────────────────────── */
	.panel {
		display: flex;
		flex-direction: column;
		gap: 0.9rem;
		background: var(--surface-card);
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-2xl);
		padding: 1.5rem;
	}
	@media (max-width: 600px) {
		.panel {
			padding: 1.1rem;
			gap: 1rem;
		}
	}
	.panel.danger-panel {
		border-color: color-mix(in srgb, var(--color-danger) 35%, var(--border-primary));
	}
	.panel-head {
		display: flex;
		flex-direction: column;
		gap: 0.25rem;
		margin-bottom: 0.2rem;
	}
	.panel-title {
		margin: 0;
		font-size: 1.1rem;
		font-weight: 700;
		letter-spacing: -0.01em;
		color: var(--text-primary);
	}
	.title-accent { color: var(--forge-accent); }
	.panel-sub {
		font-family: var(--forge-mono);
		font-size: 0.7rem;
		color: var(--text-tertiary);
		line-height: 1.5;
	}

	/* ── Schedule status panel ─────────────────── */
	.sched-row {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 1rem;
		flex-wrap: wrap;
	}
	.sched-block {
		display: flex;
		flex-direction: column;
		gap: 0.3rem;
	}
	.fire-flash {
		padding: 0.55rem 0.75rem;
		background: color-mix(in srgb, var(--forge-accent) 12%, transparent);
		border: 1px solid color-mix(in srgb, var(--forge-accent) 45%, transparent);
		border-radius: var(--radius-lg);
		font-size: 0.83rem;
		color: var(--text-primary);
		font-family: var(--forge-mono);
	}

	/* ── Fields ────────────────────────────────────── */
	.field {
		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);
	}
	/* ── Hints ─────────────────────────────────────── */
	.hint {
		font-size: 0.78rem;
		color: var(--text-tertiary);
		line-height: 1.5;
		margin: 0;
	}
	.hint.foot { margin-top: 0.6rem; }

	/* ── Note banner ────────────────────────────────── */
	.note {
		display: flex;
		gap: 0.75rem;
		align-items: flex-start;
		padding: 0.75rem 0.9rem;
		background: var(--surface-card-hover);
		border: 1px dashed var(--border-primary);
		border-radius: var(--radius-lg);
	}
	.muted-note { background: transparent; }
	.note-tag {
		padding: 0.18rem 0.45rem;
		background: var(--text-primary);
		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);
		flex: 0 0 auto;
		line-height: 1.4;
	}
	.note p {
		margin: 0;
		font-size: 0.85rem;
		color: var(--text-secondary);
		line-height: 1.5;
	}

	/* ── Actions ────────────────────────────────────── */
	.actions {
		display: flex;
		justify-content: flex-end;
		gap: 0.55rem;
		flex-wrap: wrap;
	}
	.danger-actions { display: flex; justify-content: flex-end; }
	@media (max-width: 480px) {
		.actions, .danger-actions {
			flex-direction: column-reverse;
			align-items: stretch;
		}
		.actions :global(.forge-btn),
		.actions :global(.forge-btn-ghost),
		.danger-actions :global(.forge-btn-ghost) {
			justify-content: center;
		}
	}

	/* ── URL display ────────────────────────────────
	   The URL gets its own framed slot with a left-side
	   monospace text region and a right-side action stack.
	   The text wraps so long secrets don't horizontally
	   blow out the panel on narrow viewports. */
	.url-box {
		display: flex;
		gap: 0.75rem;
		align-items: stretch;
		padding: 0.65rem 0.75rem;
		background: var(--surface-card-hover);
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-lg);
		flex-wrap: wrap;
	}
	.url-text {
		flex: 1;
		min-width: 0;
		font-family: var(--forge-mono);
		font-size: 0.78rem;
		color: var(--text-primary);
		word-break: break-all;
		line-height: 1.5;
		display: inline-flex;
		align-items: center;
	}
	.url-actions {
		display: inline-flex;
		gap: 0.4rem;
		align-items: center;
		flex-wrap: wrap;
	}
	:global(.forge-btn-ghost.xs) {
		padding: 0.25rem 0.55rem;
		font-size: 0.6rem;
	}
	.copied-flag {
		align-self: flex-end;
		font-family: var(--forge-mono);
		font-size: 0.6rem;
		color: var(--color-success-dark);
		letter-spacing: 0.14em;
		text-transform: uppercase;
	}

	/* ── Signing pill ───────────────────────────────── */
	.signing-line {
		display: flex;
		gap: 0.55rem;
		flex-wrap: wrap;
	}
	.sig-pill {
		display: inline-flex;
		align-items: center;
		gap: 0.45rem;
		padding: 0.25rem 0.65rem;
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-full);
		font-family: var(--forge-mono);
		font-size: 0.62rem;
		font-weight: 600;
		letter-spacing: 0.1em;
		text-transform: uppercase;
		background: var(--surface-card-hover);
		color: var(--text-secondary);
	}
	.sig-pill.on {
		background: color-mix(in srgb, var(--color-success) 10%, transparent);
		border-color: color-mix(in srgb, var(--color-success) 35%, transparent);
		color: var(--color-success-dark);
	}
	.sig-pill.off {
		background: color-mix(in srgb, var(--color-warning, #f59e0b) 10%, transparent);
		border-color: color-mix(in srgb, var(--color-warning, #f59e0b) 35%, transparent);
		color: var(--color-warning-dark, #b45309);
	}
	.status-dot {
		width: 7px;
		height: 7px;
		border-radius: 50%;
	}
	.sig-pill.on .status-dot {
		background: var(--color-success);
		box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-success) 20%, transparent);
	}
	.sig-pill.off .status-dot {
		background: var(--color-warning, #f59e0b);
	}
	.small { font-size: 0.6rem; opacity: 0.7; }
	.mono { font-family: var(--forge-mono); }

	/* ── Bindings list ──────────────────────────────
	   List, not table — bindings are short and the
	   actions column makes a one-row-per-binding card
	   stack a more comfortable read at this width. */
	.bindings-list {
		list-style: none;
		margin: 0;
		padding: 0;
		display: flex;
		flex-direction: column;
		gap: 0.5rem;
	}
	.binding {
		display: flex;
		align-items: center;
		gap: 0.75rem;
		padding: 0.7rem 0.85rem;
		background: var(--surface-card-hover);
		border: 1px solid var(--border-primary);
		border-radius: var(--radius-lg);
		flex-wrap: wrap;
	}
	.b-main {
		display: inline-flex;
		align-items: baseline;
		gap: 0.7rem;
		flex: 1;
		min-width: 0;
	}
	.b-ref {
		font-size: 0.66rem;
		letter-spacing: 0.1em;
		color: var(--text-tertiary);
		flex: 0 0 auto;
	}
	.b-name {
		font-weight: 600;
		color: var(--text-primary);
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
		min-width: 0;
	}
	.b-state {
		font-size: 0.58rem;
		font-weight: 700;
		letter-spacing: 0.16em;
		text-transform: uppercase;
		padding: 0.12rem 0.45rem;
		border-radius: var(--radius-sm);
	}
	.b-state.on {
		background: color-mix(in srgb, var(--color-success) 12%, transparent);
		color: var(--color-success-dark);
	}
	.b-state.off {
		background: var(--surface-card);
		color: var(--text-tertiary);
	}
	.b-actions {
		display: inline-flex;
		gap: 0.35rem;
		align-items: center;
	}
	:global(.forge-btn-icon.danger) {
		color: var(--color-danger);
	}
	:global(.forge-btn-icon.danger:hover:not(:disabled)) {
		background: var(--color-danger-light);
	}
	:global([data-theme='dark']) :global(.forge-btn-icon.danger:hover:not(:disabled)) {
		background: color-mix(in srgb, var(--color-danger) 14%, transparent);
	}
</style>
