<!--
Phase 30 ship-class calculator. Replaces the Phase 17/18 standalone
designer: it fuses the ship design blocks, the live derived results
(mass, speed, attack, defence, bombing), and a planet build-rate readout
into one sidebar tool, and adds single-target goal-seek — the player pins
one result and the model back-solves the single input it claims (see
`lib/calculator/calc-model.ts`). A second mode reuses the design area to
price ship-class modernization. All math comes from `pkg/calc` through
the `Core` WASM bridge; this component only renders + orchestrates.

Input state lives in the long-lived `calculatorState` singleton, not in
the component, so it survives the sidebar unmounting this tab on a tab
switch (the inspector auto-opens on a planet click) — the calculator is a
long-lived planning tool. `ensureGame` resets it when the game changes.
-->
<script lang="ts">
	import { getContext } from "svelte";
	import { appScreen } from "$lib/app-nav.svelte";

	import { i18n, type TranslationKey } from "$lib/i18n/index.svelte";
	import {
		RENDERED_REPORT_CONTEXT_KEY,
		type RenderedReportSource,
	} from "$lib/rendered-report.svelte";
	import {
		ORDER_DRAFT_CONTEXT_KEY,
		OrderDraftStore,
	} from "../../sync/order-draft.svelte";
	import { CORE_CONTEXT_KEY, type CoreHandle } from "$lib/core-context.svelte";
	import {
		SELECTION_CONTEXT_KEY,
		type SelectionStore,
	} from "$lib/selection.svelte";
	import type { ReportPlanet, ShipClassSummary } from "../../api/game-state";
	import {
		validateShipClass,
		type ShipClassInvalidReason,
	} from "$lib/util/ship-class-validation";
	import {
		computeCalculator,
		computePlanetBuild,
		type LockableOutputId,
		type LoadMode,
	} from "$lib/calculator/calc-model";
	import ShipDesignArea, {
		type TechKey,
	} from "$lib/calculator/ship-design-area.svelte";
	import { reachStore } from "$lib/calculator/reach.svelte";
	import { calculatorLoadRequest } from "$lib/calculator/load-request.svelte";
	import { calculatorState } from "$lib/calculator/calc-state.svelte";

	const LOAD_MODES: LoadMode[] = ["empty", "full", "custom"];

	const rendered = getContext<RenderedReportSource | undefined>(
		RENDERED_REPORT_CONTEXT_KEY,
	);
	const draft = getContext<OrderDraftStore | undefined>(
		ORDER_DRAFT_CONTEXT_KEY,
	);
	const coreHandle = getContext<CoreHandle | undefined>(CORE_CONTEXT_KEY);
	const selection = getContext<SelectionStore | undefined>(
		SELECTION_CONTEXT_KEY,
	);

	// The long-lived input state (survives tab unmount/remount).
	const cs = calculatorState;
	// Reset the design when the active game changes; a no-op otherwise, so
	// the design persists across tab switches within a game.
	$effect(() => {
		cs.ensureGame(appScreen.gameId ?? "");
	});

	const core = $derived(coreHandle?.core ?? null);
	const report = $derived(rendered?.report ?? null);
	const localShipClass = $derived<ShipClassSummary[]>(
		report?.localShipClass ?? [],
	);
	const existingNames = $derived(localShipClass.map((c) => c.name));

	const playerTech = $derived({
		drive: report?.localPlayerDrive ?? 0,
		weapons: report?.localPlayerWeapons ?? 0,
		shields: report?.localPlayerShields ?? 0,
		cargo: report?.localPlayerCargo ?? 0,
	});

	const techKeys: TechKey[] = ["drive", "weapons", "shields", "cargo"];

	// Non-overridden tech levels track the player's current tech; the
	// effect resets them whenever the report (history snapshot included)
	// changes, so the calculator reflects the right turn's tech.
	$effect(() => {
		for (const k of techKeys) {
			if (!cs.techOverridden[k]) cs.techValues[k] = playerTech[k];
		}
	});
	// Seed the modernization target with the player's current tech once
	// the report has loaded; afterwards it is the player's to edit.
	$effect(() => {
		if (cs.targetSeeded) return;
		if (
			playerTech.drive ||
			playerTech.weapons ||
			playerTech.shields ||
			playerTech.cargo
		) {
			cs.targetTech = { ...playerTech };
			cs.targetSeeded = true;
		}
	});

	const result = $derived(
		computeCalculator(
			{
				blocks: cs.blocks,
				driveTech: cs.techValues.drive,
				weaponsTech: cs.techValues.weapons,
				shieldsTech: cs.techValues.shields,
				cargoTech: cs.techValues.cargo,
				loadMode: cs.loadMode,
				customLoad: cs.customLoad,
				lock: cs.lock === null ? null : { output: cs.lock, value: cs.lockValue },
			},
			core,
		),
	);

	// Selected own planet (MVP: own planets only).
	const selectedPlanet = $derived.by<ReportPlanet | null>(() => {
		const sel = selection?.selected;
		if (sel === undefined || sel === null || sel.kind !== "planet") return null;
		const planet = report?.planets.find((p) => p.number === sel.id) ?? null;
		if (planet === null || planet.kind !== "local") return null;
		return planet;
	});

	$effect(() => {
		if (!cs.matOverridden) {
			cs.matValue = selectedPlanet?.materialsStockpile ?? 0;
		}
	});

	// With no cargo block there is no hold to load: pin the load to empty
	// and disable the toggle.
	const cargoEmpty = $derived(cs.blocks.cargo === 0);
	$effect(() => {
		if (cargoEmpty && cs.loadMode !== "empty") cs.loadMode = "empty";
	});

	const planetBuild = $derived.by(() => {
		if (selectedPlanet === null) return null;
		const emptyMass = result.outputs?.emptyMass;
		if (emptyMass === undefined) return null;
		return computePlanetBuild(
			{
				shipMass: emptyMass,
				freeIndustry: selectedPlanet.freeIndustry ?? 0,
				material: cs.matValue,
				resources: selectedPlanet.resources ?? 0,
			},
			core,
		);
	});

	// Publish the selected planet's reach (loaded speed) so the map view
	// can draw 1–3 reach circles. Cleared when the design is invalid, no
	// own planet is selected, or the calculator is in modernization mode.
	$effect(() => {
		const out = result.outputs;
		if (cs.mode === "ship" && selectedPlanet !== null && out !== null) {
			reachStore.set(
				{ x: selectedPlanet.x, y: selectedPlanet.y },
				out.speedLoaded,
			);
		} else {
			reachStore.clear();
		}
		return () => reachStore.clear();
	});

	const nameInvalidKeyMap: Record<ShipClassInvalidReason, TranslationKey> = {
		empty: "game.calculator.invalid.empty",
		too_long: "game.calculator.invalid.too_long",
		starts_with_special: "game.calculator.invalid.starts_with_special",
		ends_with_special: "game.calculator.invalid.ends_with_special",
		consecutive_specials: "game.calculator.invalid.consecutive_specials",
		whitespace: "game.calculator.invalid.whitespace",
		disallowed_character: "game.calculator.invalid.disallowed_character",
		duplicate_name: "game.calculator.invalid.duplicate_name",
		drive_value: "game.calculator.invalid.drive_value",
		armament_value: "game.calculator.invalid.armament_value",
		armament_not_integer: "game.calculator.invalid.armament_not_integer",
		weapons_value: "game.calculator.invalid.weapons_value",
		shields_value: "game.calculator.invalid.shields_value",
		cargo_value: "game.calculator.invalid.cargo_value",
		armament_weapons_pair: "game.calculator.invalid.armament_weapons_pair",
		all_zero: "game.calculator.invalid.all_zero",
	};

	const nameValidation = $derived(
		validateShipClass({ name: cs.name, ...result.blocks }, { existingNames }),
	);
	const createMessage = $derived(
		nameValidation.ok ? "" : i18n.t(nameInvalidKeyMap[nameValidation.reason]),
	);
	const canCreate = $derived(nameValidation.ok && draft !== undefined);
	const canDelete = $derived(
		cs.loadedExisting !== null &&
			existingNames.includes(cs.loadedExisting) &&
			draft !== undefined,
	);

	// Per-block modernization upgrade cost (current tech → target tech).
	const modernCosts = $derived.by(() => {
		if (core === null) return null;
		const weaponsMass = core.weaponsBlockMass({
			weapons: cs.blocks.weapons,
			armament: cs.blocks.armament,
		});
		const rows: { key: TechKey; mass: number }[] = [
			{ key: "drive", mass: cs.blocks.drive },
			{ key: "weapons", mass: weaponsMass ?? 0 },
			{ key: "shields", mass: cs.blocks.shields },
			{ key: "cargo", mass: cs.blocks.cargo },
		];
		const perBlock = rows.map((r) => ({
			key: r.key,
			cost: core.blockUpgradeCost({
				blockMass: r.mass,
				currentTech: cs.techValues[r.key],
				targetTech: cs.targetTech[r.key],
			}),
		}));
		const total = perBlock.reduce((sum, r) => sum + r.cost, 0);
		return { perBlock, total };
	});

	// Display every computed number rounded up to three decimals via the
	// shared `Ceil3` bridge, so a value is never shown lower than it is.
	function fmt(value: number | null | undefined): string {
		if (value === null || value === undefined) {
			return i18n.t("game.calculator.unavailable");
		}
		const rounded = core !== null ? core.ceil3({ value }) : value;
		return rounded.toLocaleString(undefined, { maximumFractionDigits: 3 });
	}

	// The goal-seek back-solved block, shown in its read-only cell, is
	// ceiled the same way (only the claimed block's cell is displayed).
	const resolvedCeil = $derived.by(() => {
		if (core === null) return result.blocks;
		const c = (v: number) => core.ceil3({ value: v });
		return {
			drive: c(result.blocks.drive),
			armament: result.blocks.armament,
			weapons: c(result.blocks.weapons),
			shields: c(result.blocks.shields),
			cargo: c(result.blocks.cargo),
		};
	});

	// A custom load must stay within [0, cargo capacity]; beyond that the
	// ship cannot hold it.
	const customLoadError = $derived.by(() => {
		if (cs.loadMode !== "custom") return "";
		if (cs.customLoad < 0) return i18n.t("game.calculator.invalid.negative");
		if (cs.customLoad > result.cargoCapacity) {
			return i18n.t("game.calculator.invalid.load_over_capacity");
		}
		return "";
	});
	const matError = $derived(
		cs.matValue < 0 ? i18n.t("game.calculator.invalid.negative") : "",
	);

	// Locking a speed back-solves the drive block; with a zero drive the
	// ship is deliberately immobile, so disallow it.
	function lockDisabledReason(output: LockableOutputId): string {
		if (
			(output === "speedEmpty" || output === "speedLoaded") &&
			cs.blocks.drive === 0
		) {
			return i18n.t("game.calculator.lock.no_drive");
		}
		return "";
	}

	function onTechInput(key: TechKey): void {
		cs.techOverridden[key] = true;
	}
	function onResetTech(key: TechKey): void {
		cs.techOverridden[key] = false;
	}
	function onMatInput(): void {
		cs.matOverridden = true;
	}
	function resetMat(): void {
		cs.matOverridden = false;
	}

	function lockOutput(output: LockableOutputId): void {
		if (cs.lock !== null) return;
		cs.lockValue = result.outputs?.[output] ?? 0;
		cs.lock = output;
	}
	function unlock(): void {
		cs.lock = null;
	}

	function loadExisting(clsName: string): void {
		const cls = localShipClass.find((c) => c.name === clsName);
		if (cls === undefined) return;
		cs.blocks = {
			drive: cls.drive,
			armament: cls.armament,
			weapons: cls.weapons,
			shields: cls.shields,
			cargo: cls.cargo,
		};
		cs.name = cls.name;
		cs.loadedExisting = cls.name;
		cs.lock = null;
	}

	// React to the ship-classes table / bottom-tabs asking to load a
	// class (or start a fresh design) into the calculator. The layout
	// flips the sidebar to this tab in parallel.
	$effect(() => {
		const token = calculatorLoadRequest.token;
		if (token === cs.handledLoadToken) return;
		cs.handledLoadToken = token;
		cs.mode = "ship";
		if (calculatorLoadRequest.name === null) cs.resetDesign();
		else loadExisting(calculatorLoadRequest.name);
	});

	async function create(): Promise<void> {
		if (!nameValidation.ok || draft === undefined) return;
		// Capture the validated draft before awaiting: adding the command
		// re-projects `localShipClass`, which re-runs the `nameValidation`
		// derived into a `duplicate_name` failure (the class now exists),
		// leaving `nameValidation.value` undefined after the await.
		const created = nameValidation.value;
		await draft.add({
			kind: "createShipClass",
			id: crypto.randomUUID(),
			name: created.name,
			drive: created.drive,
			armament: created.armament,
			weapons: created.weapons,
			shields: created.shields,
			cargo: created.cargo,
		});
		cs.loadedExisting = created.name;
	}

	async function deleteClass(): Promise<void> {
		if (cs.loadedExisting === null || draft === undefined) return;
		await draft.add({
			kind: "removeShipClass",
			id: crypto.randomUUID(),
			name: cs.loadedExisting,
		});
		cs.loadedExisting = null;
	}

	const LOCK_LABELS: Record<LockableOutputId, string> = $derived({
		emptyMass: i18n.t("game.calculator.out.mass"),
		loadedMass: i18n.t("game.calculator.out.mass"),
		speedEmpty: i18n.t("game.calculator.out.speed"),
		speedLoaded: i18n.t("game.calculator.out.speed"),
		attack: i18n.t("game.calculator.out.attack"),
		defense: i18n.t("game.calculator.out.defense"),
	});
