<!--
  /admin/users — owner + admin only. Members get bounced to /profile
  with ?denied=users. Replaces the pre-3.5 /admin/admins page; this
  one knows about roles, email, and the last-owner/last-admin guards.
-->
<script lang="ts">
	import { onMount } from 'svelte';
	import { goto } from '$app/navigation';
	import { base } from '$app/paths';
	import {
		api,
		ApiError,
		type AdminDto,
		type InstanceRole
	} from '$lib/api';
	import { currentUser } from '$lib/auth';
	import RoleChip from '$lib/RoleChip.svelte';
	import ConfirmModal from '$lib/ConfirmModal.svelte';
	import ActionMenu from '$lib/ActionMenu.svelte';
	import { generatePassword } from '$lib/password-gen';

	const me = $derived($currentUser);
	const myRole = $derived(me?.instance_role);
	const isOwner = $derived(myRole === 'owner');

	// Member guard. The backend already 403s the list call, but
	// surfacing a friendly redirect avoids the dead-end empty page.
	$effect(() => {
		if (me && me.instance_role === 'member') {
			void goto(`${base}/profile?denied=users`);
		}
	});

	let admins = $state<AdminDto[]>([]);
	let loadError = $state<string | null>(null);
	let banner = $state<{ kind: 'error' | 'info'; message: string } | null>(null);

	let search = $state('');
	const filtered = $derived(
		(() => {
			const q = search.trim().toLowerCase();
			if (!q) return admins;
			return admins.filter(
				(a) =>
					a.username.toLowerCase().includes(q) ||
					(a.email ?? '').toLowerCase().includes(q)
			);
		})()
	);

	// Invite (create) modal --------------------------------------------------
	let inviteOpen = $state(false);
	// F-U-018: Owner is selectable for the bootstrap case (owners can
	// always create more owners; the asymmetry with editRoleOptions
	// was unnecessary friction).
	let inviteForm = $state<{ username: string; email: string; instance_role: InstanceRole }>({
		username: '',
		email: '',
		instance_role: 'admin'
	});
	let invitePending = $state(false);
	let inviteError = $state<string | null>(null);

	// One-time password reveal (used by both invite + reset)
	let revealPassword = $state<string | null>(null);
	let revealForUsername = $state<string>('');
	let revealKind = $state<'invite' | 'reset'>('invite');
	let revealAck = $state(false);
	let copyState = $state<'idle' | 'copied'>('idle');

	// Edit modal -------------------------------------------------------------
	let editTarget = $state<AdminDto | null>(null);
	let editForm = $state<{
		username: string;
		email: string;
		instance_role: InstanceRole;
	}>({ username: '', email: '', instance_role: 'admin' });
	let editPending = $state(false);
	let editError = $state<string | null>(null);

	// Delete modal -----------------------------------------------------------
	let deleteTarget = $state<AdminDto | null>(null);
	let deletePending = $state(false);

	// Deactivate modal -------------------------------------------------------
	// Reactivate is one-click (non-destructive); deactivate routes
	// through the modal because it signs the user out and expires
	// every API key they hold.
	let deactivateTarget = $state<AdminDto | null>(null);
	let deactivatePending = $state(false);

	// Validation rules (mirror backend: 2-32, [a-z0-9._-]) -------------------
	const USERNAME_RE = /^[a-z0-9._-]{2,32}$/;
	const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

	const inviteUsernameValid = $derived(USERNAME_RE.test(inviteForm.username));
	const inviteEmailValid = $derived(
		inviteForm.email.trim() === '' || EMAIL_RE.test(inviteForm.email.trim())
	);
	const canInvite = $derived(inviteUsernameValid && inviteEmailValid && !invitePending);

	const editUsernameValid = $derived(USERNAME_RE.test(editForm.username));
	const editEmailValid = $derived(
		editForm.email.trim() === '' || EMAIL_RE.test(editForm.email.trim())
	);
	const canSubmitEdit = $derived(editUsernameValid && editEmailValid && !editPending);

	// Admin (non-owner) cannot touch owner rows for delete or role demote.
	function canDelete(row: AdminDto): boolean {
		if (isOwner) return true;
		return row.instance_role !== 'owner';
	}

	const editRoleOptions = $derived<InstanceRole[]>(
		isOwner ? ['owner', 'admin', 'member'] : ['admin', 'member']
	);

	onMount(refresh);

	async function refresh() {
		loadError = null;
		try {
			admins = await api.admins.list();
		} catch (e) {
			loadError = e instanceof ApiError ? e.message : 'failed to load users';
		}
	}

	function flash(kind: 'error' | 'info', message: string) {
		banner = { kind, message };
		setTimeout(() => {
			if (banner?.message === message) banner = null;
		}, 6000);
	}

	function openInvite() {
		inviteForm = { username: '', email: '', instance_role: 'admin' };
		inviteError = null;
		inviteOpen = true;
	}

	async function submitInvite(event: SubmitEvent) {
		event.preventDefault();
		if (!canInvite) return;
		invitePending = true;
		inviteError = null;
		const password = generatePassword(16);
		try {
			const created = await api.admins.create({
				username: inviteForm.username,
				password,
				instance_role: inviteForm.instance_role,
				email: inviteForm.email.trim() === '' ? null : inviteForm.email.trim()
			});
			admins = [...admins, created].sort((a, b) => a.username.localeCompare(b.username));
			inviteOpen = false;
			revealPassword = password;
			revealForUsername = created.username;
			revealKind = 'invite';
			revealAck = false;
			copyState = 'idle';
		} catch (e) {
			inviteError = e instanceof ApiError ? e.message : 'failed to create user';
		} finally {
			invitePending = false;
		}
	}

	function openEdit(row: AdminDto) {
		editTarget = row;
		editForm = {
			username: row.username,
			email: row.email ?? '',
			instance_role: row.instance_role
		};
		editError = null;
	}

	async function submitEdit(event: SubmitEvent) {
		event.preventDefault();
		if (!editTarget || !canSubmitEdit) return;
		editPending = true;
		editError = null;
		const patch: {
			username?: string;
			email?: string | null;
			instance_role?: InstanceRole;
		} = {};
		if (editForm.username !== editTarget.username) patch.username = editForm.username;
		if ((editTarget.email ?? '') !== editForm.email.trim()) {
			patch.email = editForm.email.trim() === '' ? null : editForm.email.trim();
		}
		if (editForm.instance_role !== editTarget.instance_role) {
			patch.instance_role = editForm.instance_role;
		}
		try {
			const updated = await api.admins.update(editTarget.id, patch);
			admins = admins
				.map((a) => (a.id === updated.id ? updated : a))
				.sort((a, b) => a.username.localeCompare(b.username));
			const name = updated.username;
			editTarget = null;
			flash('info', `Updated "${name}".`);
		} catch (e) {
			editError = e instanceof ApiError ? e.message : 'failed to update user';
		} finally {
			editPending = false;
		}
	}

	async function resetPassword() {
		if (!editTarget) return;
		const target = editTarget;
		const password = generatePassword(16);
		editPending = true;
		editError = null;
		try {
			await api.admins.update(target.id, { password });
			editTarget = null;
			revealPassword = password;
			revealForUsername = target.username;
			revealKind = 'reset';
			revealAck = false;
			copyState = 'idle';
		} catch (e) {
			editError = e instanceof ApiError ? e.message : 'failed to reset password';
		} finally {
			editPending = false;
		}
	}

	async function reactivate(row: AdminDto) {
		try {
			const updated = await api.admins.update(row.id, { is_active: true });
			admins = admins.map((a) => (a.id === updated.id ? updated : a));
			flash('info', `${updated.username} reactivated.`);
		} catch (e) {
			flash('error', e instanceof ApiError ? e.message : 'failed to update user');
		}
	}

	function askDeactivate(row: AdminDto) {
		deactivateTarget = row;
	}

	async function confirmDeactivate() {
		if (!deactivateTarget) return;
		deactivatePending = true;
		const target = deactivateTarget;
		try {
			const updated = await api.admins.update(target.id, { is_active: false });
			admins = admins.map((a) => (a.id === updated.id ? updated : a));
			deactivateTarget = null;
			flash('info', `${updated.username} deactivated.`);
		} catch (e) {
			flash('error', e instanceof ApiError ? e.message : 'failed to update user');
		} finally {
			deactivatePending = false;
		}
	}

	function openDelete(row: AdminDto) {
		deleteTarget = row;
	}

	async function confirmDelete() {
		if (!deleteTarget) return;
		deletePending = true;
		const target = deleteTarget;
		try {
			await api.admins.remove(target.id);
			deleteTarget = null;
			if (me && me.id === target.id) {
				// Self-delete: bail out to login.
				await api.auth.logout();
				await goto(`${base}/login`);
				return;
			}
			admins = admins.filter((a) => a.id !== target.id);
			flash('info', `Deleted "${target.username}".`);
		} catch (e) {
			flash('error', e instanceof ApiError ? e.message : 'failed to delete user');
		} finally {
			deletePending = false;
		}
	}

	async function copyPassword() {
		if (!revealPassword) return;
		try {
			await navigator.clipboard.writeText(revealPassword);
			copyState = 'copied';
			setTimeout(() => (copyState = 'idle'), 2000);
		} catch {
			flash('error', 'Clipboard write failed — select and copy manually.');
		}
	}

	function dismissReveal() {
		revealPassword = null;
		revealAck = false;
	}

	function relative(iso: string | null): string {
		if (!iso) return 'Never';
		const sec = Math.round((Date.now() - new Date(iso).getTime()) / 1000);
		if (sec < 60) return `${sec}s ago`;
		const min = Math.round(sec / 60);
		if (min < 60) return `${min}m ago`;
		const hr = Math.round(min / 60);
		if (hr < 24) return `${hr}h ago`;
		const day = Math.round(hr / 24);
		if (day < 7) return `${day}d ago`;
		return new Date(iso).toISOString().slice(0, 10);
	}

	function shortDate(iso: string): string {
		return new Date(iso).toISOString().slice(0, 10);
	}
