/* Workflow board — full-bleed infinite canvas that replaces the panes host
   when the user picks "Workflow" in the sidebar. Lives at workspace grid-row
   2 alongside .panes-host and .agent-network; toggled via body[data-view].

   Renderer note: element nodes are DOM (contenteditable text, <img>, real
   HTML, resize handles, CSS theming — all free), connections are SVG (bezier,
   dashes, arrowheads — painful in DOM). Both live inside ONE transformed
   world div, so a box edge and the line attached to it can never drift.

   The three custom properties the JS writes on .workflow__canvas are the
   whole contract:
     --wf-inv   1/scale — counter-scales handles so they stay a constant
                SCREEN size at any zoom (a 9px handle must not become 36px)
     --wf-tx/ty translation in screen px, used to slide the dotted grid in
                lockstep with the world without transforming a huge element */

.workflow {
  position: relative;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  background:
    radial-gradient(900px 600px at 70% -10%, rgba(124, 140, 255, 0.10), transparent 65%),
    radial-gradient(800px 500px at -10% 110%, rgba(94, 226, 160, 0.06), transparent 65%),
    var(--bg);
}
.workflow[hidden] { display: none; }

/* The .workspace grid declares only two rows and relies on exactly one
   row-2 child being display:none. With a third child BOTH others must be
   hidden or the grid grows an implicit row. */
body[data-view="workflow"] .panes-host,
body[data-view="workflow"] .agent-network,
body[data-view="workflow"] .tarayici { display: none; }
body[data-view="workflow"] .tabs-host { visibility: hidden; }
body[data-view="workflow"] #claude-toggles,
body[data-view="workflow"] #voice-indicator,
body[data-view="workflow"] #new-tab-btn,
body[data-view="workflow"] #reconnect-btn { display: none; }

/* ---- chrome ------------------------------------------------------------ */

.workflow__chrome {
  position: absolute;
  top: 12px; left: 14px; right: 14px;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 14px;
  pointer-events: none;
  flex-wrap: wrap;
}
.workflow__title {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--fg-muted);
}
.workflow__tools,
.workflow__zoom {
  display: flex;
  gap: 6px;
  pointer-events: auto;
}
.workflow__zoom { margin-left: auto; }

.wf-tool {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 30px;
  padding: 0 11px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--fg-muted);
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.wf-tool:hover { background: var(--surface-3); color: var(--fg); }
.wf-tool.is-active {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-strong);
}
.wf-tool__icon { font-size: 13px; line-height: 1; }

/* 44×44 hit area (WCAG 2.5.5) with a 32×32 visible chip — same pattern as
   .agent-network__zoom and .tab__close. */
.wf-zoom-btn {
  position: relative;
  width: 44px; height: 44px;
  margin: -7px 0;
  display: grid;
  place-items: center;
  background: none;
  border: 0;
  color: var(--fg-muted);
  font-size: 15px;
  cursor: pointer;
}
.wf-zoom-btn::before {
  content: "";
  position: absolute;
  width: 32px; height: 32px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  border: 1px solid var(--border);
}
.wf-zoom-btn > span { position: relative; }
.wf-zoom-btn:hover { color: var(--fg); }
.wf-zoom-btn:hover::before { background: var(--surface-3); }

/* An empty status pill would still paint its background + border, so the
   chrome would show a stray grey blob whenever there's nothing to say. */
.workflow__status:empty { display: none; }
.workflow__status {
  pointer-events: none;
  font-size: 11.5px;
  color: var(--fg-dim);
  padding: 3px 8px;
  border-radius: 999px;
  background: var(--surface-2);
  border: 1px solid var(--border);
}
.workflow__status[data-state="offline"],
.workflow__status[data-state="error"] { color: var(--danger); border-color: var(--danger); }
.workflow__status[data-state="saving"] { color: var(--warning); }

/* ---- canvas / world ---------------------------------------------------- */

