/* blueprint-nodes.css
 * ====================
 * Canonical CSS for the blueprint node rendering used by the editor.
 */

/* ═══════════════════════════════════════════════════════════════════════════
 * MASTER NODE CSS VARIABLES - Fonte di verità per tutte le dimensioni
 * Modifica QUI per cambiare tutti i nodi figli automaticamente
 * ═══════════════════════════════════════════════════════════════════════════ */
:root {
  /* Colors */
  --bg-primary: #0f0f1a;
  --bg-secondary: #1a1a2e;
  --bg-tertiary: #252540;
  --text-primary: #f1f5f9;
  --text-secondary: #94a3b8;
  --text-muted: #64748b;
  --border: #334155;
  --border-light: #475569;
  
  /* MASTER NODE - Layout Dimensions */
  --node-label-min-width: 70px;   /* Larghezza minima label (es. "Type:") */
  --node-label-max-width: 180px;  /* Larghezza massima label (es. "Random Type:") */
  --node-cell-gap: 0px;           /* Gap tra celle nella riga */
  --node-cell-padding-x: 12px;    /* Padding orizzontale celle */
  --node-cell-padding-x-tight: 4px; /* Padding orizzontale ridotto (label+dropdown) */
  --node-cell-padding-y: 8px;     /* Padding verticale celle */
  --node-row-height: 35px;        /* Altezza riga content */
  --node-row-height-compact: 30px; /* Altezza riga compatta */
  --node-multiline-row-height: 70px; /* Altezza minima per righe multilinea esplicative */
  --node-textarea-height: 80px;   /* Altezza riga textarea - aumentata per dialogue */
  --node-textarea-padding: 6px;   /* Padding sopra/sotto la textarea */
  --node-header-height: 41px;     /* Altezza header */
  --node-footer-height: 20px;     /* Altezza footer */
  --node-border-radius: 8px;      /* Border radius generale */
  --node-header-icon-width: 36px; /* Larghezza icona header */
  --node-header-btn-width: 36px;  /* Larghezza bottone header */
  --node-min-width: 250px;        /* Larghezza minima nodo */

  /* M3 Physical P5.5.2 revision (R1, per the challenge-review reconciliation): the outer frame's own
     border-width, named so the internal-divider inset below can be DERIVED from it instead of standing as
     an independent literal that could silently drift out of sync if the frame's border-width ever changes. */
  --node-frame-border-width: 2px;
  /* Internal divider inset from each row's own edge: the frame's border-width (its inward extent from the
     28px shell inset) plus a fixed 4px clearance margin, so the divider's endpoint always lands measurably
     past the frame's inner edge, not just "6px" by coincidence. */
  --node-separator-inset: calc(var(--node-frame-border-width) + 4px);
}

/* M3 Physical P5: `.blueprint-node` is the class every node's HOST element (the plain HTML overlay div
 * replacing the old <g class="node node-{type} blueprint-node">/<foreignObject> pair -- see
 * NodeBlueprintRenderer.js's createNode()) always carries. #nodeOverlay (dist/styles.css) sets
 * `pointer-events: none` on the whole overlay so a background click/pan falls through to the real SVG
 * underneath; this re-enables it for actual node content, and (since `pointer-events` is inherited)
 * every descendant -- rows, ports, controls, resize handle -- inherits `auto` back from here without
 * needing its own override. `position: absolute` is the one CONSTANT positioning property every node
 * host needs; `left`/`top`/`width`/`height` themselves are per-node dynamic values and stay inline
 * styles set at creation/move/resize time (see createNode()/applyNodeGroupPosition() in main.js), not
 * duplicated here. */
.blueprint-node {
  position: absolute;
  pointer-events: auto;
  /* M3 Physical P5.3: every node host is a sibling inside #nodeOverlayWorld, which becomes a stacking
   * context only once SvgCoordinateSpace.overlayWorldTransform() sets its `transform` -- but that single
   * shared context is exactly where the bug lived: descendants with an explicit z-index (`.port-slot`
   * left/right at 10, `.resize-handle` at 1000) are NOT scoped to their own node, since neither this rule
   * nor `.node-blueprint` previously established a stacking context of their own. A lower Node's ports/
   * resize-handle could therefore out-rank an upper Node's ordinary (z-index:auto) body content in the
   * SAME shared context, regardless of DOM/paint order. `isolation: isolate` makes every node host its
   * own atomic stacking context -- z-index values on its descendants (ports, resize handle, selection
   * glow) now only ever compete against each other, never against another node's descendants. Cross-node
   * order then falls back to plain DOM order, same as before this fix for ordinary body content. No
   * z-index was added or escalated anywhere to achieve this. */
  isolation: isolate;
  /* M3 Physical P5.4: a physical retest of P5.3's atomic-stacking fix surfaced a real product gap --
   * cross-node order now correctly follows plain DOM order, but DOM order is creation order, so an
   * OLDER selected/dragged Node still painted underneath a newer sibling while the author was actively
   * manipulating it. `z-index: 0` here is the explicit baseline of a small, fixed, three-tier
   * presentation hierarchy (0 ordinary / 1 selected / 2 actively-dragged, via `.node-front-selected`/
   * `.node-front-dragging` below) -- not a monotonic per-drag counter, never written to `node` model
   * data, and orthogonal to `isolation: isolate` above (that still scopes each host's OWN descendants;
   * this only orders the hosts themselves against their siblings within #nodeOverlayWorld's stacking
   * context). See main.js's updateNodeFrontElevation() for the single place these two classes are ever
   * toggled, and its header comment for why one hook there covers every selection/drag call site. */
  z-index: 0;
}

/* M3 Physical P5.4: see the z-index:0 baseline comment above -- fixed tiers only, applied/removed
 * exclusively by main.js's updateNodeFrontElevation(). A node in BOTH states at once cannot happen
 * (updateNodeFrontElevation always clears both classes before applying at most one), but the numeric
 * order below (1 < 2) documents the intended precedence regardless. */
.blueprint-node.node-front-selected {
  z-index: 1;
}

.blueprint-node.node-front-dragging {
  z-index: 2;
}

/* M4 SDU physical retest flicker review-blocker correction 2 (docs/handoffs/CHATGPT_M4_SDU_PHYSICAL_
 * RETEST_FLICKER_REVIEW_BLOCKED_2.md): the per-node `.blueprint-node.node-settling` rule this comment
 * used to document is RETIRED, not merely unused -- the whole-presentation freeze/commit design
 * (beginPresentationFreeze()/commitFrozenPresentationAfterSettle(), main.js) hides the entire live
 * Node+Group+Connection container as one unit instead of individual rebuilt nodes, so no per-node hidden
 * state exists to style anymore. See that design's own header comment for the full reasoning (it also
 * closes BLOCKER 2A/2B, which the retired per-node mechanism could not). */

/* The Node Blueprint - CSS Grid layout
 * MASTER_NODE Layout: Ogni riga contiene [PORT-IN] [CONTENT] [PORT-OUT]
 * Le porte sono automaticamente centrate verticalmente nella loro riga
 */
.node-blueprint {
  display: flex;
  flex-direction: column;
  overflow: visible;
  position: relative;
}

/* I bordi colorati sono applicati direttamente alle righe */

/* Selection - MASTER_NODE: usa il glow SVG come tutti i nodi */
/* .node.blueprint-node.selected .node-row {
  border-color: #6366f1 !important;
  box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
}

/* Disable default SVG selection styling for blueprint nodes 
.node.blueprint-node.selected > foreignObject {
  filter: none !important;
} */

/* Comment node - nodo normale senza porte visibili */
.node-blueprint.comment-node .port-slot {
  visibility: hidden;
}