</script>

<header class="head">
	<h1>Users</h1>
	<div class="head-controls">
		<input
			type="search"
			placeholder="Search by username or email…"
			bind:value={search}
			class="search"
		/>
		<button type="button" class="primary" onclick={openInvite}>+ Invite user</button>
	</div>
</header>

{#if banner}
	<div class="banner banner-{banner.kind}">{banner.message}</div>
{/if}

{#if loadError}
	<div class="error">
		{loadError}
		<button type="button" class="retry" onclick={refresh}>Retry</button>
	</div>
{:else if admins.length === 0}
	<p class="empty">No users yet. Invite one to get started.</p>
{:else}
	<div class="table">
		<div class="row head-row">
			<div>Username</div>
			<div>Role</div>
			<div>Email</div>
			<div>Status</div>
			<div>Created</div>
			<div>Last login</div>
			<div class="actions-col"></div>
		</div>
		{#each filtered as row (row.id)}
			<div class="row">
				<div class="name-cell">
					<span class="name">{row.username}</span>
					{#if me && me.id === row.id}
						<span class="you-tag">(you)</span>
					{/if}
				</div>
				<div><RoleChip role={row.instance_role} size="sm" /></div>
				<div class="email-cell">{row.email ?? '—'}</div>
				<div>
					{#if row.is_active}
						<span class="status status-active">● Active</span>
					{:else}
						<span class="status status-inactive">○ Inactive</span>
					{/if}
				</div>
				<div>{shortDate(row.created_at)}</div>
				<div title={row.last_login_at ?? ''}>{relative(row.last_login_at)}</div>
				<div class="actions-col">
					<ActionMenu
						label="User actions for {row.username}"
						items={[
							{ label: 'Edit', onClick: () => openEdit(row) },
							{
								label: row.is_active ? 'Deactivate' : 'Reactivate',
								onClick: () =>
									row.is_active ? askDeactivate(row) : reactivate(row)
							},
							{
								label: 'Delete',
								danger: true,
								disabled: !canDelete(row),
								onClick: () => openDelete(row)
							}
						]}
					/>
				</div>
			</div>
		{/each}
		{#if filtered.length === 0 && admins.length > 0}
			<div class="row empty-row">No matches for "{search}".</div>
		{/if}
	</div>
{/if}

<!-- Invite modal -->
{#if inviteOpen}
	<div
		class="modal-backdrop"
		role="presentation"
		onclick={(e) => {
			if (e.target === e.currentTarget && !invitePending) inviteOpen = false;
		}}
	>
		<form class="modal" onsubmit={submitInvite}>
			<div class="modal-head">
				<h2>Invite user</h2>
				<button
					type="button"
					class="x"
					aria-label="Close"
					disabled={invitePending}
					onclick={() => (inviteOpen = false)}>✕</button
				>
			</div>
			<p class="modal-intro">
				A random password will be generated and shown to you exactly once. PiCloud cannot send
				email — copy and share through your own channel.
			</p>
			<label class="field">
				<span>Username</span>
				<input
					type="text"
					autocomplete="off"
					spellcheck="false"
					bind:value={inviteForm.username}
					required
				/>
				<small>2–32 chars. Lowercase letters, digits, <code>.</code> <code>_</code> <code>-</code>.</small>
				{#if inviteForm.username && !inviteUsernameValid}
					<small class="invalid">Doesn't match the allowed pattern.</small>
				{/if}
			</label>
			<label class="field">
				<span>Email <span class="opt">(optional)</span></span>
				<input
					type="email"
					autocomplete="off"
					spellcheck="false"
					bind:value={inviteForm.email}
				/>
				{#if !inviteEmailValid}
					<small class="invalid">Doesn't look like an email address.</small>
				{/if}
			</label>
			<fieldset class="field">
				<legend>Role</legend>
				{#if me?.instance_role === 'owner'}
					<label class="radio">
						<input type="radio" bind:group={inviteForm.instance_role} value="owner" />
						<span>Owner — full instance control (irrevocable except by another owner).</span>
					</label>
				{/if}
				<label class="radio">
					<input type="radio" bind:group={inviteForm.instance_role} value="admin" />
					<span>Admin — can manage users, scripts, and all apps.</span>
				</label>
				<label class="radio">
					<input type="radio" bind:group={inviteForm.instance_role} value="member" />
					<span>Member — only sees apps they're added to.</span>
				</label>
				{#if me?.instance_role !== 'owner'}
					<small>Owners can't be created here — promote via Edit after creation.</small>
				{/if}
			</fieldset>
			{#if inviteError}
				<div class="error">{inviteError}</div>
			{/if}
			<div class="modal-actions">
				<button type="button" class="ghost" onclick={() => (inviteOpen = false)} disabled={invitePending}>
					Cancel
				</button>
				<button type="submit" class="primary" disabled={!canInvite}>
					{invitePending ? 'Creating…' : 'Create user'}
				</button>
			</div>
		</form>
	</div>
{/if}

<!-- Edit modal -->
{#if editTarget}
	{@const target = editTarget}
	<div
		class="modal-backdrop"
		role="presentation"
		onclick={(e) => {
			if (e.target === e.currentTarget && !editPending) editTarget = null;
		}}
	>
		<form class="modal" onsubmit={submitEdit}>
			<div class="modal-head">
				<h2>Edit {target.username}</h2>
				<button
					type="button"
					class="x"
					aria-label="Close"
					disabled={editPending}
					onclick={() => (editTarget = null)}>✕</button
				>
			</div>
			<label class="field">
				<span>Username</span>
				<input
					type="text"
					autocomplete="off"
					spellcheck="false"
					bind:value={editForm.username}
					required
				/>
				{#if editForm.username && !editUsernameValid}
					<small class="invalid">2–32 chars, lowercase + digits + . _ - only.</small>
				{/if}
			</label>
			<label class="field">
				<span>Email <span class="opt">(optional)</span></span>
				<input
					type="email"
					autocomplete="off"
					spellcheck="false"
					bind:value={editForm.email}
				/>
				{#if !editEmailValid}
					<small class="invalid">Doesn't look like an email address.</small>
				{/if}
			</label>
			<label class="field">
				<span>Role</span>
				<select bind:value={editForm.instance_role}>
					{#each editRoleOptions as r (r)}
						<option value={r}>{r}</option>
					{/each}
				</select>
				<small>
					{#if target.instance_role === 'owner' && !isOwner}
						Only owners can change another owner's role.
					{:else if !isOwner}
						Admins can grant admin or member; only owners can grant owner.
					{:else}
						The last active owner can't be demoted — the request will 422 if that's the case.
					{/if}
				</small>
			</label>
			{#if editError}
				<div class="error">{editError}</div>
			{/if}
			<div class="modal-actions split">
				<button type="button" class="ghost" onclick={resetPassword} disabled={editPending}>
					Reset password
				</button>
				<div class="modal-actions">
					<button
						type="button"
						class="ghost"
						onclick={() => (editTarget = null)}
						disabled={editPending}
					>
						Cancel
					</button>
					<button type="submit" class="primary" disabled={!canSubmitEdit}>
						{editPending ? 'Saving…' : 'Save'}
					</button>
				</div>
			</div>
		</form>
	</div>
{/if}

<!-- Password reveal (post-invite or post-reset) -->
{#if revealPassword}
	<div class="modal-backdrop" role="presentation">
		<div class="modal reveal-modal">
			<div class="modal-head">
				<h2>
					{revealKind === 'invite' ? 'User created' : 'Password reset'} — {revealForUsername}
				</h2>
			</div>
			<p class="banner banner-warn">
				Save this password now — it will never be shown again. PiCloud cannot send email yet,
				so copy it and share through your own channel.
			</p>
			<div class="token-row">
				<code class="token">{revealPassword}</code>
				<button type="button" class="ghost" onclick={copyPassword}>
					{copyState === 'copied' ? 'Copied ✓' : 'Copy'}
				</button>
			</div>
			<label class="ack">
				<input type="checkbox" bind:checked={revealAck} />
				<span>I've shared this with the user.</span>
			</label>
			<div class="modal-actions">
				<button type="button" class="primary" disabled={!revealAck} onclick={dismissReveal}>
					Done
				</button>
			</div>
		</div>
	</div>
{/if}

<!-- Deactivate confirmation -->
{#if deactivateTarget}
	{@const dt = deactivateTarget}
	<ConfirmModal
		title="Deactivate {dt.username}?"
		variant="danger"
		confirmLabel="Deactivate"
		busyLabel="Deactivating…"
		busy={deactivatePending}
		onConfirm={confirmDeactivate}
		onCancel={() => (deactivateTarget = null)}
	>
		<p>
			Deactivating signs <strong>{dt.username}</strong> out immediately and
			expires <strong>every API key</strong> they hold. Their sessions and keys
			won't come back if you reactivate — they'll need to log in again and
			mint new keys.
		</p>
		<p class="muted">
			Reactivation is one click — this isn't permanent.
		</p>
	</ConfirmModal>
{/if}

<!-- Delete confirmation -->
{#if deleteTarget}
	{@const dt = deleteTarget}
	<ConfirmModal
		title="Delete user?"
		variant="danger"
		confirmLabel="Delete user"
		confirmPhrase={dt.username}
		confirmPhrasePrompt="Type the username to confirm:"
		busy={deletePending}
		busyLabel="Deleting…"
		onConfirm={confirmDelete}
		onCancel={() => (deleteTarget = null)}
	>
		{#if me && me.id === dt.id}
			<p>
				You're about to delete <strong>your own</strong> account. You'll be signed out
				immediately and won't be able to sign back in.
			</p>
		{:else}
			<p>
				This permanently removes <strong>{dt.username}</strong>, all their sessions, and all
				their API keys. This cannot be undone.
			</p>
		{/if}
		<p class="muted">
			If they're the only remaining owner or active admin the server will reject the request
			with a 422 — promote/activate someone else first.
		</p>
	</ConfirmModal>
{/if}

<style>
	.head {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 1rem;
		margin-bottom: 1.5rem;
		flex-wrap: wrap;
	}
	.head h1 {
		font-size: 1.25rem;
		margin: 0;
		color: #e2e8f0;
	}
	.head-controls {
		display: flex;
		gap: 0.5rem;
		align-items: center;
	}
	.search {
		background: #0b1220;
		border: 1px solid #1e293b;
		border-radius: 0.375rem;
		padding: 0.5rem 0.75rem;
		color: #e2e8f0;
		font-size: 0.85rem;
		min-width: 16rem;
	}
	.search:focus {
		outline: none;
		border-color: #38bdf8;
	}

	.banner {
		padding: 0.55rem 0.85rem;
		border-radius: 0.375rem;
		margin-bottom: 1rem;
		font-size: 0.85rem;
	}
	.banner-error {
		background: #450a0a;
		border: 1px solid #b91c1c;
		color: #fecaca;
	}
	.banner-info {
		background: #0c2a36;
		border: 1px solid #155e75;
		color: #a5f3fc;
	}
	.banner-warn {
		background: #2a1d04;
		border: 1px solid #ca8a04;
		color: #fde68a;
		margin: 0;
	}

	.empty {
		color: #64748b;
		text-align: center;
		padding: 2.5rem 0;
		border: 1px dashed #1e293b;
		border-radius: 0.5rem;
	}

	.table {
		display: flex;
		flex-direction: column;
		border: 1px solid #1e293b;
		border-radius: 0.5rem;
		background: #0b1220;
		overflow: visible;
	}
	.row {
		display: grid;
		grid-template-columns: 1.3fr 0.7fr 1.5fr 0.9fr 0.8fr 0.9fr 2.5rem;
		align-items: center;
		gap: 0.75rem;
		padding: 0.7rem 1rem;
		border-bottom: 1px solid #1e293b;
		font-size: 0.85rem;
	}
	.row:last-child {
		border-bottom: none;
	}
	.head-row {
		color: #94a3b8;
		font-size: 0.7rem;
		text-transform: uppercase;
		letter-spacing: 0.05em;
		background: #0f172a;
	}
	.empty-row {
		grid-column: 1 / -1;
		color: #64748b;
		text-align: center;
		padding: 1.25rem;
	}
	.name-cell {
		display: flex;
		align-items: baseline;
		gap: 0.4rem;
	}
	.name {
		color: #e2e8f0;
		font-weight: 500;
	}
	.you-tag {
		color: #64748b;
		font-size: 0.72rem;
	}
	.email-cell {
		color: #cbd5e1;
		font-size: 0.82rem;
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}
	.status {
		font-size: 0.8rem;
	}
	.status-active {
		color: #34d399;
	}
	.status-inactive {
		color: #64748b;
	}
	.actions-col {
		display: flex;
		justify-content: flex-end;
	}

	button.primary {
		background: #38bdf8;
		color: #0b1220;
		border: none;
		padding: 0.5rem 0.9rem;
		border-radius: 0.375rem;
		font-weight: 600;
		cursor: pointer;
		font-size: 0.85rem;
	}
	button.primary:disabled {
		opacity: 0.6;
		cursor: not-allowed;
	}
	button.ghost {
		background: transparent;
		color: #94a3b8;
		border: 1px solid #334155;
		padding: 0.45rem 0.85rem;
		border-radius: 0.375rem;
		cursor: pointer;
		font-size: 0.85rem;
	}
	button.ghost:hover {
		background: #1e293b;
		color: #e2e8f0;
	}

	.error {
		background: #450a0a;
		border: 1px solid #b91c1c;
		color: #fecaca;
		padding: 0.55rem 0.8rem;
		border-radius: 0.375rem;
		font-size: 0.85rem;
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 1rem;
	}
	.retry {
		background: transparent;
		border: 1px solid #b91c1c;
		color: #fecaca;
		padding: 0.2rem 0.55rem;
		border-radius: 0.25rem;
		cursor: pointer;
		font-size: 0.75rem;
	}

	.modal-backdrop {
		position: fixed;
		inset: 0;
		background: rgba(2, 6, 23, 0.7);
		display: flex;
		align-items: center;
		justify-content: center;
		padding: 1rem;
		z-index: 50;
	}
	.modal {
		background: #0b1220;
		border: 1px solid #334155;
		border-radius: 0.5rem;
		padding: 1.5rem;
		width: 100%;
		max-width: 28rem;
		max-height: calc(100vh - 2rem);
		overflow-y: auto;
		display: flex;
		flex-direction: column;
		gap: 1rem;
		box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
	}
	.reveal-modal {
		border-color: #ca8a04;
	}
	.modal-head {
		display: flex;
		align-items: center;
		justify-content: space-between;
	}
	.modal h2 {
		margin: 0;
		font-size: 1rem;
		color: #e2e8f0;
	}
	.x {
		background: transparent;
		border: none;
		color: #64748b;
		font-size: 1.1rem;
		cursor: pointer;
	}
	.x:hover {
		color: #e2e8f0;
	}
	.modal-intro {
		margin: 0;
		font-size: 0.82rem;
		color: #94a3b8;
		line-height: 1.45;
	}
	.field {
		display: flex;
		flex-direction: column;
		gap: 0.3rem;
		font-size: 0.85rem;
		color: #cbd5e1;
		border: none;
		padding: 0;
		margin: 0;
	}
	.field legend {
		font-size: 0.85rem;
		color: #cbd5e1;
		padding: 0;
		margin-bottom: 0.3rem;
	}
	.field input[type='text'],
	.field input[type='email'],
	.field select {
		background: #0f172a;
		color: #e2e8f0;
		border: 1px solid #1e293b;
		border-radius: 0.375rem;
		padding: 0.5rem 0.7rem;
		font-size: 0.9rem;
	}
	.field input:focus,
	.field select:focus {
		outline: none;
		border-color: #38bdf8;
	}
	.field small {
		color: #64748b;
		font-size: 0.72rem;
	}
	.field small.invalid {
		color: #fca5a5;
	}
	.field small code {
		background: #1e293b;
		color: #cbd5e1;
		padding: 0 0.2rem;
		border-radius: 0.2rem;
	}
	.opt {
		color: #64748b;
		font-weight: 400;
	}

	.radio {
		display: flex;
		align-items: center;
		gap: 0.5rem;
		padding: 0.25rem 0;
		font-size: 0.82rem;
		color: #cbd5e1;
	}


	.token-row {
		display: flex;
		align-items: stretch;
		gap: 0.5rem;
	}
	.token {
		flex: 1;
		background: #020617;
		border: 1px solid #1e293b;
		border-radius: 0.375rem;
		padding: 0.6rem 0.75rem;
		color: #e2e8f0;
		font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
		font-size: 0.85rem;
		overflow-x: auto;
		white-space: nowrap;
	}
	.ack {
		display: flex;
		align-items: center;
		gap: 0.5rem;
		font-size: 0.85rem;
		color: #cbd5e1;
		cursor: pointer;
	}

	.modal-actions {
		display: flex;
		justify-content: flex-end;
		gap: 0.5rem;
	}
	.modal-actions.split {
		justify-content: space-between;
	}

	.muted {
		color: #94a3b8;
	}
</style>
