diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeChannelGutters.VerticalClearance.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeChannelGutters.VerticalClearance.cs
new file mode 100644
index 000000000..a0075c783
--- /dev/null
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeChannelGutters.VerticalClearance.cs
@@ -0,0 +1,180 @@
+namespace StellaOps.ElkSharp;
+
+///
+/// Ensures adequate vertical (Y-axis) clearance between nodes connected by edges.
+/// The Sugiyama placement uses median-based positioning that can pack connected nodes
+/// too tightly, leaving insufficient Y-space for clean edge routing. This pass shifts
+/// nodes within their layer to create routing-viable corridors.
+///
+/// This is the Y-axis counterpart to the X-axis gutter expansion in
+/// .
+///
+internal static class ElkEdgeVerticalClearance
+{
+ ///
+ /// Minimum Y-clearance between source and target nodes for edges that need
+ /// orthogonal routing space. When nodes overlap or are too close vertically,
+ /// edges must route through their vertical space, causing under-node violations,
+ /// shared-lane conflicts, and short gateway hooks.
+ ///
+ private const double MinEdgeClearanceY = 12d;
+
+ internal static bool EnforceEdgeRoutingClearance(
+ Dictionary positionedNodes,
+ IReadOnlyCollection edges,
+ IReadOnlyDictionary layersByNodeId,
+ IReadOnlyDictionary nodesById,
+ double nodeSpacing,
+ ElkLayoutDirection direction)
+ {
+ if (direction != ElkLayoutDirection.LeftToRight)
+ {
+ return false;
+ }
+
+ // Build layer membership for efficient within-layer adjustment.
+ var nodesByLayer = new Dictionary>();
+ foreach (var (nodeId, layer) in layersByNodeId)
+ {
+ if (!positionedNodes.ContainsKey(nodeId))
+ {
+ continue;
+ }
+
+ if (!nodesByLayer.TryGetValue(layer, out var list))
+ {
+ list = [];
+ nodesByLayer[layer] = list;
+ }
+
+ list.Add(nodeId);
+ }
+
+ // Identify edges where source and target nodes have insufficient Y-clearance.
+ // This covers the root causes: 5px gap (edge/5), 8px gap (edge/25), 22px gap (edge/15),
+ // and identical Y-positions (edge/32+33 sources).
+ var adjustments = new Dictionary(StringComparer.Ordinal);
+
+ foreach (var edge in edges)
+ {
+ if (!positionedNodes.TryGetValue(edge.SourceNodeId, out var sourceNode)
+ || !positionedNodes.TryGetValue(edge.TargetNodeId, out var targetNode))
+ {
+ continue;
+ }
+
+ // Only enforce clearance for edges crossing layers (same-layer edges
+ // have dedicated intra-layer routing).
+ var sourceLayer = layersByNodeId.GetValueOrDefault(edge.SourceNodeId, 0);
+ var targetLayer = layersByNodeId.GetValueOrDefault(edge.TargetNodeId, 0);
+ if (sourceLayer == targetLayer)
+ {
+ continue;
+ }
+
+ // Compute the Y-gap between the two nodes.
+ var sourceBottom = sourceNode.Y + sourceNode.Height;
+ var targetBottom = targetNode.Y + targetNode.Height;
+ var yGap = sourceNode.Y >= targetBottom
+ ? sourceNode.Y - targetBottom
+ : targetNode.Y >= sourceBottom
+ ? targetNode.Y - sourceBottom
+ : 0d; // overlapping ranges → gap = 0
+
+ // Only shift when the gap is extremely tight (< MinEdgeClearanceY).
+ // Wider gaps have adequate routing space and should not be disturbed.
+ if (yGap >= MinEdgeClearanceY)
+ {
+ continue;
+ }
+
+ // Determine which node to shift. Prefer shifting the node in the later layer
+ // to minimize cascade effects on upstream nodes.
+ var shiftNodeId = targetLayer >= sourceLayer ? edge.TargetNodeId : edge.SourceNodeId;
+ var anchorNode = targetLayer >= sourceLayer ? sourceNode : targetNode;
+ var shiftNode = positionedNodes[shiftNodeId];
+
+ // Compute required shift to create MinEdgeClearanceY gap.
+ double requiredShift;
+ if (shiftNode.Y >= anchorNode.Y)
+ {
+ // Shift node is below or at same level — push it down.
+ var currentGap = shiftNode.Y - (anchorNode.Y + anchorNode.Height);
+ requiredShift = Math.Max(0d, MinEdgeClearanceY - currentGap);
+ }
+ else
+ {
+ // Shift node is above — push it up.
+ var currentGap = anchorNode.Y - (shiftNode.Y + shiftNode.Height);
+ requiredShift = -Math.Max(0d, MinEdgeClearanceY - currentGap);
+ }
+
+ if (Math.Abs(requiredShift) < 1d)
+ {
+ continue;
+ }
+
+ // Accumulate the largest required shift per node.
+ var existing = adjustments.GetValueOrDefault(shiftNodeId);
+ if (Math.Abs(requiredShift) > Math.Abs(existing))
+ {
+ adjustments[shiftNodeId] = requiredShift;
+ }
+ }
+
+ if (adjustments.Count == 0)
+ {
+ return false;
+ }
+
+ // Apply shifts individually — no cascade. Only shift if the node
+ // won't overlap with its layer neighbors after the move.
+ var applied = false;
+ foreach (var (shiftNodeId, shift) in adjustments)
+ {
+ if (!positionedNodes.TryGetValue(shiftNodeId, out var node)
+ || !layersByNodeId.TryGetValue(shiftNodeId, out var layer))
+ {
+ continue;
+ }
+
+ var newY = node.Y + shift;
+
+ // Check that the shifted position doesn't overlap with neighbors.
+ if (nodesByLayer.TryGetValue(layer, out var layerNodes))
+ {
+ var wouldOverlap = false;
+ foreach (var neighborId in layerNodes)
+ {
+ if (string.Equals(neighborId, shiftNodeId, StringComparison.Ordinal)
+ || !positionedNodes.TryGetValue(neighborId, out var neighbor))
+ {
+ continue;
+ }
+
+ var newBottom = newY + node.Height;
+ var neighborBottom = neighbor.Y + neighbor.Height;
+ var gap = Math.Min(
+ Math.Abs(newY - neighborBottom),
+ Math.Abs(neighbor.Y - newBottom));
+ if (newY < neighborBottom && neighbor.Y < newBottom && gap < nodeSpacing * 0.5)
+ {
+ wouldOverlap = true;
+ break;
+ }
+ }
+
+ if (wouldOverlap)
+ {
+ continue;
+ }
+ }
+
+ positionedNodes[shiftNodeId] = ElkLayoutHelpers.CreatePositionedNode(
+ nodesById[shiftNodeId], node.X, newY, direction);
+ applied = true;
+ }
+
+ return applied;
+ }
+}