.workflow__canvas {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* Mandatory on a <div>: agent-network gets away without these because it
     is an <svg>. Without them the phone page-scrolls and pull-to-refreshes
     instead of panning. */
  touch-action: none;
  overscroll-behavior: contain;
  user-select: none;
  -webkit-user-select: none;
  cursor: grab;
  /* Defaults only. Both of these used to be REWRITTEN here on every zoom step,
     which invalidated inherited style for the entire board (see _applyView).
     They now live on the elements that read them — --wf-scale on the grid,
     --wf-inv on whichever nodes are currently showing chrome — and what is
     left here is the value everything else falls back to. It has to stay
     declared: `calc(1.5px * var(--wf-inv))` with no value at all is invalid at
     computed-value time, which would drop the rule rather than fall back. */
  --wf-inv: 1;
  --wf-scale: 1;
}

/* The dotted grid used to be painted by .workflow__canvas itself, with its
   --wf-tx/--wf-ty offsets rewritten on every pan frame. Those are custom
   properties on the ANCESTOR of every node, so each write dirtied inherited
   style for the whole board — a full style recalc per frame of pan, for a
   background offset. Giving the grid its own leaf layer confines that
   invalidation to one childless element. --wf-scale is written here too, for
   the same reason: this background is its only reader on the whole board. */
.workflow__grid {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  --wf-tx: 0px;
  --wf-ty: 0px;
  background-image:
    radial-gradient(circle, rgba(124, 140, 255, 0.16) 1.2px, transparent 1.2px),
    radial-gradient(circle, rgba(124, 140, 255, 0.10) 1px, transparent 1px);
  background-size:
    calc(120px * var(--wf-scale, 1)) calc(120px * var(--wf-scale, 1)),
    calc(24px * var(--wf-scale, 1)) calc(24px * var(--wf-scale, 1));
  background-position: var(--wf-tx) var(--wf-ty), var(--wf-tx) var(--wf-ty);
}
.workflow__canvas.is-panning { cursor: grabbing; }
.workflow__canvas.is-connecting { cursor: crosshair; }

.workflow__world {
  position: absolute;
  left: 0; top: 0;
  width: 0; height: 0;
  transform-origin: 0 0;
  z-index: 1;              /* above .workflow__grid */
}
/* 1×1 with overflow:visible — the SVG user space then IS world space, so
   paths draw at negative coordinates and arbitrarily far out without a
   giant rasterised surface. */
.workflow__links {
  position: absolute;
  left: 0; top: 0;
  width: 1px; height: 1px;
  overflow: visible;
  z-index: 0;
  pointer-events: none;
}
.workflow__links .wf-link__hit {
  stroke: transparent;
  stroke-width: 18;
  fill: none;
  pointer-events: stroke;
  cursor: pointer;
}
.workflow__links .wf-link__line { fill: none; stroke-linecap: round; }
.workflow__links .wf-link.is-selected .wf-link__line {
  stroke: var(--accent-strong);
  filter: drop-shadow(0 0 4px var(--accent-soft));
}
.workflow__links .wf-link__label {
  fill: var(--fg-muted);
  font-family: var(--font-ui);
  text-anchor: middle;
  paint-order: stroke;
  stroke: var(--bg);
  stroke-width: 4px;
  stroke-linejoin: round;
}
.workflow__nodes { position: absolute; left: 0; top: 0; width: 0; height: 0; }

/* X-ray layer: a faint duplicate of every connection painted OVER the boxes,
   so a link routed behind one is still readable. Because it carries the same
   .workflow__links class it inherits all the link styling above; the modifier
   only lifts it above the nodes and knocks it back to 30%. */
.workflow__links--ghost {
  z-index: 2;
  opacity: 0.3;
  pointer-events: none;
}

/* ---- review status ----------------------------------------------------- */
/* The badge is what turns this board into agent-readable memory: ✓ means an
   agent may take the element as settled, ✕ means known-wrong, ? means open.
   It sits half outside the top-left corner so it never covers content, and
   counter-scales like the resize handles so it stays a constant SCREEN size. */