</script>

{#snippet lockable(output: LockableOutputId, value: number | undefined)}
	{#if cs.lock === output}
		<span class="cell locked" class:infeasible={!result.lockFeasible}>
			<input
				type="number"
				step="0.001"
				bind:value={cs.lockValue}
				data-testid={`calculator-locked-${output}`}
				title={result.lockFeasible ? "" : i18n.t("game.calculator.lock.infeasible")}
			/>
			<button
				type="button"
				class="lock active"
				title={i18n.t("game.calculator.lock.reset")}
				aria-label={i18n.t("game.calculator.lock.reset")}
				data-testid={`calculator-unlock-${output}`}
				onclick={unlock}
			>
				🔒
			</button>
		</span>
	{:else}
		{@const extra = lockDisabledReason(output)}
		<span class="cell">
			<span class="val" data-testid={`calculator-out-${output}`}>{fmt(value)}</span>
			<button
				type="button"
				class="lock"
				disabled={cs.lock !== null || value === undefined || extra !== ""}
				title={cs.lock !== null
					? i18n.t("game.calculator.lock.max")
					: extra !== ""
						? extra
						: LOCK_LABELS[output]}
				aria-label={LOCK_LABELS[output]}
				data-testid={`calculator-lock-${output}`}
				onclick={() => lockOutput(output)}
			>
				🔓
			</button>
		</span>
	{/if}
{/snippet}

<section class="calculator" data-testid="sidebar-tool-calculator">
	<div class="modes" role="tablist">
		<button
			type="button"
			class:active={cs.mode === "ship"}
			data-testid="calculator-mode-ship"
			onclick={() => (cs.mode = "ship")}
		>
			{i18n.t("game.calculator.mode.ship")}
		</button>
		<button
			type="button"
			class:active={cs.mode === "modernization"}
			data-testid="calculator-mode-modernization"
			onclick={() => (cs.mode = "modernization")}
		>
			{i18n.t("game.calculator.mode.modernization")}
		</button>
	</div>

	<div class="namebar">
		<input
			type="text"
			class="name"
			list="calculator-existing-classes"
			placeholder={i18n.t("game.calculator.name.placeholder")}
			maxlength="30"
			bind:value={cs.name}
			oninput={() => (cs.loadedExisting = null)}
			onchange={() => loadExisting(cs.name)}
			aria-invalid={nameValidation.ok ? "false" : "true"}
			data-testid="calculator-name"
		/>
		<datalist id="calculator-existing-classes">
			{#each localShipClass as cls (cls.name)}
				<option value={cls.name}></option>
			{/each}
		</datalist>
		{#if cs.mode === "ship"}
			<button
				type="button"
				class="create"
				disabled={!canCreate}
				title={canCreate ? "" : createMessage}
				data-testid="calculator-create"
				onclick={() => void create()}
			>
				{i18n.t("game.calculator.action.create")}
			</button>
		{/if}
	</div>
	{#if cs.mode === "ship" && canDelete}
		<button
			type="button"
			class="delete"
			data-testid="calculator-delete"
			onclick={() => void deleteClass()}
		>
			{i18n.t("game.calculator.action.delete")} {cs.loadedExisting}
		</button>
	{/if}

	<ShipDesignArea
		bind:blocks={cs.blocks}
		resolved={resolvedCeil}
		bind:techs={cs.techValues}
		techOverridden={cs.techOverridden}
		computedInput={result.computedInput}
		{onTechInput}
		{onResetTech}
	/>

	{#if cs.mode === "ship"}
		<div class="load">
			<span class="label">{i18n.t("game.calculator.load.label")}</span>
			<div class="seg" role="group">
				{#each LOAD_MODES as m (m)}
					<button
						type="button"
						class:active={cs.loadMode === m}
						disabled={cargoEmpty}
						data-testid={`calculator-load-${m}`}
						onclick={() => (cs.loadMode = m)}
					>
						{i18n.t(`game.calculator.load.${m}` as TranslationKey)}
					</button>
				{/each}
			</div>
			{#if cs.loadMode === "custom"}
				<input
					type="number"
					step="0.01"
					min="0"
					class="custom-load"
					bind:value={cs.customLoad}
					aria-invalid={customLoadError !== "" ? "true" : "false"}
					title={customLoadError}
					data-testid="calculator-custom-load"
				/>
			{:else if cs.loadMode === "full"}
				<span
					class="full-capacity"
					title={i18n.t("game.calculator.out.cargo_capacity")}
					data-testid="calculator-full-capacity"
				>
					{fmt(result.cargoCapacity)}
				</span>
			{/if}
		</div>

		<div class="results" data-testid="calculator-results">
			<div class="rrow head">
				<span></span>
				<span class="col-head">{i18n.t("game.calculator.col.empty")}</span>
				<span class="col-head">{i18n.t("game.calculator.col.loaded")}</span>
			</div>
			<div class="rrow">
				<span class="label">{i18n.t("game.calculator.out.mass")}</span>
				{@render lockable("emptyMass", result.outputs?.emptyMass)}
				{@render lockable("loadedMass", result.outputs?.loadedMass)}
			</div>
			<div class="rrow">
				<span class="label">{i18n.t("game.calculator.out.speed")}</span>
				{@render lockable("speedEmpty", result.outputs?.speedEmpty)}
				{@render lockable("speedLoaded", result.outputs?.speedLoaded)}
			</div>
			<div class="rrow">
				<span class="label">{i18n.t("game.calculator.out.attack")}</span>
				{@render lockable("attack", result.outputs?.attack)}
				<span></span>
			</div>
			<div class="rrow">
				<span class="label">{i18n.t("game.calculator.out.defense")}</span>
				{@render lockable("defense", result.outputs?.defense)}
				<span></span>
			</div>
			<div class="rrow">
				<span class="label">{i18n.t("game.calculator.out.bombing")}</span>
				<span class="cell">
					<span class="val" data-testid="calculator-out-bombing">
						{fmt(result.outputs?.bombing)}
					</span>
				</span>
				<span></span>
			</div>
			<div class="rrow">
				<span class="label">{i18n.t("game.calculator.out.cargo_capacity")}</span>
				<span class="cell">
					<span class="val" data-testid="calculator-out-cargo-capacity">
						{fmt(result.outputs === null ? null : result.cargoCapacity)}
					</span>
				</span>
				<span></span>
			</div>
		</div>

		<div class="planet" data-testid="calculator-planet-area">
			{#if selectedPlanet === null}
				<p class="hint" data-testid="calculator-planet-none">
					{i18n.t("game.calculator.planet.none")}
				</p>
			{:else}
				<p class="planet-name" data-testid="calculator-planet-name">
					{i18n.t("game.calculator.planet.label", {
						name: selectedPlanet.name,
						number: String(selectedPlanet.number),
					})}
				</p>
				<div class="rrow">
					<span class="label">{i18n.t("game.calculator.planet.mat")}</span>
					<span class="cell">
						<input
							type="number"
							step="0.01"
							min="0"
							bind:value={cs.matValue}
							oninput={onMatInput}
							aria-invalid={matError !== "" ? "true" : "false"}
							title={matError}
							data-testid="calculator-planet-mat"
						/>
						{#if cs.matOverridden}
							<button
								type="button"
								class="lock active"
								title={i18n.t("game.calculator.mat.reset")}
								aria-label={i18n.t("game.calculator.mat.reset")}
								data-testid="calculator-mat-reset"
								onclick={resetMat}
							>
								🔒
							</button>
						{/if}
					</span>
					<span></span>
				</div>
				<dl class="planet-stats">
					<div>
						<dt>{i18n.t("game.calculator.planet.ships_per_turn")}</dt>
						<dd data-testid="calculator-ships-per-turn">
							{fmt(planetBuild?.shipsPerTurn)}
						</dd>
					</div>
					<div>
						<dt>{i18n.t("game.calculator.planet.turns_per_ship")}</dt>
						<dd data-testid="calculator-turns-per-ship">
							{fmt(planetBuild?.turnsPerShip ?? null)}
						</dd>
					</div>
				</dl>
			{/if}
		</div>
	{:else}
		<div class="modern" data-testid="calculator-modernization">
			<div class="rrow head">
				<span></span>
				<span class="col-head">{i18n.t("game.calculator.modern.target")}</span>
				<span class="col-head">{i18n.t("game.calculator.modern.cost")}</span>
			</div>
			{#each modernCosts?.perBlock ?? [] as row (row.key)}
				<div class="rrow">
					<span class="label">{i18n.t(`game.calculator.field.${row.key}` as TranslationKey)}</span>
					<span class="cell">
						<input
							type="number"
							step="0.001"
							min="0"
							bind:value={cs.targetTech[row.key]}
							aria-invalid={cs.targetTech[row.key] < 0 ? "true" : "false"}
							title={cs.targetTech[row.key] < 0
								? i18n.t("game.calculator.invalid.negative")
								: ""}
							data-testid={`calculator-target-${row.key}`}
						/>
					</span>
					<span class="cell">
						<span class="val" data-testid={`calculator-modern-cost-${row.key}`}>
							{fmt(row.cost)}
						</span>
					</span>
				</div>
			{/each}
			<div class="rrow total">
				<span class="label">{i18n.t("game.calculator.modern.total")}</span>
				<span class="cell">
					<span class="val" data-testid="calculator-modern-total">
						{fmt(modernCosts?.total)}
					</span>
				</span>
			</div>
		</div>
	{/if}
</section>

<style>
	.calculator {
		display: flex;
		flex-direction: column;
		gap: 0.6rem;
		padding: 0.75rem;
		font-family: system-ui, sans-serif;
	}
	.modes {
		display: flex;
		gap: 0.25rem;
	}
	.modes button {
		flex: 1;
		font: inherit;
		font-size: 0.8rem;
		padding: 0.25rem;
		background: transparent;
		color: var(--color-text-muted);
		border: 1px solid var(--color-border);
		border-radius: 3px;
		cursor: pointer;
	}
	.modes button.active {
		color: var(--color-text);
		border-color: var(--color-accent);
		background: var(--color-surface-raised);
	}
	.namebar {
		display: flex;
		gap: 0.35rem;
	}
	.name {
		flex: 1;
		min-width: 0;
		font: inherit;
		font-size: 0.85rem;
		padding: 0.25rem 0.4rem;
		background: var(--color-bg);
		color: var(--color-text);
		border: 1px solid var(--color-border);
		border-radius: 3px;
	}
	.name[aria-invalid="true"] {
		border-color: var(--color-danger);
	}
	.create,
	.delete {
		font: inherit;
		font-size: 0.8rem;
		padding: 0.25rem 0.55rem;
		background: transparent;
		color: var(--color-text-muted);
		border: 1px solid var(--color-border);
		border-radius: 3px;
		cursor: pointer;
	}
	.create:not(:disabled):hover {
		color: var(--color-text);
		border-color: var(--color-accent);
	}
	.create:disabled {
		opacity: 0.5;
		cursor: not-allowed;
	}
	.delete {
		color: var(--color-danger);
		align-self: flex-start;
	}
	.load {
		display: flex;
		align-items: center;
		gap: 0.4rem;
	}
	.seg {
		display: flex;
		gap: 0.15rem;
	}
	.seg button {
		font: inherit;
		font-size: 0.75rem;
		padding: 0.15rem 0.4rem;
		background: transparent;
		color: var(--color-text-muted);
		border: 1px solid var(--color-border);
		border-radius: 3px;
		cursor: pointer;
	}
	.seg button.active {
		color: var(--color-text);
		border-color: var(--color-accent);
	}
	.custom-load {
		width: 4rem;
		font: inherit;
		font-size: 0.8rem;
		padding: 0.15rem 0.3rem;
		background: var(--color-bg);
		color: var(--color-text);
		border: 1px solid var(--color-border);
		border-radius: 3px;
	}
	.results,
	.modern {
		display: flex;
		flex-direction: column;
		gap: 0.25rem;
	}
	.rrow {
		display: grid;
		grid-template-columns: 4.5rem 1fr 1fr;
		align-items: center;
		gap: 0.35rem;
	}
	.col-head {
		color: var(--color-text-muted);
		font-size: 0.7rem;
		text-align: center;
	}
	.label {
		color: var(--color-text-muted);
		font-size: 0.8rem;
	}
	.cell {
		display: flex;
		align-items: center;
		gap: 0.2rem;
		justify-content: flex-end;
	}
	.cell .val {
		font-variant-numeric: tabular-nums;
		font-size: 0.85rem;
		text-align: right;
	}
	.cell input {
		width: 100%;
		min-width: 0;
		font: inherit;
		font-size: 0.8rem;
		padding: 0.15rem 0.3rem;
		background: var(--color-bg);
		color: var(--color-text);
		border: 1px solid var(--color-border);
		border-radius: 3px;
		font-variant-numeric: tabular-nums;
		text-align: right;
	}
	.cell.locked input {
		color: var(--color-accent);
		border-color: var(--color-accent);
	}
	.cell.infeasible input {
		border-color: var(--color-danger);
		color: var(--color-danger);
	}
	.lock {
		flex: none;
		padding: 0;
		font-size: 0.7rem;
		line-height: 1;
		background: transparent;
		border: 0;
		cursor: pointer;
		opacity: 0.5;
	}
	.lock.active,
	.lock:not(:disabled):hover {
		opacity: 1;
	}
	.lock:disabled {
		cursor: not-allowed;
		opacity: 0.2;
	}
	.planet {
		border-top: 1px solid var(--color-border-subtle);
		padding-top: 0.5rem;
		display: flex;
		flex-direction: column;
		gap: 0.35rem;
	}
	.hint {
		margin: 0;
		color: var(--color-text-muted);
		font-size: 0.8rem;
	}
	.planet-name {
		margin: 0;
		font-size: 0.8rem;
		color: var(--color-text);
	}
	.planet-stats {
		margin: 0;
		display: grid;
		grid-template-columns: 1fr max-content;
		row-gap: 0.2rem;
		column-gap: 0.5rem;
	}
	.planet-stats div {
		display: contents;
	}
	.planet-stats dt {
		color: var(--color-text-muted);
		font-size: 0.8rem;
	}
	.planet-stats dd {
		margin: 0;
		font-variant-numeric: tabular-nums;
		font-size: 0.85rem;
		text-align: right;
	}
	.rrow.total .label {
		grid-column: 1 / 3;
		color: var(--color-text);
		white-space: nowrap;
	}
	input[aria-invalid="true"] {
		border-color: var(--color-danger);
	}
	.seg button:disabled {
		opacity: 0.4;
		cursor: not-allowed;
	}
	.full-capacity {
		font-variant-numeric: tabular-nums;
		font-size: 0.8rem;
		color: var(--color-accent);
	}
</style>