/* M3 Physical P4: replaces this file's own former `filter: drop-shadow(...)`/`filter: brightness(...)`
 * selection/play-active glow and `filter: brightness(0.7)` dim-others, all applied directly on
 * `.node-blueprint` (the near-root element inside every Blueprint node's `<foreignObject>`) or its
 * `.circular-body`, whenever ANY node is selected -- via `highlightNode()` in main.js, which fires on
 * essentially every tap-select/drag-start. Real-iPhone trace evidence (docs/handoffs/
 * CHATGPT_TO_CLAUDE_M3_PHYSICAL_P4_WEBKIT_FOREIGNOBJECT_PAINT_HITTEST_FIX.md) proved WebKit's painted
 * foreignObject layer for a dragged node had diverged from its own live layout geometry (elementFromPoint
 * correctly hit-tested the finger's actual position, but the node's own getBoundingClientRect() reported
 * a position far away) -- `filter` is a documented, direct trigger for exactly this class of WebKit
 * foreignObject bug (WebKit bug 23113 and related reports: CSS that creates a compositing layer --
 * `filter` prominently among them -- on content inside a foreignObject can detach that content's paint
 * from the SVG's own coordinate updates). `box-shadow` and explicit muted colors below achieve the same
 * VISUAL result (a colored glow ring on the selected node; darker/muted rows on every other node) without
 * ever creating a new compositing layer inside the live subtree. This restores, for the glow, the exact
 * per-row box-shadow design this file's own commented-out pre-filter block above (line ~54) already
 * used -- not a new invention.
 */
/* M3 Physical P5.1: replaces the M3 Physical P4 per-row selection/play-active glow above. That design
 * was itself a correct fix for a real WebKit `filter`-inside-`<foreignObject>` compositing bug (see the
 * comment block above), but its `box-shadow` was applied to EVERY individual `.node-row` independently --
 * visually correct only by accident when every row happened to butt directly against its neighbors with
 * no gap, but the real, confirmed bug: a `box-shadow` ring painted on each row independently, meaning
 * every internal horizontal divider between rows (header/Type/Flow In/A/operator/B/Result/footer, on a
 * multi-row node like Logic) gets its OWN glow ring, making the node look like a stack of independently
 * lit boxes instead of one selected object with a single outer outline. P5 removed the live-foreignObject
 * constraint that made `filter` risky in the first place, so this round can move the glow to ONE outer
 * presentation surface without reopening that WebKit risk (this still uses `box-shadow`, never `filter`).
 *
 * The glow is NOT placed directly on `.node-blueprint` itself, even though `.node-blueprint` is the
 * near-root "whole node" element: `.node-blueprint`'s own bounding box is the FULL width of
 * `.node-row-wrapper`'s three-column grid (`grid-template-columns: 28px 1fr 28px`, see below), i.e. it
 * includes the 28px port-slot gutters on each side -- confirmed by direct runtime measurement (a Logic
 * node: `.node-blueprint` rect width ~300px, but the visible colored `.node-row` body is only ~243.6px
 * wide, inset exactly 28px on each side, with the port circles sitting in that gutter). A box-shadow on
 * `.node-blueprint` directly would therefore ring the PORT GUTTERS too, making ports visually part of the
 * glowing perimeter -- exactly what this round's own visual contract forbids. Instead, a single
 * presentation-only, `pointer-events: none` `::after` pseudo-element is inset by that same 28px on each
 * side (matching `.node-row-wrapper`'s own grid column width, not a new independent number) and spans the
 * full height -- its box (not `.node-blueprint`'s own) is what the glow shadow is drawn on, so it hugs
 * exactly the visible node body's silhouette: the union of every `.node-row`'s own colored border, not
 * the wider row-wrapper track. `border-radius: 10px` matches the same hardcoded radius the first/last
 * `.node-row` already use for their own rounded top/bottom corners (`.node-row-wrapper:first-child .node-
 * row`/`:last-child .node-row` above), so the glow's rounded ends line up with the node's own visible
 * corners. Being a pseudo-element (not a new positioned child), it adds no interaction layer, no layout
 * box relevant to childern's box model, and cannot itself be hit-tested or receive focus. */
/* M3 Physical P5.2: fixes a real-iPhone-confirmed defect in the P5.1 outer glow above -- the selection
 * emphasis rendered as a visibly DETACHED ring, ~1px separated from the node's actual edge, instead of
 * hugging its silhouette. Root cause: the P5.1 design drew the entire emphasis as `box-shadow` on a
 * `content: ''` pseudo-element with no visible fill/border of its own -- `0 0 0 2px COLOR` (zero blur,
 * 2px spread) is, in CSS terms, a solid ring starting exactly at the box edge with no gap, but that
 * guarantee holds only for the ABSTRACT box; WebKit's real rasterization of a box-shadow ring cast by an
 * otherwise-invisible box (no border/background of its own to anchor the anti-aliasing against) can
 * still show a hairline separation at real device pixel ratios, confirmed on physical iPhone hardware
 * (not reproduced/reproducible in this environment's Chromium-only tooling, which is exactly why this
 * class of box-shadow-only-ring artifact survived Chromium-based P5.1 verification). Fixed by giving the
 * pseudo-element a REAL `border` for the crisp ring instead of a `box-shadow` spread -- a border is part
 * of the box's own paint, always flush with that box's edge by construction, with no separate-shadow
 * rasterization step that could introduce a gap. The soft blurred glow layer stays a genuine
 * `box-shadow` (unchanged in spirit) since a soft blur's own falloff has no comparable "flush edge"
 * expectation to violate. Positioning/inset (28px, matching `.node-row-wrapper`'s own port-gutter grid
 * columns -- see the P5.1 comment above for the full derivation) and `border-radius: 10px` are otherwise
 * unchanged from P5.1 -- only the crisp-ring technique changed. */
/* M3 Physical P5.5: ONE shared outer card shell -- the colored frame + shared background + drop-shadow
 * every `.node-row` used to paint independently (see that rule's own comment for the corner-mitering and
 * gap defects this replaces). Reuses the EXACT same 28px inset/`border-radius:10px` geometry the P5.1/
 * P5.2 selection glow (`::after` below) already established and the existing P5.x selection-outline logic
 * already trusts as "the visible node body" -- so this shell can never disagree with where the selection
 * ring/outline is drawn, and neither needed to change for this round.
 *
 * M3 Physical P5.5.2: split back into TWO pseudo-elements, for a reason distinct from (and not reopening)
 * the P5.5.1 supplement's own merge below -- physical iPad/desktop evidence showed the colored frame
 * missing on the left/right sides, with small colored seam fragments where internal dividers met the
 * body edge. Root cause, confirmed from the CSS painting-order algorithm itself (CSS2.1 Appendix E): both
 * `::before` (generated as the FIRST child) and every `.node-row-wrapper` are `position` elements with
 * `z-index: auto`, so they paint together, in DOM/generated-content order, within the same stacking
 * context -- `::before` (first) paints, then each `.node-row-wrapper` (later) paints ON TOP of it. Every
 * `.node-row` is opaque (`background: var(--bg-secondary)`, `width: 100%`) and occupies the EXACT SAME
 * 28px-inset central column the shell's border sits at (the border's inner edge is 2px in from that same
 * 28px line) -- so the row's own opaque fill necessarily paints over the shell's left/right border pixels.
 * Top/bottom survived only incidentally (WebKit-specific antialiasing of a thin horizontal vs. vertical
 * border strip, not a structural difference this stylesheet controls), which is exactly why a green
 * Chromium/DOM geometry suite never caught this: the STRUCTURE was already unsafe, Chromium's rasterizer
 * simply didn't expose it.
 *
 * A background fill and row content BELOW it cannot coexist on the same paint layer as a border ABOVE row
 * content -- an opaque `background` on a layer that paints after rows would hide the rows' own text/
 * controls entirely. The two concerns are therefore split onto the two pseudo-elements every element
 * already has, each doing exactly one job:
 *   `::before` (first child, paints BEHIND every row): background fill + ambient card drop-shadow only.
 *     Unchanged position/inset/radius; no longer carries a `border` or the selection/play-active glow.
 *   `::after` (LAST child, paints AFTER every row -- per the same CSS2.1 Appendix E ordering, structurally
 *     guaranteed regardless of engine/antialiasing quirks): the colored frame `border`, unconditional on
 *     every node exactly like `::before`'s border used to be, `background: transparent` (implicit -- must
 *     stay transparent or it would hide row content), `pointer-events: none`. Selection/play-active glow
 *     moves here too, staying on the frame's OWN box exactly as the P5.5.1 supplement established -- this
 *     is not "two independent geometry owners for the glow" (the thing that fix guarded against): frame
 *     and glow remain ONE single owner (`::after`), still driven by the one `top/right/bottom/left`
 *     declaration below; only the BACKGROUND (never part of the glow's own identity, and never at risk of
 *     being paint-covered since it's not a boundary edge) moved to a separate, always-behind layer. */