.wf-status {
  position: absolute;
  top: 0; left: 0;
  width: 20px; height: 20px;
  margin: -10px 0 0 -10px;
  transform: scale(var(--wf-inv, 1));
  transform-origin: 0 0;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--fg-dim);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  z-index: 3;
  /* Unmarked elements shouldn't look annotated — the empty badge only shows
     on hover or while the element is selected. pointer-events follows the
     pixels: on touch there is no hover, so a permanently-live invisible badge
     would let a stray thumb near the corner flip an element's review mark. */
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
}
.wf-el:hover .wf-status,
.wf-el.is-selected .wf-status,
.wf-el.is-reviewed .wf-status { opacity: 1; pointer-events: auto; }
.wf-status:hover { border-color: var(--accent); }
.wf-status[data-status="verified"] { color: #5ee2a0; border-color: #5ee2a0; }
.wf-status[data-status="unsure"] { color: #f7c873; border-color: #f7c873; }
.wf-status[data-status="rejected"] { color: #ff7a8a; border-color: #ff7a8a; }

/* A mark reads at a glance without shouting. Fed through --wf-status-tint so
   it only applies when the user has NOT picked an explicit border colour —
   see the fallback chain on .wf-el__frame. */
.wf-el[data-status="verified"] { --wf-status-tint: rgba(94, 226, 160, 0.55); }
.wf-el[data-status="unsure"] { --wf-status-tint: rgba(247, 200, 115, 0.55); }
.wf-el[data-status="rejected"] { --wf-status-tint: rgba(255, 122, 138, 0.55); }
/* Known-wrong content should recede rather than compete with live content. */
.wf-el[data-status="rejected"] .wf-el__frame { opacity: 0.72; }

/* ---- alignment guides -------------------------------------------------- */
/* Same 1×1 overflow-visible trick as .workflow__links so guide coordinates
   ARE world coordinates; z-index puts them over the boxes they align, and
   the JS sets vector-effect so the hairline stays 1px at every zoom. */
.workflow__guides {
  position: absolute;
  left: 0; top: 0;
  width: 1px; height: 1px;
  overflow: visible;
  z-index: 3;
  pointer-events: none;
}
.wf-guide {
  stroke: #ff7a8a;
  stroke-width: 1;
  opacity: 0.9;
}
.wf-guide--centre { stroke-dasharray: 5 4; }

/* ---- elements ---------------------------------------------------------- */

.wf-el {
  position: absolute;
  box-sizing: border-box;
  overflow: visible;
  z-index: 1;
}
.wf-el__frame {
  position: absolute;
  inset: 0;
  overflow: hidden;
  /* Zoomed in, five or six boxes fill the screen and the other 170-odd are
     somewhere off it — but the engine still laid out and painted every one of
     them, wrapping text and shaping glyphs nobody can see. This hands that
     decision to the engine, which can make it on the compositor instead of on
     the main thread. It is safe HERE and nowhere else on the node: the frame's
     size comes entirely from the node's explicit px w/h (inset:0), so nothing
     can reflow when content is skipped and no contain-intrinsic-size is
     needed, and the implied paint containment is a clip this box already has
     from overflow:hidden. On .wf-el itself that clip would cut off the ports,
     handles and badge, which deliberately hang outside the box.
     NOT the same idea as culling nodes from the DOM in JS — that was measured
     last round and lost, because mutating the DOM inside the composited world
     forces a re-raster mid-gesture. Nothing is mutated here. */
  content-visibility: auto;
  border-radius: inherit;
  background: var(--wf-fill, var(--surface-2));
  /* Fallback chain, most specific first: an explicit stroke the user picked
     always wins; otherwise a review mark tints the frame; otherwise default. */
  border: var(--wf-stroke-w, 1px) solid
          var(--wf-stroke, var(--wf-status-tint, var(--border-strong)));
  border-radius: var(--wf-radius, 10px);
}
.wf-el--text > .wf-el__frame { background: transparent; border-color: transparent; }
.wf-el--image > .wf-el__frame { background: transparent; }

.wf-el__text {
  position: absolute;
  inset: 0;
  display: flex;
  padding: 10px 12px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  line-height: 1.35;
  color: var(--wf-text, var(--fg));
  font-size: calc(var(--wf-fs, 16) * 1px);
  font-weight: var(--wf-fw, 400);
  outline: none;
}
.wf-el__text[contenteditable="true"],
.wf-el__text[contenteditable="plaintext-only"] {
  user-select: text;
  -webkit-user-select: text;
  cursor: text;
  background: rgba(124, 140, 255, 0.06);
}
.wf-el__img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  border-radius: inherit;
  pointer-events: none;
  -webkit-user-drag: none;
}
.wf-el__html {
  position: absolute;
  inset: 0;
  overflow: hidden;
  padding: var(--wf-pad, 12px);
  color: var(--wf-text, var(--fg));
  font-size: 14px;
  line-height: 1.45;
}
.wf-el__html :is(h1, h2, h3, h4) { margin: 0.2em 0 0.35em; line-height: 1.25; }
.wf-el__html h1 { font-size: 1.5em; }
.wf-el__html h2 { font-size: 1.28em; }
.wf-el__html h3 { font-size: 1.1em; }
.wf-el__html p, .wf-el__html ul, .wf-el__html ol { margin: 0.35em 0; }
.wf-el__html ul, .wf-el__html ol { padding-left: 1.2em; }
.wf-el__html a { color: var(--accent-strong); }
.wf-el__html code, .wf-el__html pre {
  font-family: var(--font-mono);
  font-size: 0.9em;
  background: rgba(0, 0, 0, 0.28);
  border-radius: 4px;
  padding: 0.1em 0.32em;
}
.wf-el__html pre { padding: 8px 10px; overflow: auto; }
.wf-el__html table { border-collapse: collapse; }
.wf-el__html td, .wf-el__html th { border: 1px solid var(--border-strong); padding: 3px 6px; }
.wf-el__html img { max-width: 100%; }

.wf-el__placeholder {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--fg-dim);
  font-size: 13px;
  border: 1px dashed var(--border-strong);
  border-radius: inherit;
}

/* Selection ring and handles counter-scale so they read the same at any
   zoom. outline (not border) so it never affects layout. */
.wf-el.is-selected > .wf-el__frame {
  outline: calc(1.5px * var(--wf-inv)) solid var(--accent);
  outline-offset: calc(2px * var(--wf-inv));
}
.wf-el.is-remote > .wf-el__frame {
  outline: calc(1.5px * var(--wf-inv)) dashed var(--success);
}

.wf-el__handles, .wf-el__ports { position: absolute; inset: 0; pointer-events: none; }
.wf-el.is-selected .wf-el__handles { pointer-events: none; }

.wf-h {
  position: absolute;
  width: 10px; height: 10px;
  background: var(--bg);
  border: 1.5px solid var(--accent-strong);
  border-radius: 2px;
  pointer-events: auto;
  transform: scale(var(--wf-inv));
  display: none;
}
.wf-el.is-selected .wf-h { display: block; }
.wf-h--nw { left: 0;    top: 0;    transform-origin: 0 0;       margin: -5px 0 0 -5px; cursor: nwse-resize; }
.wf-h--n  { left: 50%;  top: 0;    transform-origin: 50% 0;     margin: -5px 0 0 -5px; cursor: ns-resize; }
.wf-h--ne { left: 100%; top: 0;    transform-origin: 100% 0;    margin: -5px 0 0 -5px; cursor: nesw-resize; }
.wf-h--e  { left: 100%; top: 50%;  transform-origin: 100% 50%;  margin: -5px 0 0 -5px; cursor: ew-resize; }
.wf-h--se { left: 100%; top: 100%; transform-origin: 100% 100%; margin: -5px 0 0 -5px; cursor: nwse-resize; }
.wf-h--s  { left: 50%;  top: 100%; transform-origin: 50% 100%;  margin: -5px 0 0 -5px; cursor: ns-resize; }
.wf-h--sw { left: 0;    top: 100%; transform-origin: 0 100%;    margin: -5px 0 0 -5px; cursor: nesw-resize; }
.wf-h--w  { left: 0;    top: 50%;  transform-origin: 0 50%;     margin: -5px 0 0 -5px; cursor: ew-resize; }

.wf-port {
  position: absolute;
  width: 11px; height: 11px;
  margin: -5.5px 0 0 -5.5px;
  border-radius: 50%;
  background: var(--surface-1);
  border: 1.5px solid var(--accent);
  opacity: 0;
  /* Invisible must also mean untouchable. With hover unavailable, a phone
     would otherwise start dragging a connection off a port the user cannot
     see — the reveal rules below hand interactivity back with the pixels. */
  pointer-events: none;
  cursor: crosshair;
  transform: scale(var(--wf-inv));
  transition: opacity var(--t-fast) var(--ease);
}
.wf-el:hover .wf-port,
.wf-el.is-selected .wf-port,
.workflow__canvas.is-connecting .wf-port { opacity: 1; pointer-events: auto; }
.wf-port:hover { background: var(--accent); }
/* Ports sit OUTSIDE the frame. The mid-edge resize handles live exactly on
   the border, so a port centred on the same point would cover them and edge
   resize would be unreachable. The offset is written as
   `13px * var(--wf-inv)` — i.e. 13/scale world units — so it renders as a
   constant 13 SCREEN px at any zoom, the same trick the handles use. */
.wf-port--top {
  left: 50%; top: 0; transform-origin: 50% 100%;
  margin-top: calc(-5.5px - 13px * var(--wf-inv));
}
.wf-port--bottom {
  left: 50%; top: 100%; transform-origin: 50% 0;
  margin-top: calc(-5.5px + 13px * var(--wf-inv));
}
.wf-port--left {
  left: 0; top: 50%; transform-origin: 100% 50%;
  margin-left: calc(-5.5px - 13px * var(--wf-inv));
}
.wf-port--right {
  left: 100%; top: 50%; transform-origin: 0 50%;
  margin-left: calc(-5.5px + 13px * var(--wf-inv));
}

/* Marquee lives in SCREEN space, outside the transformed world. */
.wf-marquee {
  position: absolute;
  z-index: 2;
  border: 1px solid var(--accent);
  background: var(--accent-soft);
  border-radius: 2px;
  pointer-events: none;
}

.workflow__empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  text-align: center;
  color: var(--fg-dim);
  pointer-events: none;
  z-index: 1;
}
/* reset.css's [hidden]{display:none} has the SAME specificity as the class
   above and loses on source order, so the empty state would stay painted on
   top of a full board. Restate it with the attribute attached. */
