<script lang="ts">
	import { getContext, onDestroy, onMount, tick } from "svelte";
	import { cubicInOut } from "svelte/easing";
	import { fade } from "svelte/transition";
	import NodeCatalog from "/src/components/floating-menus/NodeCatalog.svelte";
	import FloatingMenu from "/src/components/layout/FloatingMenu.svelte";
	import LayoutCol from "/src/components/layout/LayoutCol.svelte";
	import IconButton from "/src/components/widgets/buttons/IconButton.svelte";
	import TextButton from "/src/components/widgets/buttons/TextButton.svelte";
	import TextLabel from "/src/components/widgets/labels/TextLabel.svelte";
	import type { DocumentStore } from "/src/stores/document";
	import type { NodeGraphStore } from "/src/stores/node-graph";
	import { closeContextMenu } from "/src/stores/node-graph";
	import type { SubscriptionsRouter } from "/src/subscriptions-router";
	import type { EditorWrapper, FrontendGraphInput, FrontendGraphOutput, FrontendNode } from "/wrapper/pkg/graphite_wasm_wrapper";

	const GRID_COLLAPSE_SPACING = 10;
	const GRID_SIZE = 24;
	const FADE_TRANSITION = { duration: 200, easing: cubicInOut };

	const editor = getContext<EditorWrapper>("editor");
	const nodeGraph = getContext<NodeGraphStore>("nodeGraph");
	const nodeGraphTransform = nodeGraph.transformStore;
	const nodeGraphImportsExports = nodeGraph.importsExportsStore;
	const visibleNodes = nodeGraph.visibleNodesStore;
	const nodeGraphWires = nodeGraph.wiresStore;
	const documentState = getContext<DocumentStore>("document");
	const subscriptions = getContext<SubscriptionsRouter>("subscriptions");

	let graph: HTMLDivElement | undefined;

	$: gridSpacing = calculateGridSpacing($nodeGraphTransform.scale);
	$: gridDotRadius = 1 + Math.floor($nodeGraphTransform.scale - 0.5 + 0.001) / 2;

	// Close the context menu when the graph view overlay is closed
	$: if (!$documentState.graphViewOverlayOpen) closeContextMenu();

	let inputElement: HTMLInputElement;
	let hoveringImportIndex: number | undefined = undefined;
	let hoveringExportIndex: number | undefined = undefined;

	let editingNameImportIndex: number | undefined = undefined;
	let editingNameExportIndex: number | undefined = undefined;
	let editingNameNodeId: bigint | undefined = undefined;
	let editingNameText = "";

	function exportsToEdgeTextInputWidth() {
		let exportTextDivs = document.querySelectorAll(`[data-export-text-edge]`);
		let exportTextDiv = Array.from(exportTextDivs).find((div) => {
			return div.getAttribute("data-index") === String(editingNameExportIndex);
		});
		if (!graph || !exportTextDiv) return "50px";
		let distance = graph.getBoundingClientRect().right - exportTextDiv.getBoundingClientRect().right;
		return distance - 15 + "px";
	}

	function importsToEdgeTextInputWidth() {
		let importTextDivs = document.querySelectorAll(`[data-import-text-edge]`);
		let importTextDiv = Array.from(importTextDivs).find((div) => {
			return div.getAttribute("data-index") === String(editingNameImportIndex);
		});
		if (!graph || !importTextDiv) return "50px";
		let distance = importTextDiv.getBoundingClientRect().left - graph.getBoundingClientRect().left;
		return distance - 15 + "px";
	}

	function setEditingImportNameIndex(index: number, currentName: string) {
		focusInput(currentName);
		editingNameImportIndex = index;
	}

	function setEditingExportNameIndex(index: number, currentName: string) {
		focusInput(currentName);
		editingNameExportIndex = index;
	}

	async function focusInput(currentName: string) {
		editingNameText = currentName;
		await tick();
		inputElement?.focus();
	}

	function setEditingImportName(event: Event) {
		if (editingNameImportIndex !== undefined) {
			if (!(event.target instanceof HTMLInputElement)) return;
			let text = event.target.value;
			editor.setImportName(editingNameImportIndex, text);
			editingNameImportIndex = undefined;
		}
	}

	function setEditingExportName(event: Event) {
		if (editingNameExportIndex !== undefined) {
			if (!(event.target instanceof HTMLInputElement)) return;
			let text = event.target.value;
			editor.setExportName(editingNameExportIndex, text);
			editingNameExportIndex = undefined;
		}
	}

	function commitEditingNodeName(event: Event) {
		if (editingNameNodeId === undefined || !(event.target instanceof HTMLInputElement)) return;

		editor.setLayerName(editingNameNodeId, event.target.value);
		editingNameNodeId = undefined;
	}

	onMount(() => {
		// Backend dispatches this when the user double-clicks a layer's name area
		subscriptions.subscribeFrontendMessage("TriggerEditLayerNameInGraph", async (data) => {
			const node = $nodeGraph.nodes.get(data.nodeId);
			if (!node) return;

			editingNameText = node.displayName;
			editingNameNodeId = data.nodeId;

			await tick();

			inputElement?.focus();
			inputElement?.select();
		});
	});

	onDestroy(() => {
		subscriptions.unsubscribeFrontendMessage("TriggerEditLayerNameInGraph");
	});

	function calculateGridSpacing(scale: number): number {
		const dense = scale * GRID_SIZE;
		let sparse = dense;

		while (sparse > 0 && sparse < GRID_COLLAPSE_SPACING) {
			sparse *= 2;
		}

		return sparse;
	}

	function createNode(identifier: string) {
		if ($nodeGraph.contextMenuInformation === undefined) return;

		editor.createNode(identifier, $nodeGraph.contextMenuInformation.nodeCreationCoordinates[0], $nodeGraph.contextMenuInformation.nodeCreationCoordinates[1]);
	}

	function nodeBorderMask(nodeWidth: number, primaryInputExists: boolean, exposedSecondaryInputs: number, primaryOutputExists: boolean, exposedSecondaryOutputs: number): string {
		const nodeHeight = Math.max(1 + exposedSecondaryInputs, 1 + exposedSecondaryOutputs) * 24;

		const boxes: { x: number; y: number; width: number; height: number }[] = [];

		// Primary input
		if (primaryInputExists) boxes.push({ x: -8, y: 4, width: 16, height: 16 });
		// Secondary inputs
		for (let i = 0; i < exposedSecondaryInputs; i++) boxes.push({ x: -8, y: 4 + (i + 1) * 24, width: 16, height: 16 });

		// Primary output
		if (primaryOutputExists) boxes.push({ x: nodeWidth - 8, y: 4, width: 16, height: 16 });
		// Exposed outputs
		for (let i = 0; i < exposedSecondaryOutputs; i++) boxes.push({ x: nodeWidth - 8, y: 4 + (i + 1) * 24, width: 16, height: 16 });

		return borderMask(boxes, nodeWidth, nodeHeight);
	}

	function layerBorderMask(nodeWidthFromThumbnail: number, nodeChainAreaLeftExtension: number, hasLeftInputWire: boolean): string {
		const NODE_HEIGHT = 2 * 24;
		const THUMBNAIL_WIDTH = 72 + 8 * 2;
		const FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT = 2;

		const nodeWidth = nodeWidthFromThumbnail + nodeChainAreaLeftExtension;

		const boxes: { x: number; y: number; width: number; height: number }[] = [];

		// Left input
		if (hasLeftInputWire && nodeChainAreaLeftExtension > 0) {
			boxes.push({ x: -8, y: 16, width: 16, height: 16 });
		}

		// Thumbnail
		boxes.push({ x: nodeChainAreaLeftExtension - 8, y: -FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT, width: THUMBNAIL_WIDTH, height: NODE_HEIGHT + FUDGE_HEIGHT_BEYOND_LAYER_HEIGHT * 2 });

		// Right visibility button
		boxes.push({ x: nodeWidth - 12, y: (NODE_HEIGHT - 24) / 2, width: 24, height: 24 });

		return borderMask(boxes, nodeWidth, NODE_HEIGHT);
	}

	function borderMask(boxes: { x: number; y: number; width: number; height: number }[], nodeWidth: number, nodeHeight: number): string {
		const rectangles = boxes.map((box) => `M${box.x},${box.y} L${box.x + box.width},${box.y} L${box.x + box.width},${box.y + box.height} L${box.x},${box.y + box.height}z`);
		return `M-2,-2 L${nodeWidth + 2},-2 L${nodeWidth + 2},${nodeHeight + 2} L-2,${nodeHeight + 2}z ${rectangles.join(" ")}`;
	}

	function nodeNameTooltipLabel(node: FrontendNode): string {
		return node.displayName === node.implementationName ? node.displayName : `${node.displayName} (${node.implementationName})`;
	}

	function validTypesText(value: FrontendGraphInput): string {
		const validTypes = value.validTypes.length > 0 ? value.validTypes.map((x) => `• ${x}`).join("\n") : "None";
		return `Valid Types:\n${validTypes}`;
	}

	function outputConnectedToText(output: FrontendGraphOutput): string {
		return editor.inDevelopmentMode() ? output.connectedTo.join("\n") : "";
	}

	function inputConnectedToText(input: FrontendGraphInput): string {
		return editor.inDevelopmentMode() ? input.connectedTo : "";
	}

	function zipWithUndefined(arr1: FrontendGraphInput[], arr2: FrontendGraphOutput[]) {
		const maxLength = Math.max(arr1.length, arr2.length);
		const result = [];
		for (let i = 0; i < maxLength; i++) {
			result.push([arr1[i], arr2[i]]);
		}
		return result;
	}