.node-blueprint:not(.circular-node)::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 28px;
  right: 28px;
  border-radius: 10px;
  background: var(--bg-secondary);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
  pointer-events: none;
}

/* M3 Physical P5.5.2: the frame -- see the `::before` comment above for why this moved to `::after`
 * (paints AFTER every row, so it can never be covered by their opaque backgrounds). Unconditional on
 * every node, selected or not, same as the pre-P5.5.2 `::before` border was. Border-width uses the named
 * `--node-frame-border-width` (R1, challenge-review reconciliation) so the internal-divider inset below
 * can be derived from this SAME value instead of standing as an unrelated independent literal. */
.node-blueprint:not(.circular-node)::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 28px;
  right: 28px;
  border-radius: 10px;
  border: var(--node-frame-border-width) solid var(--node-color, var(--border));
  pointer-events: none;
}

/* M3 Physical P5.5.1 supplement (relocated in P5.5.2, principle unchanged): the selected/play-active
 * emphasis stays on the SAME box as the frame it decorates -- one geometry owner, an extra `box-shadow`
 * layer stacked on top of the frame's own border, never a second independently-tracked box. */
.node-blueprint.node-selected:not(.circular-node)::after {
  box-shadow: 0 0 10px 1px var(--node-color, #6366f1);
}

.node-blueprint.node-play-active:not(.circular-node)::after {
  box-shadow: 0 0 16px 3px var(--node-color, #6366f1);
}

/* M3 Physical P5.1: baseline drop shadow for every circular node body, unselected or not. Previously set
 * as an INLINE style in NodeBlueprintRenderer.js's `circleEl.style.cssText` -- an inline style always
 * wins over any stylesheet rule regardless of selector specificity, which meant the two selected/play-
 * active `.circular-body` box-shadow rules below could never actually take effect (confirmed by direct
 * measurement: circular-node selection glow was silently non-functional). Moved here so the ordinary
 * cascade (this baseline rule, then the more specific selected/play-active rules) works as intended. */
.circular-body {
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}

.node-blueprint.circular-node.node-selected .circular-body {
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3), 0 0 0 3px var(--node-color, #6366f1), 0 0 10px 1px var(--node-color, #6366f1);
}

.node-blueprint.circular-node.node-play-active .circular-body {
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3), 0 0 0 3px var(--node-color, #6366f1), 0 0 16px 3px var(--node-color, #6366f1);
}

/* Dim every OTHER node while one is selected/active -- explicit muted background/text instead of
 * `filter: brightness(0.7)`, same M3 Physical P4 rationale as above. */
.node-play-dimmed .node-row {
  background: #14141f;
}

.node-play-dimmed .cell {
  color: var(--text-muted);
}

/* MASTER_NODE: Ogni riga usa grid a 3 colonne [port-in | content | port-out] */
.node-row-wrapper {
  display: grid;
  grid-template-columns: 28px 1fr 28px;
  align-items: center;  /* Centra verticalmente le porte */
  width: 100%;
  position: relative;
}

/* M3 Physical P5.5.10: H1 root-cause fix -- see docs/handoffs/
 * CLAUDE_TO_CHATGPT_M3_PHYSICAL_H1_ROOT_CAUSE_PROPOSAL.md and ChatGPT's own challenge-review at
 * CHATGPT_M3_PHYSICAL_H1_ROOT_CAUSE_CHALLENGE_REVIEW_AND_IMPLEMENTATION_GATE.md. `min-height: 100%` here
 * was a PERCENTAGE block-size on a grid item whose row track (`.node-row-wrapper`, above) declares no
 * `grid-template-rows`/height of its own -- the track is always content-sized (`auto`). Per CSS, a
 * percentage height against an indefinite containing block has no defined resolution; Chromium correctly
 * treats it as inert (confirmed: the row-wrapper discriminator's own Chromium baseline shows zero
 * divergence everywhere), but a real iPhone capture (build-18, opt-in diagnostic tracing) showed WebKit resolving
 * it anyway -- to a value matching the LIVE-DRAG excursion's peak grow-row target, still present, unchanged,
 * in the SETTLED post-teardown snapshot, well after the gesture returned to its start and the node's own
 * outer height (`nodeEl`) was correctly reconciled back down. No JS anywhere writes `.node-row-wrapper`'s
 * own height -- this was purely a CSS percentage-resolution hazard.
 * `align-self: stretch` (replacing the percentage) is the CSS-native, non-percentage way to express
 * "fill this grid item's own row track": it needs no containing-block height at all, so there is nothing
 * for the same class of resolution/invalidation hazard to attach to. Deliberately `align-self` (per-item),
 * not the wrapper's own `align-items` -- the wrapper's `align-items: center` (above) stays UNCHANGED, so
 * the real content row (`.node-row`, the grid's actual track-sizing owner) keeps its own existing
 * center-alignment contract untouched; only the two port-slot participants -- the ones the physical
 * evidence actually implicated -- change how they fill that track once it is sized. */
.port-slot {
  display: flex;
  align-items: center;
  justify-content: center;
  align-self: stretch;
  pointer-events: none;
}

.port-slot .port {
  pointer-events: auto;
}

/* M4 SDU desktop physical visual/port fail gate (docs/handoffs/CHATGPT_TO_CLAUDE_M4_SDU_DESKTOP_
 * PHYSICAL_VISUAL_PORT_FAIL_GATE.md), section B -- Antonio: Choice's Flow In port "reads as slightly
 * inset/misaligned relative to the node outer frame rather than sharing a clean tangent/alignment".
 *
 * Derivation (verified against live-rendered getBoundingClientRect() measurement, not assumed):
 * `.port-slot` is a flex box occupying its 28px grid-track column, `justify-content: center`ing its one
 * `.port` child. A `margin-right: -14px` (left slot; mirrored `margin-left` on the right slot) makes the
 * slot's own rendered width become `28 - marginRight` = 42px, so the child port always centers at x=21
 * from the slot's outer edge, REGARDLESS of the port's own size -- by construction, every port role
 * shares one fixed center line, never a per-role one.
 * The node's own outer frame (`.node-blueprint:not(.circular-node)::after`) sits at exactly `left: 28px`
 * / `right: 28px` -- the same 28px the slot's un-margined track width already is. A port's frame-facing
 * edge = center(21) +/- renderedWidth/2, where renderedWidth is the port's actual painted box (its CSS
 * `width` PLUS its own border, since `.port` has no `box-sizing: border-box` and border still paints
 * outside the declared width): default (`.port` alone, no role class, e.g. `.data`/`.actor`) is
 * `14px + 2*2px border = 18px`; `.port.flow` is `17px + 2*2px = 21px`; `.port.mixed` is `19px + 0
 * (border: none) = 19px`.
 * With the ONE shared `-14px` margin (=> center 21, frame at 28), the frame-facing edge lands at:
 *   default: 21 + 18/2 = 30 -- i.e. 2px past the frame's outer line (into the node body)
 *   flow:    21 + 21/2 = 31.5 -- i.e. 3.5px past it
 *   mixed:   21 + 19/2 = 30.5 -- i.e. 2.5px past it
 * These are NOT the same offset. `default` (2px) is what every ordinary data/actor port on every other
 * node type already shows and is the long-established look (this exact `-14px` constant, and the 14/17/
 * 19px role widths, all date to a single February 2026 commit, `f2a7ca5c`, seven months before any M4 SDU
 * work -- this is old geometry, not something this branch's own commits introduced). `flow` sits 1.5px
 * DEEPER into the node than that baseline -- measured on a live Choice node at 3 significant figures
 * (3.501px at 1.149x canvas zoom, i.e. 3.5 logical px), matching this derivation exactly -- which is what
 * reads as "inset" next to ordinary ports the eye is calibrated to, and is the one role Antonio actually
 * named. `mixed` is only 0.5px off -- imperceptible, and not what was reported.
 *
 * Fix: give `.port.flow` and `.port.mixed` their OWN slot margin so every role's frame-facing edge
 * lands at the SAME 2px-past-frame offset the accepted default already uses, solving `margin =
 * renderedWidth - 32` (the algebraic rearrangement of the edge equation above for a target edge of
 * frame+2): flow (21px rendered) => -11px, mixed (19px rendered) => -13px (both were -14px, the
 * unmodified default-port constant). `:has()` (already used elsewhere in this file, see
 * `.node-row-wrapper:not(:last-child):not(:has(...))` above) lets the margin depend on which port role
 * the slot actually contains, without a renderer change or a new data attribute.
 *
 * REVIEW-BLOCKER round (docs/handoffs/CHATGPT_M4_SDU_DESKTOP_PHYSICAL_VISUAL_PORT_FAIL_REVIEW_BLOCKED.md,
 * BLOCKER 4): the previous round derived this same `-13px` for `.port.mixed` but withheld it because
 * applying it flipped two checks in test_m4_sdu_final_physical_connection_residuals_blocker_cdp.js
 * (B2-5, B2-6). ChatGPT correctly rejected that reasoning: the return's OWN evidence already showed
 * those checks sit on a pathological `elementFromPoint()` seam between two adjacent nodes' HTML
 * row-wrappers, present on the UNMODIFIED baseline -- a brittle fixture is not a reason to leave a
 * proven geometry inconsistency in the shipped product. The single visual invariant this whole section
 * establishes (every port role lands at the same +2px frame offset) is now applied uniformly; the
 * fixture itself is repaired instead (see that test file's own updated comment: the two affected
 * scenarios' node spacing was widened so the cable's hit-test point is no longer adjacent to the seam,
 * and both were re-verified green after the widening, on top of this geometry change, not instead of
 * it). */
.port-slot.left {
  margin-right: -14px;
  z-index: 10;
}

.port-slot.left:has(> .port.flow) {
  margin-right: -11px;
}

.port-slot.left:has(> .port.mixed) {
  margin-right: -13px;
}

.port-slot.right {
  margin-left: -14px;
  z-index: 10;
}

.port-slot.right:has(> .port.flow) {
  margin-left: -11px;
}

.port-slot.right:has(> .port.mixed) {
  margin-left: -13px;
}

/* MASTER_NODE: Il contenuto centrale ha sfondo e bordi */
/* Con la nuova struttura row-wrapper, il .node-row è la parte centrale */
/* M3 Physical P5.5: `.node-row` used to draw its OWN colored `border-left`/`border-right` (2px) on every
 * single row, together with its own internal `border-bottom` (1px, plain grey) divider on the SAME box.
 * Real-iPad screenshots showed the internal divider visibly overlapping/running into the colored side
 * border at the shared corner -- an ordinary CSS border-corner miter artifact between two borders of very
 * different width/color painted on the same element, worst on a manually-resized Choice node. The colored
 * outer frame is now owned by ONE shell (`.node-blueprint:not(.circular-node)::before` below), reusing
 * the exact 28px-inset "visible body" geometry the P5.1/P5.2 selection glow (`::after`) already
 * established and the P5.x selection-outline logic already trusts -- so this round changes NEITHER that
 * geometry nor the selection outline's own alignment. `.node-row` itself keeps only its own background
 * (a fallback fill directly behind the shell's identical background closes any hairline sub-pixel gap
 * between adjacent rows) and no longer paints any border of its own; the internal 1px divider moves to a
 * dedicated rule below that explicitly excludes the last row (see `.node-row-wrapper:not(:last-child)`),
 * since the last row's bottom edge is now the shell's own bottom edge, not an internal seam. */
.node-row {
  display: flex;
  align-items: center;
  position: relative;
  overflow: visible;
  height: var(--node-row-height, 35px);
  background: var(--bg-secondary);
  width: 100%;
  box-sizing: border-box;
}

/* M3 Physical P5.5.2: internal separator, never on the last row (whose bottom edge is the shell's own
 * bottom edge). Previously a full-width `border-bottom` on `.node-row` itself -- its endpoints landed
 * EXACTLY at the same 28px-inset x-position the outer frame's border occupies (`.node-row` spans the same
 * 28px-to-(100%-28px) central column the frame is inset to), i.e. coincident with the frame, not internal
 * to it. Physical iPad/WebKit evidence showed small colored seam-pixel fragments exactly at those
 * coincident endpoints. Replaced with a dedicated inset pseudo-element on `.node-row` itself (needs no new
 * wrapper -- `.node-row` is already `position: relative`). Revision R1 (challenge-review reconciliation):
 * `left`/`right` use `--node-separator-inset` (defined in `:root` as the frame's own
 * `--node-frame-border-width` plus a 4px clearance margin) instead of a bare `6px` literal, so if the
 * frame's border-width ever changes, this inset is DERIVED from it rather than silently falling out of
 * sync -- same visual result as before (6px today), now a provable relationship instead of a coincidence.
 * M3 Physical P5.5.11: build-19's own physical H1 PASS (docs/handoffs/
 * CHATGPT_M3_PHYSICAL_BUILD19_H1_PASS_AND_FOOTER_POLISH_GATE.md) exposed a pre-existing presentation-only
 * artifact once the real tear was gone: `:not(:last-child)` alone still painted this divider on the row
 * immediately BEFORE the technical footer row-wrapper, since that row is never itself the last child (the
 * footer always is). The added `:not(:has(...))` clause excludes exactly that one case -- a row-wrapper
 * whose very next sibling contains the footer row (`[data-row-type="footer"]`, the same attribute
 * `_captureRowWrapperDiscriminator()`/`applyAcceptedNodeGeometry()` already key off) -- leaving every
 * OTHER internal content-to-content separator untouched. Footer height, the resize handle's own
 * `position:absolute; bottom:0; right:0`, and all row/geometry logic are untouched by this rule alone. */
.node-row-wrapper:not(:last-child):not(:has(+ .node-row-wrapper > .node-row[data-row-type="footer"])) .node-row::after {
  content: '';
  position: absolute;
  left: var(--node-separator-inset);
  right: var(--node-separator-inset);
  bottom: 0;
  height: 1px;
  background: var(--border);
  pointer-events: none;
}

/* Compact rows */
.node-row[data-compact="true"] {
  height: var(--node-row-height-compact, 26px) !important;
  min-height: var(--node-row-height-compact, 26px) !important;
}

.node-row[data-compact="true"] .cell {
  padding-top: 4px;
  padding-bottom: 4px;
}

/* Compact rows: smaller controls to avoid overlap */
.node-row[data-compact="true"] .cell input,
.node-row[data-compact="true"] .cell select {
  padding: 4px 8px;
  font-size: 11px;
  height: 100%;
  box-sizing: border-box;
}

/* M3 Physical P5.5: the colored top/bottom border these two rules used to add is now owned by the shell
 * (`.node-blueprint:not(.circular-node)::before` below); only the corner radius stays here, so each row's
 * own OPAQUE background fill is clipped to match the shell's rounded silhouette instead of showing a
 * square corner poking past it. The first row's outer drop-shadow moved to the shell too (folded into
 * its own box-shadow declaration) rather than being duplicated on both. */
.node-row-wrapper:first-child .node-row {
  border-top-left-radius: 10px;
  border-top-right-radius: 10px;
}

.node-row-wrapper:last-child .node-row {
  border-bottom-left-radius: 10px;
  border-bottom-right-radius: 10px;
}

/* Righe con textarea AUTO-ESPANDIBILI */
.node-row[data-has-textarea="true"] {
  height: auto !important;
  min-height: var(--node-textarea-height, 80px);
}

/* Righe con testo multilinea esplicativo */
.node-row[data-multiline="true"] {
  height: auto !important;
  min-height: var(--node-multiline-row-height, 70px);
  align-items: flex-start;
}

.node-row[data-multiline="true"] .cell {
  align-items: flex-start;
}

/* Auto-fit row columns to content (MASTER_NODE) */
.node-row[data-fit-content="true"] {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: max-content;
  grid-template-columns: none;
}

.node-row[data-fit-content="true"] .cell {
  width: auto;
  min-width: 0;
}

.node-row[data-fit-content="true"] .cell.label-cell {
  min-width: 0;
  max-width: none;
  width: auto;
}

/* Dynamic rows - altezza minima 1 riga */
.node-row.dynamic-row {
  min-height: 35px;
  height: auto;
}

.node-row.header-row {
  display: grid !important;
  grid-template-columns: 36px 1fr 36px;
  align-items: center;
  text-align: center;
}

.node-row.header-row .cell {
  display: flex;
  align-items: center;
  min-width: 0;
}

.node-row.header-row .cell.icon-cell {
  grid-column: 1;
  justify-content: center;
  justify-self: start;
}

.node-row.header-row .cell.label-cell {
  grid-column: 2;
  justify-content: center;
  text-align: center;
  justify-self: center;
  font-size: 14px;
  font-weight: 600;
}

.node-row.header-row .cell.label-cell input {
  text-align: center;
}

.node-row.header-row .cell.button-cell {
  grid-column: 3;
  justify-content: flex-end;
  justify-self: end;
  padding-right: 8px;
}

.node-row.footer-row {
  background: rgba(0,0,0,0.2);
  min-height: 20px !important;
  height: 20px !important;
  max-height: 20px !important;
  position: relative;
}

.node-row.footer-row .cell {
  padding: 0;
}

/* M3 Physical P5.5: a manually-resized node with no growable content row (Action, Field,
 * FunctionCall/Entry/Return, etc. -- see NodeBlueprintRenderer.js's getManualResizeGrowableRows()) gets
 * this instead of leaving the outer host taller than its rows with a blank transparent gap. Inserted
 * immediately before the footer row-wrapper (JS-driven; not part of any blueprint's static row list), so
 * the footer always stays pinned to the card's bottom edge. Same background as an ordinary row/the shell
 * behind it -- reads as intentional room within one continuous card, never a torn-open seam. No border of
 * its own on purpose: it is not a section, just absorbed space.
 */
.node-row-spacer {
  background: var(--bg-secondary);
  margin-left: 28px;
  margin-right: 28px;
  box-sizing: border-box;
  flex-shrink: 0;
}

.node-row.button-row-row {
  justify-content: center;
  padding: 5px 0;
}

/* Cells */
.cell {
  padding: var(--node-cell-padding-y, 8px) var(--node-cell-padding-x, 12px);
  display: flex;
  align-items: center;
  gap: var(--node-cell-gap, 4px);  /* Usa CSS variable */
}

/* Tight spacing for label+control rows */
.node-row[data-lock-labels="true"] .cell {
  padding-left: var(--node-cell-padding-x-tight, 4px);
  padding-right: var(--node-cell-padding-x-tight, 4px);
}

/* Checkbox cells hanno padding ridotto */
.cell.checkbox-cell {
  padding: 8px 0 8px 8px;
  justify-content: center;
}

.cell.checkbox-cell input[type="checkbox"] {
  margin: 0;
}

/* Label cells usano larghezza da CSS variable (escluso header e footer) */
.node-row:not(.header-row):not(.footer-row) .cell.label-cell:not([data-full-width="true"]) {
  min-width: var(--node-label-min-width, 70px);
  max-width: var(--node-label-max-width, 180px);
  flex-shrink: 0;
  flex: 0 0 auto;
}

/* Dropdown rows: allow labels to shrink to content so selects can expand */
.node-row[data-has-dropdown="true"]:not([data-lock-labels="true"]) .cell.label-cell:not([data-full-width="true"]) {
  min-width: 0 !important;
  width: auto !important;
  max-width: var(--node-label-max-width, 180px) !important;
}

/* Celle con textarea hanno padding per distanziare dai bordi (MASTER NODE) */
.node-row:not(.header-row):not(.footer-row) .cell.textarea-cell {
  padding: var(--node-textarea-padding, 6px) 8px;
  align-items: stretch;
}

.cell.full-width {
  flex: 1;
}

.cell.icon-cell {
  width: 36px;
  justify-content: center;
  padding: 0;
}

.cell.label-cell {
  flex: 0 0 auto;
  font-weight: 600;
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cell.section-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.cell.meta-label {
  font-size: 10px;
  font-weight: 600;
  color: var(--text-secondary);
}

/* Action recursive input "Expose" should read as helper/meta text, not as main label */
.node-row.action-input-table .cell.action-expose-label {
  font-size: 10px;
  font-weight: 500;
  color: var(--text-secondary);
  letter-spacing: 0.02em;
}

.cell.label-cell[data-full-width="true"] {
  flex: 1 1 0;
}

.cell.dropdown-cell {
  flex: 1 1 0;
  min-width: 0;
}

.cell.button-cell {
  padding: 4px;
}

.cell.remove-cell {
  padding: 0;
  justify-content: center;
}

/* Function tables - align header/rows as columns */
.node-row.function-entry-table,
.node-row.function-return-table {
  display: grid;
  align-items: center;
  gap: 0;
}

.node-row.function-entry-table {
  grid-template-columns: minmax(0, 2fr) 90px 28px 28px minmax(0, 1fr) 18px;
}

.node-row.function-return-table {
  grid-template-columns: minmax(0, 2fr) 90px minmax(0, 1fr) 18px;
}

.node-row.function-entry-table .cell,
.node-row.function-return-table .cell {
  min-width: 0;
  width: 100%;
  box-sizing: border-box;
  padding-left: 4px;
  padding-right: 4px;
  overflow: hidden;
}

.node-row.function-entry-table .cell.label-cell,
.node-row.function-return-table .cell.label-cell {
  min-width: 0 !important;
  max-width: none !important;
  width: 100%;
  flex: unset;
  justify-content: flex-start;
}

.node-row.function-entry-table .cell.checkbox-cell {
  padding: 0;
  justify-content: center;
}

/* Template fields table - column alignment */
.node-row.template-fields-table {
  display: grid;
  align-items: center;
  gap: 0;
  grid-template-columns: minmax(0, 1.2fr) 110px 42px 42px 68px 34px 34px 24px 24px 24px 24px;
}

.node-row.template-fields-table .cell {
  min-width: 0;
  width: 100%;
  box-sizing: border-box;
  padding-left: 4px;
  padding-right: 4px;
  overflow: hidden;
}

.node-row.template-fields-table .cell.label-cell {
  min-width: 0 !important;
  max-width: none !important;
  width: 100%;
  flex: unset;
  justify-content: flex-start;
}

.node-row.template-fields-table .cell.toggle-cell,
.node-row.template-fields-table .cell.icon-button-cell {
  padding: 0;
  justify-content: center;
}

.node-row.template-fields-table .cell.checkbox-cell {
  padding: 0;
  justify-content: center;
}

.node-row.template-fields-table .cell,
.node-row.template-fields-table .cell > * {
  align-self: center;
}

/* Entity/workspace tables - column alignment */
.node-row.entity-table,
.node-row.entity-fields-table {
  display: grid;
  align-items: center;
  gap: 0;
  /* Label column uses CSS variable set by JS (longest label), data column fills remaining space */
  /* Fallback to max-content if variable not set */
  grid-template-columns: var(--entity-label-width, max-content) minmax(0, 1fr);
}

.node-row.entity-table .cell,
.node-row.entity-fields-table .cell {
  min-width: 0;
  width: 100%;
  box-sizing: border-box;
  padding-left: 4px;
  padding-right: 4px;
}

/* Label cells never truncate - auto-size to content */
.node-row.entity-table .cell.label-cell,
.node-row.entity-fields-table .cell.label-cell {
  overflow: visible;
  white-space: nowrap;
  width: auto;
}

/* Data cells can overflow */
.node-row.entity-table .cell:not(.label-cell),
.node-row.entity-fields-table .cell:not(.label-cell) {
  overflow: hidden;
}

/* Override any inherited constraints - label must show full text */
.node-row.entity-table > .cell.label-cell,
.node-row.entity-fields-table > .cell.label-cell {
  min-width: auto !important;
  max-width: none !important;
  width: auto !important;
  flex: none !important;
  justify-content: flex-start;
  overflow: visible !important;
  white-space: nowrap !important;
  text-overflow: clip !important;
}

.node-row.entity-fields-table .cell.entity-field-cell {
  padding-left: 4px;
  padding-right: 4px;
}

.node-row.entity-lifecycle-table {
  display: grid;
  align-items: center;
  gap: 0;
  grid-template-columns: minmax(0, 90px) 20px minmax(0, 1fr) 20px minmax(0, 1fr) 20px minmax(0, 2fr);
}

.node-row.entity-lifecycle-table[data-fit-content="true"] {
  grid-template-columns: none;
  grid-auto-flow: column;
  grid-auto-columns: max-content;
}

.node-row.entity-lifecycle-table .cell {
  min-width: 0;
  width: 100%;
  box-sizing: border-box;
  padding-left: 4px;
  padding-right: 4px;
  overflow: hidden;
}

.node-row.entity-lifecycle-table .cell.label-cell {
  min-width: 0 !important;
  max-width: none !important;
  width: 100%;
  flex: unset;
  justify-content: flex-start;
}

.node-row.entity-lifecycle-table .cell.checkbox-cell {
  padding: 0;
  justify-content: center;
}

.node-row.entity-lifecycle-table .cell.meta-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 1.1;
}

.cell.template-add-cell {
  justify-content: center;
}

.cell.template-add-cell .add-button {
  width: 140px;
}

.cell.toggle-cell {
  padding: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.toggle-btn {
  min-width: 28px;
  height: 18px;
  padding: 0 4px;
  border-radius: 3px;
  border: 1px solid var(--border);
  background: rgba(148, 163, 184, 0.2);
  color: var(--text-muted);
  font-size: 9px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}

.toggle-btn[data-toggle="port"][data-active="true"] {
  background: rgba(16, 185, 129, 0.35);
  border-color: rgba(16, 185, 129, 0.7);
  color: #ffffff;
}

.toggle-btn[data-toggle="port"][data-active="false"] {
  background: rgba(148, 163, 184, 0.2);
  border-color: rgba(148, 163, 184, 0.4);
  color: var(--text-muted);
}

.toggle-btn[data-toggle="scope"][data-scope="state"] {
  background: rgba(16, 185, 129, 0.35);
  border-color: rgba(16, 185, 129, 0.7);
  color: #ffffff;
}

.toggle-btn[data-toggle="scope"][data-scope="info"] {
  background: rgba(59, 130, 246, 0.35);
  border-color: rgba(59, 130, 246, 0.7);
  color: #ffffff;
}

.cell.toggle-cell .toggle-btn {
  user-select: none;
}

/* M3 Physical P4: `filter: brightness(1.2)` removed (desktop-hover-only decoration, no touch-relevant
 * baseline state, and `filter` is a direct WebKit foreignObject layer-trigger candidate -- see the
 * selection-glow fix above for the full rationale). Not replaced: the toggle button's own state-specific
 * background colors already provide adequate hover/pressed affordance without it. */

.cell.icon-button-cell {
  padding: 2px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.node-blueprint .icon-button {
  width: 18px;
  height: 18px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  outline: none;
  box-shadow: none;
}

.node-blueprint .icon-button .material-symbols-rounded {
  font-size: 18px;
}

.node-blueprint .icon-button[data-action="templateRenameField"] {
  color: #60a5fa;
}

.node-blueprint .icon-button[data-action="templateMoveFieldUp"],
.node-blueprint .icon-button[data-action="templateMoveFieldDown"] {
  color: #a78bfa;
}

.node-blueprint .icon-button[data-action="templateDeleteField"] {
  color: #ef4444;
}

/* M3 Physical P4: explicit muted color instead of element opacity -- always-on for a disabled icon
 * button (not a transient hover state), same rationale as the resize-handle fix above. */
.node-blueprint .icon-button[data-disabled="true"] {
  color: #475569;
  cursor: not-allowed;
}

.node-blueprint .icon-button[data-action="templateDeleteField"]:hover .material-symbols-rounded,
.node-blueprint .icon-button[data-action="templateDeleteField"]:active .material-symbols-rounded,
.node-blueprint .icon-button[data-action="templateDeleteField"]:focus .material-symbols-rounded {
  opacity: 1;
}

.cell.icon-button-cell:hover .material-symbols-rounded {
  opacity: 0.8;
}

/* Header button - matches ChoiceNode + button style */
.header-button {
  width: 20px;
  height: 20px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid #ffffff;
  color: #ffffff;
  font-size: 16px;
  font-weight: bold;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.15s;
  margin-right: 8px;
}

.header-button:hover {
  background: rgba(255, 255, 255, 0.3);
}

.header-button .material-symbols-rounded {
  font-size: 14px;
  font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}

/* Add button (master node standard) */
.add-button {
  width: 100%;
  padding: 6px 10px;
  background: var(--bg-tertiary);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-primary);
  cursor: pointer;
  font-size: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

.add-button:hover {
  background: var(--bg-secondary);
}

/* Run button uses add-button sizing but header color */
.run-button {
  width: 100%;
  padding: 6px 10px;
  background: var(--node-color);
  border: 1px solid var(--node-color);
  border-radius: 4px;
  color: #ffffff;
  cursor: pointer;
  font-size: 12px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

/* M3 Physical P4: `filter: brightness(1.1)` (hover) replaced with a box-shadow glow -- same rationale as
 * the port-hover fix above. `opacity: 0.5` (disabled, always-on when disabled) replaced with explicit
 * muted background/border/text colors instead of dimming the whole element via compositing opacity. */
.run-button:hover {
  box-shadow: 0 0 8px var(--node-color);
}

.run-button.is-disabled,
.run-button:disabled {
  background: #3a3a4a;
  border-color: #3a3a4a;
  color: rgba(241, 245, 249, 0.5);
  cursor: not-allowed;
}

/* Port styling. M4 SDU desktop physical visual/port fail gate, REVIEW-BLOCKER round (docs/handoffs/
 * CHATGPT_M4_SDU_DESKTOP_PHYSICAL_VISUAL_PORT_FAIL_REVIEW_BLOCKED.md, BLOCKER 2) -- Antonio's product
 * statement was not "make the ring match the port shape", it was that an overlay effect had appeared
 * that was never requested. The binding question is therefore APPROVAL, not tidiness, and this needed
 * a real answer, not an assertion.
 *
 * Found it on a full, un-shallowed clone: `757f435` (2026-01-28, authored by Antonio d'Amore himself --
 * the commit that created the Blueprint HTML port system and `.port.mixed` in the same change) is the
 * ORIGINAL of both this CSS rule and the JS magnetic-snap cue in `main.js`'s `drawTempLine()`. At that
 * commit, both were: `transform: scale(1.4)` PLUS a single, BLUR-ONLY `box-shadow: 0 0 8px <color>` (0
 * spread -- a soft glow with no hard edge, currentColor here, `#58a6ff`/`#ef4444` in the JS branch,
 * proven by the SAME commit's SVG-circle branch using those identical two colors for the same
 * valid/invalid semantic). That combination -- transform growing the WHOLE element (port div and its
 * child SVG together, so a mixed port's hexagon grew as one piece with nothing left over) plus a soft,
 * blurred glow -- is the actually-approved historic cue.
 * M3 Physical P4 (`89c21dc`, 2026-08-26, Claude) removed `transform` for a real, separately-justified
 * WebKit foreignObject compositing-layer reason (still valid, not reopened here) and, to keep the cue
 * reading as prominent without it, ADDED a second, crisp, ZERO-BLUR, 3px-SPREAD term: `, 0 0 0 3px
 * currentColor`. That addition has no Antonio approval anywhere in history -- it was invented to
 * compensate for the lost transform, never asked for. It is also the one that actually matters
 * visually: a crisp, hard-edged, unblurred ring exactly traces the port's own DOM BOX, which for
 * `.port.mixed` (a `border-radius: 0`, borderless, transparent 19x19 square div around a smaller inset
 * SVG hexagon -- see `NodeBlueprintRenderer.js`'s `createPort()`) is visibly bigger than the hexagon it
 * contains, producing exactly Antonio's screenshot: a rectangular ring around the hexagonal port.
 * (The pre-existing blur-only term alone, being blurred, has no comparable hard edge to expose the same
 * mismatch -- which is also consistent with this combination going unremarked for the seven months
 * between `757f435` and `89c21dc`.)
 *
 * Fix: drop exactly the unapproved addition, restoring the historically-approved blur-only glow (this
 * file keeps `transform` removed, per M3 Physical P4's still-valid WebKit reasoning -- not reopened by
 * this gate). `.port.mixed` keeps its OWN suppression below regardless, since even the blur-only term
 * was only ever approved together with the transform that made the hexagon (not the square) the thing
 * that visibly grew; the blur term alone, once isolated, is not a proven-approved effect for that one
 * role and Antonio's current complaint is specifically about it. */
.port {
  width: 14px;
  height: 14px;
  border: 2px solid;
  background: var(--bg-primary);
  cursor: pointer;
  transition: box-shadow 0.15s;
  flex-shrink: 0;
}

.port:hover {
  box-shadow: 0 0 8px currentColor;
}

.port.mixed:hover {
  box-shadow: none;
}

/* Circular nodes: ports are centered via `top: calc(50% - <half-height>px)` set inline at creation
 * (NodeBlueprintRenderer.js), not via `transform: translateY(-50%)` -- M3 Physical P4 removed that
 * transform (see that file's port-creation code for the exact per-role height math). No transform
 * override is needed here any more; only the box-shadow suppression these ports always had remains. */
.circular-node .port,
.circular-node .port:hover,
.circular-node .port.fixed-port,
.circular-node .port.fixed-port:hover {
  box-shadow: none;
}

.port.flow {
  border-radius: 2px;
  width: 17px;
  height: 17px;
  /* Color set dynamically from node */
}

.port.data {
  border-radius: 50%;
  /* Color set dynamically from node */
}

.port.actor {
  border-radius: 50%;
  /* Color set dynamically from node */
}

.port.mixed {
  border-radius: 0;
  background: transparent;
  border: none;
  padding: 0;
  width: 19px;
  height: 19px;
  /* Color set dynamically from node */
}

/* M3 Physical P4: `visibility: hidden` keeps the exact same "invisible but still occupies layout space"
 * behavior `opacity: 0` had here (this hides unused port slots on certain node types, always-on for
 * those slots, not a hover/transient state), without opacity's compositing-layer implication. */
.port.empty {
  visibility: hidden;
  pointer-events: none;
}

/* Resize handle - Material Symbols icon. M3 Physical P4: `opacity: 0.4` was unconditional on EVERY
 * rendered node, always -- a permanent, always-live layer-trigger candidate inside the foreignObject
 * subtree, per the same rationale as the selection-glow fix above. `color: rgba(...)` bakes the alpha
 * into the glyph's own paint instead of the element's compositing opacity, producing the identical
 * visible dimness with no compositing-layer implication. `--text-primary` is `#f1f5f9` = rgb(241,245,249).
 *
 * M3 Physical P5.5.12: `bottom:0; right:0` (relative to the footer `.node-row`, this element's own DOM
 * parent) used to put the handle flush against the outer frame's border/rounded corner with ZERO
 * clearance -- confirmed by direct measurement (docs/handoffs/
 * CLAUDE_TO_CHATGPT_M3_PHYSICAL_P5_5_12_FRAME_HANDLE_GEOMETRY_MEASUREMENT.md): at default node size the
 * footer row's own right/bottom edges are geometrically exact with the frame's own border line (delta
 * 0), so the handle's flush anchor sat directly on top of the frame's `border-radius: 10px` corner,
 * visually breaking the border's continuity there. Reusing `--node-separator-inset` (already the named,
 * frame-border-width-derived clearance this stylesheet uses for the internal row divider, R1 challenge-
 * review reconciliation, see that rule's own comment) keeps this inset tied to the SAME frame geometry
 * instead of a new unrelated magic number -- if the frame's border-width ever changes, this clearance
 * changes with it. The 44x44 CSS-px tactile classifier (`attachResizeHandler()`'s pointerdown handling)
 * re-derives its own hit zone from this element's LIVE `getBoundingClientRect()` at classification time,
 * so it re-centers on the inset box automatically; no separate update needed. DOM parent, resize math,
 * and lower-right affordance semantics are unchanged -- only the visible glyph's own clearance moved. */
.resize-handle {
  position: absolute;
  bottom: var(--node-separator-inset);
  right: var(--node-separator-inset);
  width: 20px;
  height: 20px;
  cursor: nwse-resize;
  color: rgba(241, 245, 249, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Material Symbols Rounded';
  font-size: 14px;
  user-select: none;
  pointer-events: all;
  z-index: 1000;
}

.resize-handle:hover {
  color: rgba(241, 245, 249, 0.8);
}

.resize-handle::before {
  content: 'zoom_out_map';
}

/* M3 P5.7: `.resize-handle-width-only` (a cursor/glyph override for a node with no declared vertical-grow
 * row) is retired -- see docs/handoffs/CHATGPT_TO_CLAUDE_M3_P5_7_BUILD24_RESIZE_UNIFICATION_GATE.md's
 * binding product decision. Every normal rectangular handle-bearing node now presents the SAME two-axis
 * affordance (the unconditional `.resize-handle` rule above); NodeBlueprintRenderer.js's renderRow() no
 * longer stamps this class on any handle (see that function's own P5.7 comment), so this rule had no
 * remaining consumer. */

/* Input styling for cells */
.cell input,
.cell select,
.cell textarea {
  background: var(--bg-tertiary);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text-primary);
  padding: 6px 10px;
  font-size: 12px;
  font-family: inherit;
  box-sizing: border-box;
  width: 100%;
  min-width: 0;
}

.cell select {
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  padding-right: 28px;
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
}

.cell textarea {
  resize: none;
  min-height: 0;
  max-height: 100%;
  width: 100%;
  height: 100%;
  padding: 8px 10px;
  font-size: 13px;
  box-sizing: border-box;
  margin: 0;
}

/* Entity/workspace field states */
.entity-field-control.readonly {
  background: rgba(100, 200, 255, 0.2);
  border-color: rgba(59, 130, 246, 0.5);
  color: #ffffff;
}

.entity-field-control.editable {
  background: rgba(255, 255, 255, 0.9);
  border-color: rgba(29, 78, 216, 0.5);
  color: #000000;
}

.entity-field-control.entity-field-dynamic {
  background: rgba(59, 130, 246, 0.15);
  border-color: rgba(59, 130, 246, 0.5);
  color: #3b82f6;
}

/* M3 Physical P4: `opacity: 0.8` removed (always-on for this element, not a hover/transient state) --
 * the element's own explicit semi-transparent background/border/italic already convey "display-only
 * field" without needing element-level compositing opacity too. */
.entity-field-display {
  width: 100%;
  background: rgba(255, 255, 255, 0.5);
  border: 1px solid rgba(29, 78, 216, 0.3);
  color: #000000;
  padding: 3px 6px;
  border-radius: 3px;
  font-size: 11px;
  box-sizing: border-box;
  font-style: italic;
}

.entity-field-range {
  text-align: center;
}

.entity-boolean {
  display: flex;
  align-items: center;
  gap: 8px;
}

.entity-boolean input {
  width: 16px;
  height: 16px;
  cursor: pointer;
  padding: 0;
  border: none;
  background: none;
}

.entity-boolean-label {
  font-size: 11px;
  color: var(--text-secondary);
}

.entity-required {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  color: #ff4444;
  font-weight: 700;
  padding: 0 6px;
  background: rgba(255, 68, 68, 0.1);
  border-radius: 3px;
  border: 1px solid rgba(255, 68, 68, 0.3);
  box-sizing: border-box;
}

.entity-required-icon {
  font-size: 14px;
}

.entity-required-text {
  font-size: 11px;
}

/* Choice row styling */
.node-row.choice-row-row {
  padding: 0;
}

.node-row.choice-manual-input-row {
  padding: 0;
}

.node-row.choice-manual-input-row .cell {
  padding-left: 4px;
  padding-right: 4px;
}

/* M4 SDU desktop physical visual/port fail gate, REVIEW-BLOCKER round (docs/handoffs/CHATGPT_M4_SDU_
 * DESKTOP_PHYSICAL_VISUAL_PORT_FAIL_REVIEW_BLOCKED.md, BLOCKER 3) -- Antonio's physical report: the
 * checkbox sits too close to the node's left border instead of centered in its reserved leading space.
 *
 * The previous round measured the checkbox against the wrong reference: it proved the checkbox is
 * exactly centered within the schema's declared `width: 20` CONTENT box (0px deviation), but that box
 * is not what a viewer sees -- what's actually VISIBLE as "the checkbox's reserved space" is the whole
 * leading region from the row's own start (flush with the node frame -- `.node-row` has no left
 * padding/border of its own) through wherever the NEXT cell (the option textarea) begins, i.e. this
 * cell's own OUTER/border-box (no row-level `gap` sits between cells -- `.node-row`'s base rule
 * declares none -- so cells are visually contiguous). That outer box was `4px` left padding + `20px`
 * content + `2px` right padding = 26px, asymmetric by construction. A symmetric padding always centers
 * a fixed-width child in BOTH its content-box AND its full outer box at once (content-box center =
 * `p + width/2`; outer-box center = `(width + 2p)/2 = width/2 + p` -- identical for any `p`, but only
 * when `p` is the SAME on both sides); the asymmetric 4px/2px is exactly what decoupled the two and
 * left the checkbox reading as off-center in the space a viewer actually sees, even though it was
 * already dead-center in the number the previous round chose to measure.
 * Fix: use the SAME `4px` every other cell in this row already uses (`.node-row.choice-manual-input-
 * row .cell` above) instead of a bespoke, asymmetric override -- not a new guessed number, the row's
 * own established value. */
.node-row.choice-manual-input-row .cell.checkbox-cell {
  padding: 4px 4px 4px 4px;
  justify-content: center;
}

.node-row.choice-manual-input-row .cell.textarea-cell {
  padding-top: 2px;
  padding-bottom: 2px;
  padding-left: 2px;
  align-items: center;
}

.node-row.choice-manual-input-row .cell.textarea-cell textarea {
  margin: 0;
}

.choice-number-cell {
  padding: 0 8px;
}

/* ═══════════════════════════════════════════════════════════════════════════
 * MASTER NODE PREVIEW - Stili per il nodo master nel canvas
 * ═══════════════════════════════════════════════════════════════════════════ */

/* Dropdown container with arrow icon */
.dropdown-wrapper {
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;
}

.dropdown-wrapper select {
  width: 100%;
  padding-right: 28px;
}

/* M3 Physical P4.1: P4's `top: calc(50% - 9px)` assumed the arrow glyph's rendered height equals its
 * font-size exactly -- unverified, and font metrics/line-height are not guaranteed to match font-size (a
 * measured Chromium regression on the analogous circular-port fix showed this class of assumption can be
 * off by a couple of px). `.dropdown-wrapper` above is `display:flex; align-items:center`, so leaving
 * `top`/`bottom` unset lets the flex algorithm center this absolutely-positioned child using its ACTUAL
 * rendered box, with no assumption about the glyph's metrics -- verified empirically (0.00px offset) in
 * the M3 Physical P4.1 handoff. Still no `transform`, so no WebKit foreignObject layer-trigger risk. */
.dropdown-wrapper .dropdown-arrow {
  position: absolute;
  right: 6px;
  font-size: 18px;
  color: var(--text-muted);
  pointer-events: none;
}

/* Material icon sizes */
.material-symbols-rounded.icon-18 {
  font-size: 18px;
}

.material-symbols-rounded.icon-14 {
  font-size: 14px;
}

/* Master node specific - gold/amber color scheme */
.node-blueprint.master-node .content-column {
  border-color: #f59e0b;
}

.node-blueprint.master-node .header-row {
  background: #f59e0b;
}

.node-blueprint.master-node .port {
  border-color: #f59e0b;
}

/* Active case highlighting for switch nodes */
.node-row.active-case {
  background-color: rgba(16, 185, 129, 0.18); /* Light green background */
  border-left: 3px solid #10b981; /* Green left border */
}

.node-row.active-case .cell input,
.node-row.active-case .cell textarea {
  background: rgba(16, 185, 129, 0.15);
  border-color: rgba(16, 185, 129, 0.6);
}