.workflow__empty[hidden] { display: none; }
.workflow__empty h2 { color: var(--fg-muted); font-size: 16px; margin-bottom: 6px; }
.workflow__empty p { font-size: 13px; }

/* ---- overlays (info / prompt) ------------------------------------------ */

.wf-overlay-backdrop {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: grid;
  place-items: center;
  background: rgba(5, 7, 11, 0.62);
  backdrop-filter: blur(2px);
  padding: 20px;
}
.wf-overlay {
  width: min(520px, 100%);
  max-height: min(70vh, 620px);
  display: flex;
  flex-direction: column;
  background: var(--surface-1);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-2);
  overflow: hidden;
}
.wf-overlay__head {
  padding: 14px 16px 10px;
  border-bottom: 1px solid var(--border);
}
.wf-overlay__title { font-size: 14px; font-weight: 600; color: var(--fg); }
.wf-overlay__sub { font-size: 12px; color: var(--fg-dim); margin-top: 3px; }
.wf-overlay__body { padding: 10px 16px 14px; overflow: auto; }
.wf-overlay__foot {
  padding: 10px 16px 14px;
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  border-top: 1px solid var(--border);
}
.wf-overlay textarea, .wf-overlay input[type="text"] {
  width: 100%;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  color: var(--fg);
  font: inherit;
  padding: 8px 10px;
  resize: vertical;
}
.wf-overlay textarea { min-height: 160px; font-family: var(--font-mono); font-size: 12.5px; }
.wf-overlay input[type="text"]:focus, .wf-overlay textarea:focus {
  outline: none;
  border-color: var(--accent);
}
.wf-overlay__hint { font-size: 11.5px; color: var(--fg-dim); margin-top: 6px; }