</script>

<div class="graph" bind:this={graph} data-node-graph>
	<div
		class="grid-background"
		style:--grid-spacing={`${gridSpacing}px`}
		style:--grid-offset-x={`${$nodeGraphTransform.x}px`}
		style:--grid-offset-y={`${$nodeGraphTransform.y}px`}
		style:--grid-dot-radius={`${gridDotRadius}px`}
	></div>
	<!-- Right click menu for adding nodes -->
	{#if $nodeGraph.contextMenuInformation}
		<FloatingMenu
			class="context-menu"
			data-context-menu
			styles={{
				left: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates[0] * $nodeGraphTransform.scale + $nodeGraphTransform.x}px`,
				top: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates[1] * $nodeGraphTransform.scale + $nodeGraphTransform.y}px`,
			}}
			open={true}
			type="Popover"
			direction="BottomLeft"
		>
			{#if $nodeGraph.contextMenuInformation.contextMenuData.type === "CreateNode"}
				<NodeCatalog initialSearchTerm={$nodeGraph.contextMenuInformation.contextMenuData.data.compatibleType || ""} on:selectNodeType={(e) => createNode(e.detail)} />
			{:else if $nodeGraph.contextMenuInformation.contextMenuData.type === "ModifyNode"}
				<LayoutCol class="modify-node-menu">
					<TextButton
						label="Merge Selected Nodes"
						action={() => {
							editor.mergeSelectedNodes();
							closeContextMenu();
						}}
						flush={true}
					/>
					{@const currentlyIsNode = $nodeGraph.contextMenuInformation.contextMenuData.data.currentlyIsNode}
					<TextButton
						label={currentlyIsNode ? "Display as Layer" : "Display as Node"}
						action={() => {
							if ($nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode") {
								editor.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, currentlyIsNode);
							}
							closeContextMenu();
						}}
						disabled={!$nodeGraph.contextMenuInformation.contextMenuData.data.canBeLayer}
						flush={true}
					/>
					{#if $nodeGraph.contextMenuInformation.contextMenuData.data.hasSelectedLayers}
						{@const allLocked = $nodeGraph.contextMenuInformation.contextMenuData.data.allSelectedLayersLocked}
						{@const nodeId = $nodeGraph.contextMenuInformation.contextMenuData.data.nodeId}
						<TextButton
							label={allLocked ? "Unlock" : "Lock"}
							action={() => {
								if ($nodeGraph.selected.includes(nodeId)) {
									editor.toggleSelectedLocked();
								} else {
									editor.toggleLayerLock(nodeId);
								}
								closeContextMenu();
							}}
							flush={true}
						/>
					{/if}
				</LayoutCol>
			{/if}
		</FloatingMenu>
	{/if}

	{#if $nodeGraph.error}
		<div class="node-error-container" style:transform-origin="0 0" style:transform={`translate(${$nodeGraphTransform.x}px, ${$nodeGraphTransform.y}px) scale(${$nodeGraphTransform.scale})`}>
			<span class="node-error faded" style:left={`${$nodeGraph.error.position[0]}px`} style:top={`${$nodeGraph.error.position[1]}px`} transition:fade={FADE_TRANSITION}>
				{$nodeGraph.error.error}
			</span>
			<span class="node-error hover" style:left={`${$nodeGraph.error.position[0]}px`} style:top={`${$nodeGraph.error.position[1]}px`} transition:fade={FADE_TRANSITION}>
				{$nodeGraph.error.error}
			</span>
		</div>
	{/if}

	<!-- Click target debug visualizations -->
	{#if $nodeGraph.clickTargets}
		<div class="click-targets" style:transform-origin="0 0" style:transform={`translate(${$nodeGraphTransform.x}px, ${$nodeGraphTransform.y}px) scale(${$nodeGraphTransform.scale})`}>
			<svg>
				{#each $nodeGraph.clickTargets.nodeClickTargets as pathString}
					<path class="node" d={pathString} />
				{/each}
				{#each $nodeGraph.clickTargets.layerClickTargets as pathString}
					<path class="layer" d={pathString} />
				{/each}
				{#each $nodeGraph.clickTargets.connectorClickTargets as pathString}
					<path class="connector" d={pathString} />
				{/each}
				{#each $nodeGraph.clickTargets.iconClickTargets as pathString}
					<path class="visibility" d={pathString} />
				{/each}
				<path class="all-nodes-bounding-box" d={$nodeGraph.clickTargets.allNodesBoundingBox} />
				{#each $nodeGraph.clickTargets.modifyImportExport as pathString}
					<path class="modify-import-export" d={pathString} />
				{/each}
			</svg>
		</div>
	{/if}

	<!-- Thick vertical layer connection wires -->
	<div class="wires" style:transform-origin="0 0" style:transform={`translate(${$nodeGraphTransform.x}px, ${$nodeGraphTransform.y}px) scale(${$nodeGraphTransform.scale})`}>
		<svg>
			{#each $nodeGraphWires.values() as map}
				{#each map.values() as { pathString, centerPathString, dataType, thick, dashed }}
					{#if thick}
						<path
							d={pathString}
							style:--data-line-width="8px"
							style:--data-color={`var(--color-data-${dataType.toLowerCase()})`}
							style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
							style:--data-dasharray={`3,${dashed ? 2 : 0}`}
						/>
						<!-- A thin inner line splits the layer-stack wire down the middle reaching past the ends into the cleaved connector slots -->
						<path d={centerPathString} style:--data-line-width="2px" style:--data-color="#444444" style:--data-color-dim="#444444" style:--data-dasharray={`3,${dashed ? 2 : 0}`} />
					{/if}
				{/each}
			{/each}
		</svg>
	</div>

	<!-- Import and Export connectors -->
	<div class="imports-and-exports" style:transform-origin="0 0" style:transform={`translate(${$nodeGraphTransform.x}px, ${$nodeGraphTransform.y}px) scale(${$nodeGraphTransform.scale})`}>
		{#if $nodeGraphImportsExports}
			{#each $nodeGraphImportsExports.imports as frontendOutput, index}
				{#if frontendOutput}
					<svg
						xmlns="http://www.w3.org/2000/svg"
						viewBox="0 0 8 8"
						class="connector"
						data-connector="output"
						data-tooltip-label={frontendOutput.resolvedType}
						data-tooltip-description={outputConnectedToText(frontendOutput)}
						data-datatype={frontendOutput.dataType}
						style:--data-color={`var(--color-data-${frontendOutput.dataType.toLowerCase()})`}
						style:--data-color-dim={`var(--color-data-${frontendOutput.dataType.toLowerCase()}-dim)`}
						style:--offset-left={($nodeGraphImportsExports.importPosition[0] - 8) / 24}
						style:--offset-top={($nodeGraphImportsExports.importPosition[1] - 8) / 24 + index}
					>
						{#if frontendOutput.connectedTo.length > 0}
							<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
						{:else}
							<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
						{/if}
					</svg>

					<div
						on:pointerenter={() => (hoveringImportIndex = index)}
						on:pointerleave={() => (hoveringImportIndex = undefined)}
						class="edit-import-export import"
						class:separator-bottom={index === 0 && $nodeGraphImportsExports.addImportExport}
						class:separator-top={index === 1 && $nodeGraphImportsExports.addImportExport}
						style:--offset-left={($nodeGraphImportsExports.importPosition[0] - 8) / 24}
						style:--offset-top={($nodeGraphImportsExports.importPosition[1] - 8) / 24 + index}
					>
						{#if editingNameImportIndex === index}
							<input
								class="import-text-input"
								type="text"
								style:width={importsToEdgeTextInputWidth()}
								bind:this={inputElement}
								bind:value={editingNameText}
								on:blur={setEditingImportName}
								on:keydown={(e) => e.key === "Enter" && setEditingImportName(e)}
							/>
						{:else}
							<p class="import-text" on:dblclick={() => setEditingImportNameIndex(index, frontendOutput.name)}>
								{frontendOutput.name}
							</p>
						{/if}
						{#if (hoveringImportIndex === index || editingNameImportIndex === index) && $nodeGraphImportsExports.addImportExport}
							<IconButton
								size={16}
								icon="Remove"
								class="remove-button-import"
								data-index={index}
								data-import-text-edge
								action={() => {
									/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
								}}
							/>
							{#if index > 0}
								<div class="reorder-drag-grip" data-tooltip-description="Reorder this export"></div>
							{/if}
						{/if}
					</div>
				{:else}
					<div class="plus" style:--offset-top={($nodeGraphImportsExports.importPosition[1] - 12) / 24} style:--offset-left={($nodeGraphImportsExports.importPosition[0] - 12) / 24}>
						<IconButton size={24} icon="Add" action={() => editor.addPrimaryImport()} />
					</div>
				{/if}
			{/each}

			{#each $nodeGraphImportsExports.exports as frontendInput, index}
				{#if frontendInput}
					<svg
						xmlns="http://www.w3.org/2000/svg"
						viewBox="0 0 8 8"
						class="connector"
						data-connector="input"
						data-tooltip-label={frontendInput.resolvedType}
						data-tooltip-description={inputConnectedToText(frontendInput)}
						data-datatype={frontendInput.dataType}
						style:--data-color={`var(--color-data-${frontendInput.dataType.toLowerCase()})`}
						style:--data-color-dim={`var(--color-data-${frontendInput.dataType.toLowerCase()}-dim)`}
						style:--offset-left={($nodeGraphImportsExports.exportPosition[0] - 8) / 24}
						style:--offset-top={($nodeGraphImportsExports.exportPosition[1] - 8) / 24 + index}
					>
						{#if frontendInput.connectedTo !== "Connected to nothing."}
							<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
						{:else}
							<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
						{/if}
					</svg>
					<div
						on:pointerenter={() => (hoveringExportIndex = index)}
						on:pointerleave={() => (hoveringExportIndex = undefined)}
						class="edit-import-export export"
						class:separator-bottom={index === 0 && $nodeGraphImportsExports.addImportExport}
						class:separator-top={index === 1 && $nodeGraphImportsExports.addImportExport}
						style:--offset-left={($nodeGraphImportsExports.exportPosition[0] - 8) / 24}
						style:--offset-top={($nodeGraphImportsExports.exportPosition[1] - 8) / 24 + index}
					>
						{#if (hoveringExportIndex === index || editingNameExportIndex === index) && $nodeGraphImportsExports.addImportExport}
							{#if index > 0}
								<div class="reorder-drag-grip" data-tooltip-description="Reorder this export"></div>
							{/if}
							<IconButton
								size={16}
								icon="Remove"
								class="remove-button-export"
								data-index={index}
								data-export-text-edge
								action={() => {
									/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
								}}
							/>
						{/if}
						{#if editingNameExportIndex === index}
							<input
								type="text"
								style:width={exportsToEdgeTextInputWidth()}
								bind:this={inputElement}
								bind:value={editingNameText}
								on:blur={setEditingExportName}
								on:keydown={(e) => e.key === "Enter" && setEditingExportName(e)}
							/>
						{:else}
							<p class="export-text" on:dblclick={() => setEditingExportNameIndex(index, frontendInput.name)}>
								{frontendInput.name}
							</p>
						{/if}
					</div>
				{:else}
					<div class="plus" style:--offset-left={($nodeGraphImportsExports.exportPosition[0] - 12) / 24} style:--offset-top={($nodeGraphImportsExports.exportPosition[1] - 12) / 24}>
						<IconButton size={24} icon="Add" action={() => editor.addPrimaryExport()} />
					</div>
				{/if}
			{/each}

			{#if $nodeGraphImportsExports.addImportExport}
				<div
					class="plus"
					style:--offset-left={($nodeGraphImportsExports.importPosition[0] - 12) / 24}
					style:--offset-top={($nodeGraphImportsExports.importPosition[1] - 12) / 24 + $nodeGraphImportsExports.imports.length}
				>
					<IconButton size={24} icon="Add" action={() => editor.addSecondaryImport()} />
				</div>
				<div
					class="plus"
					style:--offset-left={($nodeGraphImportsExports.exportPosition[0] - 12) / 24}
					style:--offset-top={($nodeGraphImportsExports.exportPosition[1] - 12) / 24 + $nodeGraphImportsExports.exports.length}
				>
					<IconButton size={24} icon="Add" action={() => editor.addSecondaryExport()} />
				</div>
			{/if}

			{#if $nodeGraph.reorderImportIndex !== undefined}
				{@const position = {
					x: Number($nodeGraphImportsExports.importPosition[0]),
					y: Number($nodeGraphImportsExports.importPosition[1]) + Number($nodeGraph.reorderImportIndex) * 24,
				}}
				<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 12) / 24}></div>
			{/if}

			{#if $nodeGraph.reorderExportIndex !== undefined}
				{@const position = {
					x: Number($nodeGraphImportsExports.exportPosition[0]),
					y: Number($nodeGraphImportsExports.exportPosition[1]) + Number($nodeGraph.reorderExportIndex) * 24,
				}}
				<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 12) / 24}></div>
			{/if}
		{/if}
	</div>

	<!-- Layers and nodes -->
	<div class="layers-and-nodes" style:transform-origin="0 0" style:transform={`translate(${$nodeGraphTransform.x}px, ${$nodeGraphTransform.y}px) scale(${$nodeGraphTransform.scale})`}>
		<!-- Layers -->
		{#each Array.from($nodeGraph.nodes)
			.filter(([nodeId, node]) => node.isLayer && $visibleNodes.has(nodeId))
			.map(([_, node]) => node) as node (node.id)}
			{@const clipPathId = String(Math.random()).substring(2)}
			{@const stackDataInput = node.exposedInputs[0]}
			{@const layerAreaWidth = $nodeGraph.layerWidths.get(node.id) || 8}
			{@const layerChainWidth = $nodeGraph.chainWidths.get(node.id) || 0}
			{@const hasLeftInputWire = $nodeGraph.hasLeftInputWire.get(node.id) || false}
			{@const description = node.reference ? $nodeGraph.nodeDescriptions.get(node.reference) : undefined}
			<div
				class="layer"
				class:selected={$nodeGraph.selected.includes(node.id)}
				class:in-selected-network={$nodeGraph.inSelectedNetwork}
				class:previewed={node.previewed}
				class:disabled={!node.visible}
				class:locked={node.locked}
				style:--offset-left={node.position?.[0] || 0}
				style:--offset-top={node.position?.[1] || 0}
				style:--clip-path-id={`url(#${clipPathId})`}
				style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
				style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
				style:--layer-area-width={layerAreaWidth}
				style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0}
				data-tooltip-label={nodeNameTooltipLabel(node)}
				data-tooltip-description={`
					${(description || "").trim()}${editor.inDevelopmentMode() ? `\n\n*ID: ${node.id}. Position: (${node.position[0]}, ${node.position[1]}).*` : ""}
					`.trim()}
				data-node={node.id}
			>
				<div class="thumbnail">
					{#if $nodeGraph.thumbnails.has(node.id)}
						{@html $nodeGraph.thumbnails.get(node.id)}
					{/if}
					<!-- Layer stacking top output -->
					{#if node.primaryOutput}
						<svg
							xmlns="http://www.w3.org/2000/svg"
							viewBox="0 0 8 12"
							class="connector top"
							data-connector="output"
							data-tooltip-label={node.primaryOutput.resolvedType}
							data-tooltip-description={outputConnectedToText(node.primaryOutput)}
							data-datatype={node.primaryOutput.dataType}
							style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`}
							style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`}
						>
							{#if node.primaryOutput.connectedTo.length > 0}
								<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color)" />
								{#if node.primaryOutputConnectedToLayer}
									<path
										d="M0,4.5 C0,4.5 0,-3.5 0,-3.5 C0,-3.5 3,-3.5 3,-3.5 C3,-3.5 3,2.565 3,2.565 C2.834,2.632 2.673,2.717 2.52,2.819 C2.52,2.819 0,4.5 0,4.5 ZM5,2.566 C5,2.566 5,-3.5 5,-3.5 C5,-3.5 8,-3.5 8,-3.5 C8,-3.5 8,4.5 8,4.5 C8,4.5 5.479,2.819 5.479,2.819 C5.326,2.717 5.166,2.633 5,2.566 Z"
										fill="var(--data-color-dim)"
									/>
								{/if}
							{:else}
								<path d="M0,6.953l2.521,-1.694a2.649,2.649,0,0,1,2.959,0l2.52,1.694v5.047h-8z" fill="var(--data-color-dim)" />
							{/if}
						</svg>
					{/if}
					<!-- Layer stacking bottom input -->
					<svg
						xmlns="http://www.w3.org/2000/svg"
						viewBox="0 0 8 12"
						class="connector bottom"
						data-connector="input"
						data-tooltip-label={node.primaryInput ? node.primaryInput.resolvedType : ""}
						data-tooltip-description={node.primaryInput ? `${validTypesText(node.primaryInput).trim()}\n\n${inputConnectedToText(node.primaryInput)}` : ""}
						data-datatype={node.primaryInput?.dataType}
						style:--data-color={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()})`}
						style:--data-color-dim={`var(--color-data-${(node.primaryInput?.dataType || "General").toLowerCase()}-dim)`}
					>
						{#if node.primaryInput?.connectedTo !== "Connected to nothing."}
							<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
							{#if node.primaryInputConnectedToLayer}
								<path
									d="M2.512,9.26 C2.673,9.157 2.834,9.072 3,9 C3,9 3,16 3,16 C3,16 0,16 0,16 C0,16 0,10.95 0,10.95 C0,10.95 2.512,9.26 2.512,9.26 ZM5,16 C5,16 5,9 5,9 C5.166,9.073 5.327,9.158 5.48,9.26 C5.48,9.26 8,10.95 8,10.95 C8,10.95 8,16 8,16 C8,16 5,16 5,16 Z"
									fill="var(--data-color-dim)"
								/>
							{/if}
						{:else}
							<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color-dim)" />
						{/if}
					</svg>
				</div>
				<!-- Layer input connector (from left) -->
				{#if node.exposedInputs.length > 0}
					<div class="input connectors">
						<svg
							xmlns="http://www.w3.org/2000/svg"
							viewBox="0 0 8 8"
							class="connector"
							data-tooltip-label={stackDataInput.resolvedType}
							data-tooltip-description={`${validTypesText(stackDataInput).trim()}\n\n${inputConnectedToText(stackDataInput)}`}
							data-connector="input"
							data-datatype={stackDataInput.dataType}
							style:--data-color={`var(--color-data-${stackDataInput.dataType.toLowerCase()})`}
							style:--data-color-dim={`var(--color-data-${stackDataInput.dataType.toLowerCase()}-dim)`}
						>
							{#if stackDataInput.connectedTo !== undefined}
								<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
							{:else}
								<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
							{/if}
						</svg>
					</div>
				{/if}
				<div class="details">
					{#if editingNameNodeId === node.id}
						<input
							class="layer-name-input"
							type="text"
							bind:this={inputElement}
							bind:value={editingNameText}
							on:pointerdown|stopPropagation
							on:dblclick|stopPropagation
							on:blur={commitEditingNodeName}
							on:keydown={(e) => {
								// Stop propagation when we handle the key ourselves so the global keyboard forwarder (`shouldRedirectKeyboardEventToBackend`) doesn't also dispatch them.
								// Its Escape carve-out would otherwise close the graph view, and Enter could trigger unrelated bindings.
								if (e.key === "Enter") {
									commitEditingNodeName(e);
									e.stopPropagation();
								} else if (e.key === "Escape") {
									editingNameNodeId = undefined;
									e.stopPropagation();
								}
							}}
						/>
					{:else}
						<TextLabel>{node.displayName}</TextLabel>
					{/if}
				</div>
				<div class="solo-drag-grip" data-tooltip-description="Drag only this layer without pushing others outside the stack"></div>
				{#if node.locked}
					<IconButton
						class="lock"
						data-lock-button
						size={24}
						icon="PadlockLocked"
						hoverIcon="PadlockUnlocked"
						action={() => {
							/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
						}}
						tooltipLabel="Unlock"
					/>
				{/if}
				<IconButton
					class="visibility"
					data-visibility-button
					size={24}
					icon={node.visible ? "EyeVisible" : "EyeHidden"}
					hoverIcon={node.visible ? "EyeHide" : "EyeShow"}
					action={() => {
						/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
					}}
					tooltipLabel={node.visible ? "Hide" : "Show"}
				/>

				<svg class="border-mask" width="0" height="0">
					<defs>
						<clipPath id={clipPathId}>
							<!-- Keep this equation in sync with the equivalent one in the CSS rule for `.layer { width: ... }` below -->
							<path clip-rule="evenodd" d={layerBorderMask(24 * layerAreaWidth - 12, layerChainWidth ? (0.5 + layerChainWidth) * 24 : 0, hasLeftInputWire)} />
						</clipPath>
					</defs>
				</svg>
			</div>
		{/each}

		<!-- Node connection wires -->
		<div class="wires">
			<svg>
				{#each $nodeGraphWires.values() as map}
					{#each map.values() as { pathString, dataType, thick, dashed, isList }}
						{#if !thick}
							{#if isList}
								<!-- A rank-1 List wire reads as two parallel lines: a triple-width data line split down the middle by a 1x background-colored overlay -->
								<path
									d={pathString}
									style:--data-line-width="4px"
									style:--data-color={`var(--color-data-${dataType.toLowerCase()})`}
									style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
									style:--data-dasharray={`3,${dashed ? 2 : 0}`}
								/>
								<path d={pathString} style:--data-line-width="2px" style:--data-color="#444444" style:--data-color-dim="#444444" style:--data-dasharray={`3,${dashed ? 2 : 0}`} />
							{:else}
								<path
									d={pathString}
									style:--data-line-width="2px"
									style:--data-color={`var(--color-data-${dataType.toLowerCase()})`}
									style:--data-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
									style:--data-dasharray={`3,${dashed ? 2 : 0}`}
								/>
							{/if}
						{/if}
					{/each}
				{/each}
				{#if $nodeGraph.wirePathInProgress}
					<path
						d={$nodeGraph.wirePathInProgress?.pathString}
						style:--data-line-width={`${$nodeGraph.wirePathInProgress.thick ? 8 : 2}px`}
						style:--data-color={`var(--color-data-${$nodeGraph.wirePathInProgress.dataType.toLowerCase()})`}
						style:--data-color-dim={`var(--color-data-${$nodeGraph.wirePathInProgress.dataType.toLowerCase()}-dim)`}
						style:--data-dasharray={`3,${$nodeGraph.wirePathInProgress.dashed ? 2 : 0}`}
					/>
				{/if}
			</svg>
		</div>

		<!-- Nodes -->
		{#each Array.from($nodeGraph.nodes)
			.filter(([nodeId, node]) => !node.isLayer && $visibleNodes.has(nodeId))
			.map(([_, node]) => node) as node (node.id)}
			{@const exposedInputsOutputs = zipWithUndefined(node.exposedInputs, node.exposedOutputs)}
			{@const clipPathId = String(Math.random()).substring(2)}
			{@const description = node.reference ? $nodeGraph.nodeDescriptions.get(node.reference) : undefined}
			<div
				class="node"
				class:selected={$nodeGraph.selected.includes(node.id)}
				class:previewed={node.previewed}
				class:disabled={!node.visible}
				style:--offset-left={node.position?.[0] || 0}
				style:--offset-top={node.position?.[1] || 0}
				style:--clip-path-id={`url(#${clipPathId})`}
				style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
				style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
				data-tooltip-label={nodeNameTooltipLabel(node)}
				data-tooltip-description={`
					${(description || "").trim()}${editor.inDevelopmentMode() ? `\n\n*ID: ${node.id}. Position: (${node.position[0]}, ${node.position[1]}).*` : ""}
					`.trim()}
				data-node={node.id}
			>
				<!-- Primary row -->
				<div class="primary" class:in-selected-network={$nodeGraph.inSelectedNetwork} class:no-secondary-section={exposedInputsOutputs.length === 0}>
					<!-- TODO: Allow the user to edit the name, just like in the Layers panel -->
					<TextLabel>{node.displayName}</TextLabel>
				</div>
				<!-- Secondary rows -->
				{#if exposedInputsOutputs.length > 0}
					<div class="secondary" class:in-selected-network={$nodeGraph.inSelectedNetwork}>
						{#each exposedInputsOutputs as [input, output]}
							<div class={`secondary-row expanded ${input !== undefined ? "input" : "output"}`}>
								<TextLabel tooltipLabel={input !== undefined ? input.name : output.name} tooltipDescription={input !== undefined ? input.description : output.description}>
									{input !== undefined ? input.name : output.name}
								</TextLabel>
							</div>
						{/each}
					</div>
				{/if}
				<!-- Input connectors -->
				<div class="input connectors">
					{#if node.primaryInput?.dataType}
						<svg
							xmlns="http://www.w3.org/2000/svg"
							viewBox="0 0 8 8"
							class="connector primary-connector"
							data-connector="input"
							data-tooltip-label={node.primaryInput.resolvedType}
							data-tooltip-description={`${validTypesText(node.primaryInput).trim()}\n\n${inputConnectedToText(node.primaryInput)}`}
							data-datatype={node.primaryInput?.dataType}
							style:--data-color={`var(--color-data-${node.primaryInput.dataType.toLowerCase()})`}
							style:--data-color-dim={`var(--color-data-${node.primaryInput.dataType.toLowerCase()}-dim)`}
						>
							{#if node.primaryInput.connectedTo !== undefined}
								<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
							{:else}
								<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
							{/if}
						</svg>
					{/if}
					{#each node.exposedInputs as secondary, index}
						{#if index < node.exposedInputs.length}
							<svg
								xmlns="http://www.w3.org/2000/svg"
								viewBox="0 0 8 8"
								class="connector"
								data-connector="input"
								data-tooltip-label={secondary.resolvedType}
								data-tooltip-description={`${validTypesText(secondary).trim()}\n\n${inputConnectedToText(secondary)}`}
								data-datatype={secondary.dataType}
								style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`}
								style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
							>
								{#if secondary.connectedTo !== undefined}
									<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
								{:else}
									<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
								{/if}
							</svg>
						{/if}
					{/each}
				</div>
				<!-- Output connectors -->
				<div class="output connectors">
					{#if node.primaryOutput}
						<svg
							xmlns="http://www.w3.org/2000/svg"
							viewBox="0 0 8 8"
							class="connector primary-connector"
							data-connector="output"
							data-tooltip-label={node.primaryOutput.resolvedType}
							data-tooltip-description={`${outputConnectedToText(node.primaryOutput)}`}
							data-datatype={node.primaryOutput.dataType}
							style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`}
							style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`}
						>
							{#if node.primaryOutput.connectedTo !== undefined}
								<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
							{:else}
								<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
							{/if}
						</svg>
					{/if}
					{#each node.exposedOutputs as secondary}
						<svg
							xmlns="http://www.w3.org/2000/svg"
							viewBox="0 0 8 8"
							class="connector"
							data-connector="output"
							data-tooltip-label={secondary.resolvedType}
							data-tooltip-description={`${outputConnectedToText(secondary)}`}
							data-datatype={secondary.dataType}
							style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`}
							style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
						>
							{#if secondary.connectedTo !== undefined}
								<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
							{:else}
								<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
							{/if}
						</svg>
					{/each}
				</div>
				<svg class="border-mask" width="0" height="0">
					<defs>
						<clipPath id={clipPathId}>
							<path
								clip-rule="evenodd"
								d={nodeBorderMask(120, node.primaryInput?.dataType !== undefined, node.exposedInputs.length, node.primaryOutput !== undefined, node.exposedOutputs.length)}
							/>
						</clipPath>
					</defs>
				</svg>
			</div>
		{/each}
	</div>
</div>

<!-- Box selection widget -->
{#if $nodeGraph.box}
	<div
		class="box-selection"
		style:left={`${Math.min($nodeGraph.box.startX, $nodeGraph.box.endX)}px`}
		style:top={`${Math.min($nodeGraph.box.startY, $nodeGraph.box.endY)}px`}
		style:width={`${Math.abs($nodeGraph.box.startX - $nodeGraph.box.endX)}px`}
		style:height={`${Math.abs($nodeGraph.box.startY - $nodeGraph.box.endY)}px`}
	></div>
{/if}

<style lang="scss">
	.graph {
		position: relative;
		overflow: hidden;
		display: flex;
		flex-direction: row;
		flex-grow: 1;

		.grid-background {
			position: absolute;
			width: 100%;
			height: 100%;
			pointer-events: none;

			// We're displaying the dotted grid in a pseudo-element because `image-rendering` is an inherited property and we don't want it to apply to child elements
			&::before {
				content: "";
				position: absolute;
				width: 100%;
				height: 100%;
				background-size: var(--grid-spacing) var(--grid-spacing);
				background-position: calc(var(--grid-offset-x) - var(--grid-dot-radius)) calc(var(--grid-offset-y) - var(--grid-dot-radius));
				background-image: radial-gradient(circle at var(--grid-dot-radius) var(--grid-dot-radius), var(--color-3-darkgray) var(--grid-dot-radius), transparent 0);
				background-repeat: repeat;
				image-rendering: pixelated;
				mix-blend-mode: screen;
			}
		}

		> img {
			position: absolute;
			bottom: 0;
		}

		.breadcrumb-trail-buttons {
			margin-top: 8px;
			margin-left: 8px;
		}

		.context-menu {
			width: max-content;

			.modify-node-menu {
				margin: -4px;

				.text-button {
					justify-content: left;
				}
			}

			.tail {
				display: none;
			}
		}

		.node-error-container {
			position: absolute;
			z-index: 1;

			.node-error {
				position: absolute;
				width: max-content;
				white-space: pre-wrap;
				max-width: 600px;
				line-height: 18px;
				color: var(--color-2-mildblack);
				background: var(--color-error-red);
				padding: 8px;
				border-radius: 4px;
				transition: opacity 0.2s;
				opacity: 0.5;
				transform: translateY(-100%);

				// Tail
				&::after {
					content: "";
					position: absolute;
					left: 6px;
					bottom: -8px;
					width: 0;
					height: 0;
					border-style: solid;
					border-width: 8px 6px 0 6px;
					border-color: var(--color-error-red) transparent transparent transparent;
				}

				&.hover {
					opacity: 0;
					z-index: 1;
					pointer-events: none;
				}

				&.faded:hover + .hover {
					opacity: 1;
				}

				&.faded:hover {
					z-index: 2;
					opacity: 1;
					user-select: text;
					transition:
						opacity 0.2s,
						z-index 0s 0.2s;

					&::selection {
						background-color: var(--color-e-nearwhite);

						// Target only Safari
						@supports (background: -webkit-named-image(i)) {
							& {
								// Setting an alpha value opts out of Safari's "fancy" (but not visible on dark backgrounds) selection highlight rendering
								// https://stackoverflow.com/a/71753552/775283
								background-color: rgb(from var(--color-e-nearwhite) r g b / calc(254 / 255));
							}
						}
					}
				}
			}
		}

		.click-targets {
			position: absolute;
			pointer-events: none;
			width: 100%;
			height: 100%;
			z-index: 10;

			svg {
				overflow: visible;
				width: 100%;
				height: 100%;
				stroke-width: 1;
				fill: none;

				.layer {
					stroke: yellow;
				}

				.node {
					stroke: blue;
				}

				.connector {
					stroke: green;
				}

				.visibility {
					stroke: red;
				}

				.all-nodes-bounding-box {
					stroke: purple;
				}

				.modify-import-export {
					stroke: orange;
				}
			}
		}

		.wires {
			pointer-events: none;
			position: absolute;
			width: 100%;
			height: 100%;

			svg {
				width: 100%;
				height: 100%;
				overflow: visible;

				path {
					fill: none;
					stroke: var(--data-color-dim);
					stroke-width: var(--data-line-width);
					stroke-dasharray: var(--data-dasharray);
				}
			}
		}

		.imports-and-exports {
			width: 100%;
			height: 100%;
			position: absolute;
			pointer-events: none;
			// Keeps the connectors above the wires
			z-index: 1;

			// Zero specificity with `:where()` to allow other rules to override `pointer-events`
			:where(.graph-view.open .graph .imports-and-exports > *) {
				pointer-events: auto;
			}

			.connector {
				position: absolute;
				width: 8px;
				height: 8px;
				margin-top: 4px;
				margin-left: 5px;
				top: calc(var(--offset-top) * 24px);
				left: calc(var(--offset-left) * 24px);
			}

			.reorder-bar {
				position: absolute;
				top: calc(var(--offset-top) * 24px);
				left: calc(var(--offset-left) * 24px);
				width: 50px;
				height: 2px;
				background: white;
			}

			.plus {
				position: absolute;
				top: calc(var(--offset-top) * 24px);
				left: calc(var(--offset-left) * 24px);
			}

			.edit-import-export {
				position: absolute;
				display: flex;
				align-items: center;
				top: calc(var(--offset-top) * 24px);
				margin-top: -5px;
				height: 24px;

				&.separator-bottom::after,
				&.separator-top::before {
					content: "";
					position: absolute;
					background: var(--color-8-uppergray);
					height: 1px;
					left: -4px;
					right: -4px;
				}

				&.separator-bottom::after {
					bottom: -1px;
				}

				&.separator-top::before {
					top: 0;
				}

				&.import {
					right: calc(100% - var(--offset-left) * 24px);
				}

				&.export {
					left: calc(var(--offset-left) * 24px + 17px);
				}

				.import-text {
					text-align: right;
					text-wrap: nowrap;
				}

				.export-text {
					text-wrap: nowrap;
				}

				.import-text-input {
					text-align: right;
				}

				.remove-button-import {
					margin-left: 3px;
				}

				.remove-button-export {
					margin-right: 3px;
				}

				.reorder-drag-grip {
					width: 8px;
					height: 24px;
					background-position: 2px 8px;
					border-radius: 2px;
					margin: -6px 0;
					background-image: var(--icon-drag-grip-hover);
				}
			}
		}

		.layers-and-nodes {
			position: absolute;
			pointer-events: none;
			width: 100%;
			height: 100%;

			// Zero specificity with `:where()` to allow other rules to override `pointer-events`
			:where(.graph-view.open .graph .layers-and-nodes > *) {
				pointer-events: auto;
			}
		}

		.layer,
		.node {
			position: absolute;
			display: flex;
			left: calc(var(--offset-left) * 24px);
			top: calc(var(--offset-top) * 24px);
			// TODO: Reenable the `transition` property below after dealing with all edge cases where the wires need to be updated until the transition is complete
			// transition: top 0.1s cubic-bezier(0, 0, 0.2, 1), left 0.1s cubic-bezier(0, 0, 0.2, 1); // Update `DRAG_SMOOTHING_TIME` in the JS above.
			// TODO: Reenable the `backdrop-filter` property once a solution can be found for the black whole-page flickering problems it causes in Chrome.
			// TODO: Additionally, find a solution for this having no effect in Firefox due to a browser bug caused when the two
			// ancestor elements, `.graph` and `.panel`, each have the simultaneous pairing of `overflow: hidden` and `border-radius`.
			// See: https://stackoverflow.com/questions/75137879/bug-with-backdrop-filter-in-firefox
			// backdrop-filter: blur(4px);
			background: rgb(from var(--color-0-black) r g b / 0.33);

			&::after {
				content: "";
				position: absolute;
				box-sizing: border-box;
				top: 0;
				left: 0;
				width: 100%;
				height: 100%;
				pointer-events: none;
				clip-path: var(--clip-path-id);
			}

			.border-mask {
				position: absolute;
				top: 0;
			}

			&.disabled {
				background: rgb(from var(--color-4-dimgray) r g b / 0.33);
				color: var(--color-a-softgray);

				.icon-label {
					fill: var(--color-a-softgray);
				}
			}

			&.previewed::after {
				border: 1px dashed var(--data-color);
			}

			.connectors {
				position: absolute;
				// Keeps the connectors above the wires
				z-index: 1;

				&.input {
					left: -3px;
				}

				&.output {
					right: -5px;
				}
			}

			.connector {
				// Double the intended value because of margin collapsing, but for the first and last we divide it by two as intended
				margin: calc(24px - 8px) 0;
				width: 8px;
				height: 8px;
			}

			.text-label {
				overflow: hidden;
				text-overflow: ellipsis;
			}
		}

		.layer {
			border-radius: 8px;
			--extra-width-to-reach-grid-multiple: 8px;
			--node-chain-area-left-extension: 0;
			// Keep this equation in sync with the equivalent one in the Svelte template `<clipPath><path d="layerBorderMask(...)" /></clipPath>` above, as well as the `left` connector offset CSS rule above in `.connectors.input` above.
			width: calc((var(--layer-area-width) - 0.5) * 24px);
			padding-left: calc(var(--node-chain-area-left-extension) * 24px);
			margin-left: calc((0.5 - var(--node-chain-area-left-extension)) * 24px);

			&::after {
				border: 1px solid var(--color-5-dullgray);
				border-radius: 8px;
			}

			&.selected {
				background: rgb(from var(--color-5-dullgray) r g b / 0.33);

				&.in-selected-network {
					background: rgb(from var(--color-6-lowergray) r g b / 0.33);
				}
			}

			.thumbnail {
				background: var(--color-2-mildblack);
				border: 1px solid var(--data-color-dim);
				border-radius: 2px;
				position: relative;
				box-sizing: border-box;
				height: 48px;
				// We shorten the width by 1px on the left and right so the inner thumbnail graphic maintains a perfect 3:2 aspect ratio
				width: calc(72px - 2px);
				margin: 0 1px;

				&::before {
					content: "";
					background-image: var(--color-transparent-checkered-background);
					background-size: var(--color-transparent-checkered-background-size);
					background-position: var(--color-transparent-checkered-background-position);
					background-repeat: var(--color-transparent-checkered-background-repeat);
				}

				&::before,
				svg:not(.connector) {
					pointer-events: none;
					position: absolute;
					margin: auto;
					top: 1px;
					left: 1px;
					width: calc(100% - 2px);
					height: calc(100% - 2px);
				}

				.connector {
					position: absolute;
					margin: 0 auto;
					left: 0;
					right: 0;
					height: 12px;

					&.top {
						top: -13px;
					}

					&.bottom {
						bottom: -13px;
					}
				}
			}

			.details {
				display: flex;
				align-items: center;
				margin: 0 8px;

				.text-label {
					white-space: nowrap;
					line-height: 48px;
				}

				.layer-name-input {
					color: inherit;
					background: var(--color-1-nearblack);
					border: none;
					outline: none;
					margin: 0 -4px;
					padding: 0 4px;
					height: 24px;
					border-radius: 2px;
					field-sizing: content;
					// Stack above the absolutely-positioned grip/lock/visibility siblings, which can otherwise overlap the input's right edge and hijack clicks there.
					position: relative;
					z-index: 1;
				}
			}

			.solo-drag-grip {
				width: 8px;
				height: 24px;
				background-position: 2px 8px;
				right: calc(-12px + 24px);
				border-radius: 2px;
			}

			&.locked .solo-drag-grip {
				right: calc(-12px + 24px + 24px);
			}

			.solo-drag-grip:hover,
			&.selected .solo-drag-grip {
				background-image: var(--icon-drag-grip);

				&:hover {
					background-image: var(--icon-drag-grip-hover);
				}
			}

			.visibility {
				right: -12px;
			}

			.lock {
				right: 12px;
			}

			.input.connectors {
				left: calc(-3px + var(--node-chain-area-left-extension) * 24px - 36px);
			}

			.solo-drag-grip,
			.lock,
			.visibility,
			.input.connectors,
			.input.connectors .connector {
				position: absolute;
				margin: auto 0;
				top: 0;
				bottom: 0;
			}

			.input.connectors .connector {
				left: 24px;
			}
		}

		.node {
			flex-direction: column;
			border-radius: 2px;
			width: 120px;
			top: calc((var(--offset-top) + 0.5) * 24px);

			&::after {
				border: 1px solid var(--data-color-dim);
				border-radius: 2px;
			}

			&.selected {
				.primary {
					background: rgb(from var(--color-f-white) r g b / 0.15);

					&.in-selected-network {
						background: rgb(from var(--color-f-white) r g b / 0.2);
					}
				}

				.secondary {
					background: rgb(from var(--color-f-white) r g b / 0.1);

					&.in-selected-network {
						background: rgb(from var(--color-f-white) r g b / 0.15);
					}
				}
			}

			.connector {
				&:first-of-type {
					margin-top: calc((24px - 8px) / 2);

					&:not(.primary-connector) {
						margin-top: calc((24px - 8px) / 2 + 24px);
					}
				}

				&:last-of-type {
					margin-bottom: calc((24px - 8px) / 2);
				}
			}

			.primary {
				display: flex;
				align-items: center;
				position: relative;
				width: 100%;
				height: 24px;
				border-radius: 2px 2px 0 0;
				background: rgb(from var(--color-f-white) r g b / 0.05);

				&.no-secondary-section {
					border-radius: 2px;
				}

				.icon-label {
					display: none; // Remove after we have unique icons for the nodes
					margin: 0 8px;
				}

				.text-label {
					// margin-right: 4px; // Restore after reenabling icon-label
					margin: 0 8px;
				}
			}

			.secondary {
				display: flex;
				flex-direction: column;
				width: 100%;
				position: relative;

				.secondary-row {
					position: relative;
					display: flex;
					align-items: center;
					margin: 0 8px;
					width: calc(100% - 8px - 8px);
					height: 24px;

					&:last-of-type {
						border-radius: 0 0 2px 2px;
					}

					.text-label {
						width: 100%;
					}

					&.output {
						flex-direction: row-reverse;
						text-align: right;

						svg {
							width: 30px;
							height: 20px;
						}
					}
				}

				&::before {
					left: 0;
				}

				&::after {
					right: 0;
				}
			}
		}
	}

	.box-selection {
		position: absolute;
		pointer-events: none;
		z-index: 2;
		// TODO: This will be removed after box selection, and all of graph rendering, is moved to the backend and this whole file
		// is removed, but for now this color needs to stay in sync with `COLOR_OVERLAY_BLUE` set in consts.rs of the editor backend.
		background: rgba(0, 168, 255, 0.05);
		border: 1px solid #00a8ff;
	}
</style>