.wf-event {
  display: flex;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 12.5px;
}
.wf-event:last-child { border-bottom: 0; }
.wf-event__badge {
  flex: none;
  width: 54px;
  text-align: center;
  align-self: flex-start;
  padding: 2px 0;
  border-radius: 999px;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.3px;
  background: var(--surface-3);
  color: var(--fg-muted);
}
.wf-event__badge--agent { background: rgba(94, 226, 160, 0.14); color: var(--success); }
.wf-event__badge--user { background: var(--accent-soft); color: var(--accent-strong); }
.wf-event__who { color: var(--fg); font-weight: 500; }
.wf-event__when { color: var(--fg-dim); }
.wf-event__fields { color: var(--fg-muted); margin-top: 2px; }
.wf-event__fields code {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--fg-muted);
}

/* ---- level of detail (zoomed out) -------------------------------------- */
/* Past a certain distance the board stops being a document and becomes a map,
   and the detail that made it a document turns into pure cost. Two rules,
   because two different things stop paying their way at two different moments.

   PER ELEMENT — data-lod-body="off", written by the renderer once an element's
   own glyphs fall under ~7.5 screen px. Only that element's text goes; a 60px
   section title is still perfectly readable at 0.2× and stays, which is what
   keeps a zoomed-out board navigable. The text is replaced by faint bars on
   the same rhythm as the lines that were there, so a card still reads as
   "content here" instead of an empty rectangle — one gradient fill instead of
   a paragraph of shaped, rasterised glyphs.

   PER VIEW — data-lod on the canvas ("chrome" under 0.5×, "flat" under 0.3×)
   for everything sized for a pointer rather than for reading: four connection
   ports per node, each with a margin written as `13px * var(--wf-inv)` so
   every zoom step recomputes 700+ of them for circles nobody can hit at this
   size; the 24px dot grid, a 5px moiré at 0.2×; and the x-ray link layer,
   whose whole job is keeping a link readable behind a box at a scale where
   nothing is readable.

   All of it is presentation: the elements themselves are untouched, so a
   zoomed-out board still saves, prints and screenshots in full. */

.wf-el[data-lod-body="off"] .wf-el__text:not([contenteditable]),
.wf-el[data-lod-body="off"] .wf-el__html { display: none; }

/* Bars only where there is really content (data-body, set by the renderer) —
   an empty container box must not sprout fake paragraphs. Their rhythm is
   derived from the element's own font size, so they land where its lines did. */
.wf-el[data-lod-body="off"][data-body] > .wf-el__frame {
  color: var(--wf-text, var(--fg-muted));
}
.wf-el[data-lod-body="off"][data-body]:not(.is-editing) > .wf-el__frame::after {
  content: "";
  position: absolute;
  inset: calc(var(--wf-fs, 16) * 0.6px) calc(var(--wf-fs, 16) * 0.75px);
  opacity: 0.26;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    to bottom,
    currentColor 0, currentColor calc(var(--wf-fs, 16) * 0.4px),
    transparent calc(var(--wf-fs, 16) * 0.4px), transparent calc(var(--wf-fs, 16) * 1.35px));
}

/* Ports only — the four of them per node are live elements whose offsets are
   written as `13px * var(--wf-inv)`, so 700+ of them get recomputed on every
   zoom step for circles too small to aim at down here. (Connect mode still
   works: dragging from the BODY of a node starts a link with an auto anchor.)
   The resize handles deliberately stay: they are already display:none unless
   the element is selected, so they cost nothing to leave in — and hiding them
   would mean a box selected at 0.4× could no longer be resized. */
.workflow__canvas[data-lod] .wf-el__ports { display: none; }
.workflow__canvas[data-lod] .wf-link__label { display: none; }
.workflow__canvas[data-lod] .workflow__links--ghost { display: none; }

/* Only the coarse 120px layer of the grid survives; the 24px one is a moiré
   at this scale. */
.workflow__canvas[data-lod] .workflow__grid {
  background-image:
    radial-gradient(circle, rgba(124, 140, 255, 0.16) 1.2px, transparent 1.2px);
  background-size: calc(120px * var(--wf-scale, 1)) calc(120px * var(--wf-scale, 1));
  background-position: var(--wf-tx) var(--wf-ty);
}

/* Under 0.3× the review badges are the last constant-SCREEN-size chrome left,
   and 180 of them cover the map they annotate. The border tint carries the
   same information at this distance. */
.workflow__canvas[data-lod="flat"] .wf-status { display: none; }
