/* ============================================================
   WORKFLOW TOP BAR — LIGHT GLASS TOKENS
   ------------------------------------------------------------
   The bar is a sibling of .wf-nav (not a descendant) AND its
   popover (.wf-bpop) is portalled to <body>, so neither inherits
   the nav's CSS custom properties. Exactly like the codebase's
   detached .wf-limitpanel.is-docked card, the FULL palette is
   RE-DECLARED here. It is declared on `.wf-bar, .wf-bpop`
   together so the bar and its portalled popover can never drift.
   Night / high-contrast are pure token overrides layered on the
   same selectors. If a token value changes on .wf-nav, change it
   here (and on the docked card) too.
   ============================================================ */
.wf-bar,
.wf-bpop,
.wf-fly,
.wf-toolbar{
  --surface:rgba(255,255,255,.56);
  --surface-solid:#FBFCFA;
  --border:rgba(255,255,255,.70);
  --divider:rgba(70,92,80,.16);
  --label:#41584C;
  --label-hover:#27543C;
  --header-hover:rgba(56,120,90,.09);
  --text:#1F2D26;
  --icon:#3C5247;
  --icon-hover:#2F6B4D;
  --muted:#6E8077;
  --row-hover:rgba(60,74,67,.06);
  --active-bg:rgba(255,255,255,.40);
  --active-text:#235446;
  --accent:#5C9A7E;
  --accent-line:rgba(92,154,126,.45);
  --dot-green:#4FA37C;
  --dot-orange:#E8C08A;
  --dot-black:#2B332E;
  --dot-gray:#A7B0AA;
  --search-bd-focus:rgba(255,255,255,.95);
  --search-bg-focus:rgba(255,255,255,.50);
  --focus:rgba(60,120,92,.55);
  --tooltip-bg:rgba(24,40,32,.94);
  --tooltip-fg:#FFFFFF;
  --blur:blur(20px) saturate(180%) brightness(1.03);
  --shadow:inset 0 1px 0 rgba(255,255,255,.6), 0 10px 30px rgba(55,72,64,.14);
  --radius:9px;
  --radius-lg:16px;
  --ease:cubic-bezier(.32,.72,0,1);   /* the one unified motion curve */
  --ease-bar:var(--aw-chrome-ease);   /* ROUND 30: the chrome's one curve; was cubic-bezier(.4,0,.2,1) — a
                                         more balanced curve. The global
                                         --ease is heavily decelerating
                                         (≈77% of distance in the first
                                         ~25% of time, then a long slow
                                         tail) which on a wide morph
                                         like the header reads as
                                         "freezes halfway" because
                                         nothing visible moves through
                                         the tail. This curve spreads
                                         the motion more evenly so the
                                         grow/shrink feels like one
                                         continuous motion. */
  --dur:.24s;
  --dur-rev:.42s;

  /* ---- bar-only tokens ---- */
  --bar-h:48px;
  --bar-gap:14px;       /* inset of the floating card from the edges */
  --bar-radius:16px;
  --cta:#1F6E45;
  --badge-bg:rgba(232,192,138,.26);
  --badge-bd:rgba(206,160,92,.45);
  --badge-fg:#7A5A23;
  --bar-shadow:inset 0 1px 0 rgba(255,255,255,.6),0 10px 30px rgba(55,72,64,.14);
}

/* ---- NIGHT MODE — calm green-tinted dark glass, same family ---- */
.wf-bar.is-night,
.wf-bpop.is-night,
.wf-fly.is-night,
.wf-toolbar.is-night{
  --surface:rgba(28,36,32,.66);
  --surface-solid:#1B221E;
  --border:rgba(255,255,255,.12);
  --divider:rgba(255,255,255,.13);
  --label:#9FB4AA;
  --label-hover:#CFE9DB;
  --header-hover:rgba(120,200,160,.13);
  --text:#E7EFE9;
  --icon:#A9BEB4;
  --icon-hover:#CFE9DB;
  --muted:#8A9C92;
  --row-hover:rgba(255,255,255,.06);
  --active-bg:rgba(255,255,255,.08);
  --active-text:#8FD3B0;
  --accent:#6FB893;
  --accent-line:rgba(111,184,147,.52);
  --search-bd-focus:rgba(255,255,255,.30);
  --search-bg-focus:rgba(255,255,255,.06);
  --focus:rgba(120,200,160,.6);
  --tooltip-bg:rgba(231,239,233,.96);
  --tooltip-fg:#10231A;
  --shadow:inset 0 1px 0 rgba(255,255,255,.06), 0 10px 30px rgba(0,0,0,.42);
  --cta:#2E8B5C;
  --badge-bg:rgba(232,192,138,.16);
  --badge-bd:rgba(232,192,138,.40);
  --badge-fg:#E6C892;
  --bar-shadow:inset 0 1px 0 rgba(255,255,255,.06),0 10px 30px rgba(0,0,0,.42);
}

/* ---- HIGH CONTRAST — solid surfaces, hard borders, stronger
        focus. Layers on top of light OR night. ---- */
.wf-bar.is-contrast,
.wf-bpop.is-contrast,
.wf-fly.is-contrast,
.wf-toolbar.is-contrast{
  --surface:#FFFFFF;
  --surface-solid:#FFFFFF;
  --border:#2B3A33;
  --divider:#2B3A33;
  --label:#10231A;
  --label-hover:#000000;
  --header-hover:rgba(31,110,69,.16);
  --text:#0A1410;
  --icon:#10231A;
  --icon-hover:#000000;
  --muted:#33453C;
  --row-hover:rgba(31,110,69,.14);
  --active-bg:rgba(31,110,69,.16);
  --active-text:#0A3D27;
  --accent:#176B40;
  --accent-line:#176B40;
  --search-bd-focus:#0A1410;
  --search-bg-focus:rgba(31,110,69,.06);
  --focus:#0A3D27;
  --badge-bg:rgba(154,110,30,.22);
  --badge-bd:#7A5A23;
  --badge-fg:#5A3F12;
  --blur:none;
  --bar-shadow:0 0 0 1px #2B3A33,0 10px 26px rgba(0,0,0,.12);
}
.wf-bar.is-night.is-contrast,
.wf-bpop.is-night.is-contrast,
.wf-fly.is-night.is-contrast,
.wf-toolbar.is-night.is-contrast{
  --surface:#0E1411;
  --surface-solid:#0E1411;
  --border:#CFE9DB;
  --divider:#7FA793;
  --label:#E7EFE9;
  --label-hover:#FFFFFF;
  --header-hover:rgba(143,211,176,.20);
  --text:#FFFFFF;
  --icon:#E7EFE9;
  --icon-hover:#FFFFFF;
  --muted:#B6C8BE;
  --row-hover:rgba(143,211,176,.16);
  --active-bg:rgba(143,211,176,.20);
  --active-text:#BFEBD3;
  --accent:#8FD3B0;
  --accent-line:#8FD3B0;
  --search-bd-focus:#FFFFFF;
  --focus:#BFEBD3;
  --tooltip-bg:#FFFFFF;
  --tooltip-fg:#0A1410;
  --blur:none;
  --bar-shadow:0 0 0 1px #CFE9DB,0 10px 28px rgba(0,0,0,.55);
}
/* high-contrast: thicker, always-visible focus everywhere */
.wf-bar.is-contrast :focus-visible,
.wf-bpop.is-contrast :focus-visible,
.wf-toolbar.is-contrast :focus-visible{
  outline:2px solid var(--focus)!important;outline-offset:2px!important;
  box-shadow:none!important}

/* AirWanderer integration: the upstream component's demo `body { … }`
   block (dot-grid background + system font) was removed here. The
   AirWanderer shell owns body styling in main.css. */

/* ============================================================
   BAR SHELL
   ============================================================ */
.wf-bar,.wf-bar *{box-sizing:border-box}
.wf-bar{
  position:sticky;top:var(--bar-gap);z-index:40;
  /* SPLIT layout: the engraved logo is a separate fixed mark on
     the left (portalled to <body>). The header is a SHORT pill
     sitting right after it — content-width, with open canvas to
     its right. Logo lane = bar-gap + 40px tile + bar-gap = 68px. */
  margin:var(--bar-gap);
  margin-left:calc(2 * var(--bar-gap) + 40px);
  width:-moz-max-content;width:max-content;
  max-width:calc(100% - 3*var(--bar-gap) - 40px);
  height:var(--bar-h);
  overflow:hidden;
  background-color:var(--surface);
  border:1px solid var(--border);
  border-radius:var(--bar-radius);
  -webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);
  box-shadow:var(--bar-shadow);
  color:var(--text);font-family:inherit;user-select:none;
  transition:background-color var(--dur) var(--ease),
             box-shadow var(--dur) var(--ease),
             border-color var(--dur) var(--ease),
             width var(--aw-chrome-dur) var(--ease-bar),
             height var(--aw-chrome-dur) var(--ease-bar),
             border-radius var(--aw-chrome-dur) var(--ease-bar),
             transform var(--aw-chrome-dur) var(--ease-bar);
  animation:wf-bar-in .55s var(--ease) backwards;
}
.wf-bar.wf-noanim{transition:none}
/* HEADER COLLAPSED — the bar SHRINKS to a 40x40 tile, the SAME
   size as the logo (width driven by JS); content fades out and
   only the toggle remains, optically centred in the tile. */
/* HEADER COLLAPSED — the bar shrinks to a 40x40 tile.
   translateY(4px) aligns the 40px tile vertically with the 40px
   logo: the rail is 48px tall with the logo centered in it
   (logo top at y=18), but the collapsed bar is only 40px and
   sticky-anchored at top:14, so without the translate its top
   edge sits at y=14 — 4px above the logo. The translate
   transitions cleanly with the rest of the morph on the same
   .34s curve. */
.wf-bar.is-collapsed{
  height:40px;border-radius:12px;transform:translateY(4px);
  cursor:pointer}
/* NOTE: there is intentionally no `.wf-bar.is-collapsed .wf-bar__inner`
   override here. The previous incarnation set padding:0 and
   justify-content:center on the inner when collapsed — that was a
   non-transitioned snap at the moment the class toggled, with every
   child of the inner having to re-resolve its position in the new
   layout in the same frame the bar started its smooth width
   transition. Removing the override means the inner stays at the
   same padding (0 90 0 13) in both states; the chevron (right:0)
   and acct (right:46) are absolute against the bar, not the inner,
   so they ignore the inner.padding entirely; and the cwrap content
   is hidden in the collapsed state purely by cwrap.opacity:0 plus
   the bar's overflow:hidden clipping the narrow 40px tile region.
   Net effect: the only things that change between states are the
   bar's outer size, its border-radius, cwrap.opacity, and the
   chevron icon's rotation — all of which are transitioned on the
   same .34s curve. Zero non-transitioned layout snaps. */
.wf-bar.is-collapsed .wf-bar__cwrap{
  opacity:0;visibility:hidden;pointer-events:none;
  /* COLLAPSE: content fades out fast (.12s, no delay), so the
     bar then shrinks visibly empty rather than dragging a clipped
     half-content around. visibility flips AFTER the fade so the
     element stays focusable/clickable until it's invisible. */
  transition:opacity .12s ease,visibility 0s linear .12s}
@keyframes wf-bar-in{
  from{opacity:0;transform:translateY(-8px)}
  to{opacity:1;transform:none}
}
/* acting-as cue — a calm accent ring around the whole floating
   card (the old bottom hairline only made sense edge-attached) */
.wf-bar.is-actingas{
  border-color:var(--accent-line);
  box-shadow:var(--bar-shadow),0 0 0 3px var(--header-hover)}

.wf-bar__inner{
  display:flex;align-items:center;height:100%;
  padding:0 90px 0 13px;          /* right column reserved for acct (right:46) + chevron (right:0) */
  gap:0}

.wf-title,.wf-run{
  animation:wf-reg-in .55s var(--ease) backwards}
.wf-title{animation-delay:.06s}
.wf-run{animation-delay:.12s}
.wf-acct{
  animation:wf-acct-in .55s var(--ease) backwards;
  animation-delay:.18s}
@keyframes wf-reg-in{
  from{opacity:0;transform:translateY(5px)}
  to{opacity:1;transform:none}
}
@keyframes wf-acct-in{
  /* acct is position:absolute with transform:translateY(-50%) for centering,
     so its entrance animates opacity only — a translateY keyframe would
     wipe out the centering for the duration of the animation. */
  from{opacity:0}
  to{opacity:1}
}
@media (prefers-reduced-motion:reduce){
  .wf-bar,.wf-logo,.wf-title,.wf-run,.wf-acct{animation:none}
}

/* a fixed breathing gap between the breadcrumb and the actions
   (replaces the old flex spacers — the bar is content-width now) */
.wf-bar__gap{flex:none;width:18px}
.wf-bar__spacer{flex:1 1 auto;min-width:16px}

/* ============================================================
   LOGO — the decided ENGRAVED "#" tile. It is NOT inside the
   bar: it is portalled to <body> and pinned to the LEFT, aligned
   with the navigation rail, while the header floats on the right.
   Brand-constant across themes (it does not read tokens).
   To re-align with a differently-offset / collapsed nav, override
   the `left` / `top` here — this is the single alignment hook.
   ============================================================ */
/* the rail is a fixed mark portalled to <body> holding ONLY the
   brand logo, aligned with the side-nav rail on the left and
   vertically centred on the header's line. The collapse toggle
   is NOT here anymore — it lives inside the header. */
.wf-rail{
  position:fixed;left:14px;top:14px;z-index:41;height:48px;
  display:flex;align-items:center;pointer-events:none;
  animation:wf-bar-in .55s var(--ease) backwards}
.wf-rail>*{pointer-events:auto}

.wf-logo{
  flex:none;width:40px;height:40px;display:flex;align-items:center;
  justify-content:center;padding:0;cursor:pointer;
  border:1px solid rgba(255,255,255,.09);border-radius:12px;
  background:linear-gradient(168deg,#3C3C40 0%,#222225 46%,#101012 100%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.14),
             inset 0 -1px 0 rgba(0,0,0,.30),
             0 4px 12px rgba(0,0,0,.28)}
/* no hover/active motion on the logo — it is a brand mark, not
   a control surface. Keyboard focus ring kept for a11y. */
.wf-logo:focus-visible{outline:0;
  box-shadow:0 0 0 2px #fff,0 0 0 4px rgba(90,90,96,.65)}
.wf-logo__mark{width:21px;height:21px;display:block;
  filter:drop-shadow(0 -1px 0 rgba(0,0,0,.50))
         drop-shadow(0 1px 0 rgba(255,255,255,.18))}

/* The chevron is anchored to the bar's RIGHT EDGE always — it is
   a sibling of cwrap (not a descendant), and uses position:absolute
   so it slides with the bar's right edge across the whole .34s
   morph instead of vanishing when the inline-flex would push it
   off the still-narrow, overflow:hidden bar. In the collapsed tile
   its right:0 anchor + 40px width make it fill the 40x40 box
   exactly; in the expanded bar it sits as a 40px column at the far
   right. One element, one position rule, both states. */
.wf-bar .wf-hdrtoggle{
  flex:none;position:absolute;right:0;top:0;bottom:0;
  width:40px;height:auto;                  /* override .wf-bar__ic 34x34 */
  display:flex;align-items:center;justify-content:center;
  margin:0;padding:0;border:0;background:transparent;
  color:var(--label);border-radius:12px;cursor:pointer;z-index:3;
  visibility:visible;
  transition:background-color .12s,color .12s}
.wf-bar .wf-hdrtoggle:hover{
  background:var(--header-hover);color:var(--cta)}
.wf-bar .wf-hdrtoggle:focus-visible{outline:0;
  box-shadow:0 0 0 2px var(--focus)}
.wf-hdrtoggle svg{transition:transform var(--aw-chrome-dur) var(--ease-bar)}
/* ROUND 30: in the collapsed tile the chevron is 38 in the 38 px box (it was 40, two px past the
   border) with the inner radius */
.wf-bar.is-collapsed .wf-hdrtoggle{width:38px;border-radius:11px}
.wf-hdrtoggle.is-collapsed svg{transform:rotate(180deg)}
/* The chevron rotation used to be .3s var(--ease) — a sharply
   decelerating curve, ~77% rotated by 25% of the duration. The
   rest of the header morph runs at var(--aw-chrome-dur) var(--ease-bar) (a more
   balanced curve), so the chevron was snap-rotating to a stop
   while the bar (and the chevron's own sliding position, which
   inherits the bar's right-edge motion) was still steadily
   growing. Two motions on the same element on two different
   curves and durations = exactly the "two animations locked
   into one action" twitch. Synced to var(--aw-chrome-dur) var(--ease-bar) so
   the rotation, the slide and the bar grow all share one
   timeline.

   The 20px-when-collapsed glyph size was also dropped: it
   snapped from 20→18 instantly at t=0 of expand (no transition),
   on top of the rotation start — a small extra twitch. The icon
   now stays at the .wf-bar__ic base 18px in both states. */

/* ---- collapsible content wrap + mode clusters ----
   The cwrap holds everything that the collapsed tile hides
   (title, run, account, tools-mode cluster). The chevron is NOT
   in here — it lives at .wf-bar__inner level and stays visible
   throughout the morph, anchored to the right edge.

   On EXPAND the cwrap opacity fades 0→1 on the SAME .34s/curve
   as the bar's width, no delay. Both motions run together and
   end together → the user perceives one continuous animation
   instead of "bar grows, pause, content pops in". On COLLAPSE
   the content fades out fast (see .is-collapsed rule below) so
   the bar then shrinks visibly empty. */
.wf-bar__cwrap{
  display:flex;align-items:center;min-width:0;
  opacity:1;
  transition:opacity var(--aw-chrome-dur) var(--ease-bar),
             visibility 0s linear 0s}
.wf-bar__cwrap.is-swapping{opacity:0;transition:opacity .12s ease}
.wf-bar__mode{display:flex;align-items:center;gap:10px;min-width:0}
.wf-bar__mode--tools{display:none;gap:4px}
.wf-bar__cwrap.is-tools .wf-bar__mode--default{display:none}
.wf-bar__cwrap.is-tools .wf-bar__mode--tools{display:flex}
/* while pinned the tools live in the floating palette, so the
   in-header tools button is redundant */
.wf-bar.wf-tools-pinned .wf-toolsbtn{display:none}

/* shared canvas-tools content (moves between header & palette) */
.wf-tools__content{display:flex;align-items:center;gap:4px;min-width:0}

/* palette at phone-tiny widths (driven by .is-tiny added in
   applyResponsive when vw<380): the drag grip and reset-position
   button are the lowest-value controls — drag is awkward on touch
   and reset is rarely needed. Hiding them recovers ~60px and lets
   the palette fit at vw≥265, covering all real phones. */
.wf-toolbar.is-tiny .wf-toolbar__grip,
.wf-toolbar.is-tiny .wf-toolbar__reset{display:none}

/* Bar's tools-mode at narrow viewports: tools + pin + Back overflow
   the bar's cwrap (and crash into the absolute-positioned chevron).
   Hide the text labels — tools become icon-only — which keeps the
   chevron unobscured. Scoped to `.wf-bar.is-tight .wf-bar__mode--tools`
   so the floating palette (which lives outside the bar in the DOM
   when pinned and uses the same .wf-tools__content elements) is
   unaffected. */
.wf-bar.is-tight .wf-bar__mode--tools .wf-tool__tx{display:none}
.wf-bar.is-tight .wf-bar__mode--tools .wf-tool{padding:0 8px}

/* canvas-tools buttons — labelled pills (these MOVE into the
   portalled palette, so they carry concrete fallbacks too) */
.wf-tool{
  display:flex;align-items:center;gap:8px;height:32px;padding:0 12px;
  border:0;border-radius:9px;background:transparent;
  color:var(--label,#41584C);
  font:inherit;font-size:13px;cursor:pointer;white-space:nowrap;
  transition:background-color .12s,color .12s}
.wf-tool:hover{background:var(--row-hover,rgba(56,120,90,.10));
  color:var(--label-hover,#27543C)}
.wf-tool:focus-visible{outline:0;
  box-shadow:0 0 0 2px var(--focus,rgba(47,138,95,.45))}
.wf-tool__ic{display:flex;width:18px;height:18px;flex:none}
.wf-tool__ic svg{width:18px;height:18px;display:block}
.wf-tool--back{color:var(--accent,#2F8A5F)}
.wf-tool--back:hover{background:var(--header-hover,rgba(56,120,90,.09));
  color:var(--cta,#1C6240)}
.wf-tool--pin{color:var(--label,#41584C)}
.wf-tool--pin:hover{background:var(--header-hover,rgba(56,120,90,.09));
  color:var(--cta,#1C6240)}
/* icon-only tool (Pin lost its label): a compact square button so
   it doesn't reserve the old text width */
.wf-tool--icon{width:34px;height:34px;padding:0;justify-content:center;
  border-radius:8px}
/* "More" overflow — a quieter, secondary affordance */
.wf-tool--more{color:var(--muted,#7C9384)}
.wf-tool--more:hover{color:var(--label-hover,#27543C)}

/* ============================================================
   FLOATING PALETTE — a draggable, width-resizable TOOLBAR.
   Portalled to <body>, so every value carries a concrete
   fallback and buttons are reset at zero specificity (host CSS
   can never flatten it). It is a single HORIZONTAL row:
       [grip] [ tool · tool · tool · tool ] | [pin reset «]
   At its natural width it is ONE line, the same height as the
   header. Narrowing it via the corner grip shrinks the tools
   area so tool buttons wrap to the next line ONE BY ONE until
   it becomes a list; widen it back and the moment they all fit
   one line again it returns to this default. No row/list
   "mode" — just a continuous width.
   Collapse/expand uses the SAME shrink-to-tile motion as the
   header (same easing+duration), driving width+height.
   ============================================================ */
.wf-toolbar,.wf-toolbar *{box-sizing:border-box}
/* ---- OUTER: the clip + chrome box. It owns the border, blur,
   shadow and the SIZE animation. It is NOT a flex container —
   it just clips and reveals the inner content layer, exactly
   like the header's .wf-bar reveals .wf-bar__cwrap. ---- */
.wf-toolbar{
  position:fixed;left:0;top:0;z-index:42;display:none;
  width:var(--tw,-webkit-max-content);
  width:var(--tw,-moz-max-content);
  width:var(--tw,max-content);          /* sizes to the inner */
  max-width:none;height:auto;padding:0;margin:0;overflow:visible;
  border:1px solid var(--border,rgba(70,92,80,.20));
  border-radius:var(--bar-radius,14px);
  background-color:var(--surface,rgba(252,253,250,.94));
  color:var(--text,#41584C);
  font-family:inherit;font-size:13px;line-height:1.4;
  -webkit-backdrop-filter:var(--blur,saturate(1.7) blur(16px));
  backdrop-filter:var(--blur,saturate(1.7) blur(16px));
  box-shadow:var(--bar-shadow,0 10px 30px rgba(20,50,38,.16));
  user-select:none;
  /* the SAME single .34s curve for every animatable property, so
     size + position + radius move as one synced motion (this is
     what makes it feel like the header) */
  transition:background-color var(--aw-chrome-dur) var(--aw-chrome-ease),
             box-shadow var(--aw-chrome-dur) var(--aw-chrome-ease),
             border-color var(--aw-chrome-dur) var(--aw-chrome-ease),
             width var(--aw-chrome-dur) var(--aw-chrome-ease),
             height var(--aw-chrome-dur) var(--aw-chrome-ease),
             border-radius var(--aw-chrome-dur) var(--aw-chrome-ease),
             left var(--aw-chrome-dur) var(--aw-chrome-ease),
             top var(--aw-chrome-dur) var(--aw-chrome-ease)}
.wf-toolbar.is-on{display:block}
.wf-toolbar.wf-noanim{transition:none}
/* clip only while the box is actively resizing to/from the tile,
   so the (fixed-layout) inner is revealed by the moving edges
   instead of overflowing; the corner grip is visible at rest */
.wf-toolbar.is-morphing{overflow:hidden}
.wf-toolbar.is-dragging{transition:none;cursor:grabbing;
  /* will-change hints to the browser that left/top will change
     during drag — encourages compositor-layer promotion so the
     bar's expensive paint (box-shadow + backdrop-filter blur) is
     rasterised once and the drag becomes a matrix update on the
     GPU. Without this, every drag pointer-move re-rasterises the
     full bar including shadow and blur — the dominant cost
     behind the "CTM freezes during fast drag" symptom. */
  will-change:left,top;
  box-shadow:0 16px 40px rgba(20,50,38,.30)}
.wf-toolbar.is-resizing{transition:none}
/* zero-specificity reset: beats the UA sheet, never our rules */
:where(.wf-toolbar button){
  margin:0;padding:0;font:inherit;color:inherit;background:transparent;
  border:0;-webkit-appearance:none;appearance:none;line-height:1}

/* ---- INNER: the content layer. Its width is the EXPANDED width
   AT ALL TIMES (var(--tw) or max-content) and is NEVER touched by
   the collapse/expand animation — so the flex layout can never
   re-wrap while the outer box resizes. That removes every "first
   height then width / clunky" artefact: the content is laid out
   once and simply revealed. ---- */
.wf-toolbar__inner{
  width:var(--tw,-webkit-max-content);
  width:var(--tw,-moz-max-content);
  width:var(--tw,max-content);
  display:flex;flex-flow:row wrap;align-items:center;
  align-content:flex-start;gap:3px 4px;padding:5px 8px}

/* boxed items fade (opacity) CONCURRENTLY with the box resize —
   driven by .is-collapsed which toggles at the start of the
   animation, exactly like the header's .wf-bar__cwrap */
.wf-toolbar__grip,.wf-toolbar .wf-tool,.wf-toolbar__ic{
  transition:background-color .12s,color .12s,
             opacity .18s var(--ease,cubic-bezier(.4,0,.2,1))}

/* ---- drag handle: a small grip at the LEADING edge ---- */
.wf-toolbar__grip{
  flex:0 0 auto;width:26px;height:34px;display:flex;
  align-items:center;justify-content:center;color:var(--muted,#9aa8a0);
  border-radius:8px;cursor:grab;touch-action:none}
.wf-toolbar__grip:hover{background:var(--row-hover,rgba(56,120,90,.08));
  color:var(--label,#41584C)}
.wf-toolbar__grip:active{cursor:grabbing}
.wf-toolbar__grip svg{width:18px;height:16px;display:block;opacity:.85}

/* slot + content are display:contents → the four tool buttons are
   REAL flex items of the inner, so they wrap one-by-one and can
   never overflow onto the controls */
.wf-toolbar__slot{display:contents}
.wf-tools__content{display:contents}
.wf-toolbar .wf-tool{flex:0 0 auto}

/* sep: inert on a single line; a full-width flex break ONLY in
   STACKED mode (height-resized) so the tools drop below a top
   band of grip + controls. Width-only resize stays one line. */
.wf-toolbar__sep{display:none}
.wf-toolbar.is-stacked .wf-toolbar__sep{
  display:block;order:-1;flex:1 0 100%;width:100%;height:0;
  margin:0;padding:0;background:transparent}

/* controls: single line → trailing edge (far right, like the
   header). STACKED → up next to the grip (top band). */
.wf-toolbar__ctrl{
  flex:0 0 auto;margin-left:auto;
  display:flex;flex-flow:row nowrap;align-items:center;gap:1px}
.wf-toolbar.is-stacked .wf-toolbar__grip{order:-3}
.wf-toolbar.is-stacked .wf-toolbar__ctrl{order:-2;margin-left:6px}
.wf-toolbar__ic{
  flex:0 0 auto;width:34px;height:34px;display:flex;
  align-items:center;justify-content:center;
  color:var(--icon,#4A6354);border-radius:8px;cursor:pointer}
.wf-toolbar__ic:hover{background:var(--row-hover,rgba(56,120,90,.10));
  color:var(--icon-hover,#27543C)}
.wf-toolbar__ic:focus-visible{outline:0;
  box-shadow:0 0 0 2px var(--focus,rgba(47,138,95,.45))}
.wf-toolbar__ic svg{width:18px;height:18px;display:block}
.wf-toolbar__ic.is-on{color:var(--accent,#2F8A5F)}
.wf-toolbar__restore{display:none}
.wf-toolbar.is-resized .wf-toolbar__restore{display:flex}

/* ---- COLLAPSED: a clean 40x40 tile (logo-sized). The outer
   clips to 40 (JS animates the inline size in/out); the inner
   keeps its full layout (clipped) so nothing re-wraps; the
   content fades and only the centred glyph remains. ---- */
.wf-toolbar.is-collapsed{
  width:40px;height:40px;
  padding:0;border-radius:12px;overflow:hidden}
.wf-toolbar.is-collapsed .wf-toolbar__grip,
.wf-toolbar.is-collapsed .wf-tool,
.wf-toolbar.is-collapsed .wf-toolbar__pin,
.wf-toolbar.is-collapsed .wf-toolbar__restore,
.wf-toolbar.is-collapsed .wf-toolbar__reset{
  opacity:0;visibility:hidden;pointer-events:none}
/* glyph is taken OUT OF FLOW and pinned to the centre of the
   OUTER tile (inner stays visible so the glyph shows), so the
   hidden siblings can never push it out — always seen & clickable */
.wf-toolbar.is-collapsed .wf-toolbar__collapse{
  position:absolute;left:0;top:0;right:0;bottom:0;
  width:auto;height:auto;margin:0;z-index:3;
  display:flex;align-items:center;justify-content:center;
  border-radius:11px;background:transparent;   /* ROUND 30: the inner radius - 12 drew a ring inside the tile's border on hover */
  color:var(--label,#41584C);pointer-events:auto;
  opacity:1;visibility:visible}
/* OWNER REPORT (2026-08-04): "the collapsed canvas-tools chip hovers unlike
   every other collapsed chip: the others change background fill and gain a
   white border, this one only recolours its glyph."

   Measured, all-collapsed row at 1920x1080 with the palette pinned, full
   computed-style diff rest -> hover of every chip AND its inner control, on
   both builds (r3/chrome-t4-hover.json, `chips.head.diffs` vs
   `chips.fix.diffs`):

     chip                     inner control          hover background (head)
     top bar collapsed        .wf-hdrtoggle          rgba(0,0,0,0) -> rgba(56,120,90,.09)
     nav rail                 .wf-nav__collapse      rgba(0,0,0,0) -> rgba(56,120,90,.09)
     zoom cluster collapsed   #btn-zoom-toggle       rgba(0,0,0,0) -> rgba(56,120,90,.09)
     help tile                .toolbar-help .btn     rgba(0,0,0,0) -> rgba(56,120,90,.09)
     canvas tools collapsed   .wf-toolbar__collapse  NO CHANGE  <-- the report

   On head this chip's whole hover diff was `border-color` and `color`
   rgb(65,88,76) -> rgb(31,110,69); with the rule below it is
   background-color rgba(0,0,0,0) -> rgba(56,120,90,.09) plus the same two,
   i.e. byte-identical to the top bar, nav rail and zoom cluster.

   All four of the others carry the fill on `background-color, color .12s
   ease`, which this rule's element already declares (measured
   `background-color, color, opacity | 0.12s, 0.12s, 0.18s | ease, ease,
   cubic-bezier(.32,.72,0,1)`), so only the VALUE was missing — no transition
   was added. The glyph colour was already right: #1F6E45 = rgb(31,110,69).

   The "white border" half of the report is the chip's RESTING chrome, not a
   hover state: no chip gains a border on hover — every outer diff in that
   dump is `{}` , on head and on fix. The canvas-tools chip already matches
   the others exactly at rest — measured background rgba(255,255,255,.56),
   border 1px rgba(255,255,255,.7), radius 12px, identical to the top bar,
   nav rail, zoom cluster and help tile in the same dump. Only the fill
   above was out of line. */
.wf-toolbar.is-collapsed .wf-toolbar__collapse:hover{
  background:rgba(56,120,90,.09);color:var(--cta,#1F6E45)}
/* the collapsed tile shows ICONS.tools (rect+circle) — swapped
   in by syncFCollapse — so we DON'T rotate it like the bar's
   chevron does. The expanded state shows ICONS.collapse with
   its natural << orientation. */

/* resize grip — sidebar .wf-grip pattern: a button JUST outside
   the bottom-right corner; icon hidden until that corner is
   hovered or while resizing, so it never shifts the layout. */
.wf-toolbar__resize{
  position:absolute;right:-22px;bottom:-22px;width:30px;height:30px;
  cursor:nwse-resize;z-index:7;touch-action:none;
  color:var(--muted,#7C9384);
  display:flex;align-items:center;justify-content:center}
.wf-toolbar__rico{
  display:flex;align-items:center;justify-content:center;
  width:15px;height:15px;opacity:0;transform:scale(.55);
  transform-origin:center;
  transition:opacity .18s var(--ease,cubic-bezier(.4,0,.2,1)),
             transform .18s var(--ease,cubic-bezier(.4,0,.2,1))}
.wf-toolbar__rico svg{width:15px;height:15px;display:block}
.wf-toolbar__resize:hover .wf-toolbar__rico,
.wf-toolbar.is-resizing .wf-toolbar__rico{opacity:.85;transform:none}
.wf-toolbar.is-resizing .wf-toolbar__resize{color:var(--accent,#2F8A5F)}
.wf-toolbar.is-collapsed .wf-toolbar__resize{display:none}

/* ============================================================
   TITLE — "Project / workflow name" breadcrumb (no status dot)
   ============================================================ */
.wf-title{
  display:flex;flex-direction:row;flex-wrap:nowrap;
  align-items:center;gap:7px;flex:none;        /* no flex-shrink: see
     note below. */
  max-width:420px;padding-left:8px;min-width:0;
  /* The title used to be flex:0 1 auto (shrinkable). That made it
     the ONLY element in the header that responded to the bar's
     width changing — and because .wf-title__text has
     text-overflow:ellipsis, every frame of the .34s collapse /
     expand re-truncated the workflow name and re-rasterised the
     text. The user perceived this as a localised hitch "starting
     from the title". flex:none pins the title's layout; the bar's
     own overflow:hidden does all the clipping naturally, so the
     content is revealed by ONE smooth motion instead of being
     re-laid-out every frame. max-width still caps very long names
     (one-off static truncation, not per-frame). */}
.wf-crumb{
  display:flex;flex-direction:row;flex-wrap:nowrap;
  align-items:center;gap:7px;flex:none;
  font-size:14.5px;font-weight:400;color:var(--label);
  letter-spacing:-.005em;white-space:nowrap}
.wf-crumb__root{color:var(--label);cursor:default}
.wf-crumb__sep{color:var(--muted);opacity:.6;font-size:14px}
/* Slice 4F-FP1: when the active workflow has no resolvable project
   name (project_id is null, the project was deleted, or the lookup
   bailed out), the adapter sets `.is-noproject` on `.wf-crumb` so we
   hide the empty leading segment + the `/` separator. The breadcrumb
   then reads "Workflow Name" instead of " / Workflow Name". */
.wf-crumb.is-noproject .wf-crumb__root,
.wf-crumb.is-noproject .wf-crumb__sep{display:none}
@keyframes wf-spin{to{transform:rotate(360deg)}}
/* the workflow name is CONTEXT, not a heading — light weight,
   quiet, aligned with the surface language; never bold */
.wf-title__text{
  font-family:inherit;font-size:14.5px;font-weight:400;
  color:var(--text);letter-spacing:-.005em;
  border:0;background:transparent;cursor:pointer;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  min-width:0;max-width:100%;padding:4px 8px;border-radius:7px;
  transition:background-color .12s}
.wf-title__text:hover{background:var(--row-hover);color:var(--label-hover)}
.wf-title__text:focus-visible{outline:0;box-shadow:0 0 0 2px var(--focus)}
/* inline editor — same glass language as .wf-input / .wf-search,
   NOT a heavy accent ring */
.wf-title__input{
  font-family:inherit;font-size:14.5px;font-weight:400;
  color:var(--text);
  border:1px solid var(--border);border-radius:var(--radius);
  background:var(--surface);
  height:30px;min-width:170px;max-width:340px;padding:0 9px;
  outline:0;
  transition:border-color .14s ease,background-color .14s ease}
.wf-title__input:focus{
  border-color:var(--search-bd-focus);background:var(--search-bg-focus)}

/* ============================================================
   RUN CLUSTER  (input · compact Run · appearance)
   ============================================================ */
.wf-run{display:flex;align-items:center;gap:8px;flex:none}

.wf-input{
  display:flex;align-items:center;gap:8px;height:34px;
  width:208px;padding:0 6px 0 12px;
  border:1px solid var(--border);border-radius:var(--radius);
  background:transparent;color:var(--muted);
  transition:border-color .14s ease,background-color .14s ease}
.wf-input:focus-within{
  border-color:var(--search-bd-focus);background:var(--search-bg-focus)}
.wf-input svg{flex:none;width:15px;height:15px}
.wf-input input{
  flex:1;min-width:0;border:0;outline:0;background:transparent;
  font:inherit;font-size:13px;color:var(--text)}
.wf-input input::placeholder{color:var(--muted)}
.wf-input__clear{
  display:none;align-items:center;justify-content:center;
  width:22px;height:22px;flex:none;border:0;background:transparent;
  color:var(--muted);border-radius:6px;cursor:pointer;line-height:0;
  transition:background .12s,color .12s}
.wf-input__clear:hover{color:var(--label-hover);background:var(--row-hover)}
.wf-input__clear svg{width:13px;height:13px}
.wf-input.has-text .wf-input__clear{display:flex}
/* Simple action view → the initial-input is an engineering
   affordance, so it is hidden there */
.wf-bar.is-simple .wf-input{display:none}

/* compact primary Run button — tight, fixed width; never
   reflows on hover (no expanding keyboard hint) */
/* primary action, Unified: a soft accent-tinted button in the
   same surface language as the nav (no solid block / glow) */
.wf-cta-run{
  display:flex;align-items:center;gap:7px;height:32px;
  padding:0 13px;border:0;border-radius:var(--radius);
  background:var(--header-hover);color:var(--cta);cursor:pointer;
  font:inherit;font-size:12.5px;font-weight:600;letter-spacing:.01em;
  white-space:nowrap;
  box-shadow:inset 0 0 0 1px var(--accent-line);
  transition:background-color .14s ease,box-shadow .14s ease,
             color .14s ease,transform .08s ease}
.wf-cta-run:hover{background:rgba(56,120,90,.16)}
.wf-cta-run:active{transform:translateY(1px)}
.wf-cta-run:focus-visible{outline:0;
  box-shadow:0 0 0 2px var(--surface),0 0 0 3px var(--focus)}
.wf-cta-run__ic{display:flex;align-items:center;justify-content:center;
  width:14px;height:14px;flex:none}
.wf-cta-run__ic svg{width:14px;height:14px;display:block}
.wf-cta-run__tx{flex:none}
/* Last-resort compact mode (driven by fitHeader at phone-tiny widths):
   keep Run essential and visible, but as an icon-only chip so the
   chevron has room. */
.wf-cta-run--compact{padding:0 9px;gap:0}
.wf-cta-run--compact .wf-cta-run__tx{display:none}
.wf-cta-run.is-running{background:var(--header-hover);color:var(--cta)}
.wf-cta-run.is-running .wf-cta-run__ic svg{animation:wf-spin .9s linear infinite}
/* running doubles as Stop — a soft, unmistakable warning tint */
.wf-cta-run.is-running:hover{
  background:rgba(192,73,47,.13);color:#B5442D;
  box-shadow:inset 0 0 0 1px rgba(192,73,47,.34)}
.wf-cta-run.is-done{animation:wf-cta-pulse .5s var(--ease)}
@keyframes wf-cta-pulse{
  0%{box-shadow:inset 0 0 0 1px var(--accent-line),0 0 0 0 rgba(92,154,126,.40)}
  100%{box-shadow:inset 0 0 0 1px var(--accent-line),0 0 0 10px rgba(92,154,126,0)}
}
.wf-cta-run.is-disabled{
  background:rgba(120,140,128,.16);color:rgba(60,82,71,.45);
  box-shadow:inset 0 0 0 1px rgba(120,140,128,.22);
  cursor:not-allowed;pointer-events:none}

/* bar icon button. Deliberately a bar-scoped class (NOT the
   sidebar's global .wf-iconbtn): both stylesheets load together
   in the app, and a shared .wf-iconbtn would let the header's
   34x34 silently override the sidebar's 30x30. Same visual
   language, isolated namespace. */
.wf-bar__ic{
  display:flex;align-items:center;justify-content:center;
  position:relative;
  width:34px;height:34px;border:0;background:transparent;
  color:var(--muted);border-radius:8px;cursor:pointer;flex:none;
  transition:background .12s,color .12s}
.wf-bar__ic:hover{background:var(--row-hover);color:var(--label-hover)}
.wf-bar__ic:focus-visible{outline:0;box-shadow:0 0 0 2px var(--focus)}
.wf-bar__ic svg{width:18px;height:18px;display:block}
.wf-bar__ic.is-open{color:var(--accent);background:var(--row-hover)}
.wf-bar__ic-dot{
  position:absolute;top:5px;right:5px;width:6px;height:6px;
  border-radius:50%;background:var(--accent);
  opacity:0;transform:scale(.3);
  transition:opacity .16s,transform .16s var(--ease)}
.wf-bar__ic.has-dot .wf-bar__ic-dot{opacity:1;transform:scale(1)}

/* ============================================================
   ACCOUNT — single avatar button (identity + act-as merged)
   ============================================================ */
/* Anchored to the bar's right edge, NOT a flex item.
   Previously this lived at the end of defMode (bar-rel x≈449-483),
   while the chevron sat at right:0 of bar. As bar.width animated
   40→529, the chevron's left edge swept 0→489 and HAD to pass
   straight through 449-483 — the acct's flex position. For ~70ms
   the chevron (z-index 3) slid on top of the acct face while the
   acct was fading in via cwrap.opacity, producing exactly the
   "two animations locked into one action" twitch.

   Solution: anchor BOTH right-side buttons to bar.right with a
   fixed offset between them. acct at right:46 (so its right edge
   sits 6px left of the chevron's left edge); chevron at right:0.
   They now slide together as a single unit as bar.width animates,
   never crossing each other. The acct still fades in via cwrap's
   opacity transition (it's still a descendant of cwrap in the
   DOM), so its reveal harmonizes with the title/run fade on the
   left side of the bar — the whole header opens as one motion. */
.wf-acct{
  position:absolute;right:46px;top:50%;
  display:flex;align-items:center;justify-content:center;
  width:34px;height:34px;border:0;cursor:pointer;
  border-radius:50%;padding:0;background:transparent;
  transform:translateY(-50%);
  transition:transform .12s,background-color .12s}
.wf-acct:active{transform:translateY(calc(-50% + .5px))}
.wf-acct:focus-visible{outline:0}
.wf-acct__face{
  display:flex;align-items:center;justify-content:center;
  width:30px;height:30px;border-radius:50%;
  background:var(--active-bg);
  border:1px solid var(--border);
  color:var(--label);font-size:12px;font-weight:600;letter-spacing:.02em;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.45);
  transition:background-color .14s var(--ease),border-color .14s,
             color .14s,box-shadow .14s}
.wf-acct:hover .wf-acct__face{
  background:var(--surface);color:var(--label-hover);
  border-color:var(--accent-line)}
.wf-acct:focus-visible .wf-acct__face{
  box-shadow:0 0 0 2px var(--surface),0 0 0 4px var(--focus)}
/* open state: subtle + consistent with .wf-bar__ic.is-open —
   a faint round backdrop and a gentle accent-tinted face.
   No focus ring, no focus colour (that is for :focus-visible). */
.wf-acct.is-open{background:var(--row-hover)}
.wf-acct.is-open .wf-acct__face{
  background:var(--surface);color:var(--accent);
  border-color:var(--accent-line)}
/* acting-as: an accent ring + a small swap badge so the altered
   context is glanceable straight from the avatar */
.wf-acct__ring{
  position:absolute;inset:0;border-radius:50%;
  border:2px solid var(--accent);
  opacity:0;transform:scale(.82);
  transition:opacity .18s var(--ease),transform .18s var(--ease)}
.wf-bar.is-actingas .wf-acct__ring{opacity:1;transform:scale(1)}
.wf-acct__swap{
  position:absolute;right:-2px;bottom:-2px;
  width:15px;height:15px;border-radius:50%;
  display:flex;align-items:center;justify-content:center;
  background:var(--accent);color:#fff;
  border:2px solid var(--surface-solid);
  opacity:0;transform:scale(.4);
  transition:opacity .18s var(--ease),transform .18s var(--ease)}
.wf-acct__swap svg{width:9px;height:9px;display:block}
.wf-bar.is-actingas .wf-acct__swap{opacity:1;transform:scale(1)}

/* ============================================================
   POPOVER (shared) — surface/shadow/fade language of the nav's
   .wf-iconpick / .wf-pop family. Palette comes from the
   .wf-bar,.wf-bpop token block above (popover is portalled to
   <body>, so it MUST carry its own tokens — like the docked
   Limits card does).
   ============================================================ */
.wf-bpop{
  position:fixed;z-index:9000;min-width:200px;
  background:var(--surface-solid);
  border:1px solid var(--border);border-radius:13px;
  box-shadow:0 14px 38px rgba(40,56,48,.20),0 0 0 1px rgba(60,74,67,.05);
  padding:6px;opacity:0;transform:translateY(-4px) scale(.99);
  transform-origin:top right;pointer-events:none;color:var(--text);
  transition:opacity .14s var(--ease),transform .16s var(--ease)}
.wf-bpop.is-night{
  box-shadow:0 16px 42px rgba(0,0,0,.5),0 0 0 1px rgba(255,255,255,.05)}
.wf-bpop.is-open{opacity:1;transform:none;pointer-events:auto}
.wf-bpop__h{
  font-size:10px;letter-spacing:.08em;text-transform:uppercase;
  color:var(--muted);padding:7px 9px 7px}
/* prominent variant used as the popover heading when the bar has
   hidden the title at extreme narrow widths — surfaces the
   "Project / Workflow Name" so the user still has orientation. */
.wf-bpop__title{
  font-size:13px;font-weight:600;color:var(--text);
  padding:9px 10px 8px;
  border-bottom:1px solid var(--divider);
  margin-bottom:4px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  max-width:240px}
.wf-bpop__sep{height:1px;background:var(--divider);margin:5px 4px}
.wf-bpop__row{
  display:flex;align-items:center;gap:10px;width:100%;
  padding:8px 9px;border:0;background:transparent;color:var(--text);
  border-radius:8px;cursor:pointer;font:inherit;font-size:13px;
  text-align:left;transition:background .12s,color .12s}
.wf-bpop__row:hover{background:var(--row-hover);color:var(--label-hover)}
.wf-bpop__row:focus-visible{outline:0;background:var(--row-hover)}
.wf-bpop__ic{display:flex;align-items:center;justify-content:center;
  width:17px;height:17px;flex:none;color:var(--muted)}
.wf-bpop__ic svg{width:16px;height:16px;display:block}
.wf-bpop__tx{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.wf-bpop__tx b{font-weight:500;white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis}
.wf-bpop__tx small{font-size:11px;color:var(--muted);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.wf-bpop__row.is-danger:hover{color:#C0492F;
  background:rgba(192,73,47,.09)}
.wf-bpop__check{display:flex;align-items:center;justify-content:center;
  width:16px;height:16px;flex:none;color:var(--accent);
  opacity:0;transition:opacity .12s}
.wf-bpop__row.is-sel .wf-bpop__check{opacity:1}
.wf-bpop__row.is-sel{color:var(--active-text)}

/* identity header inside the account popover */
.wf-acct-id{
  display:flex;align-items:center;gap:11px;padding:9px 9px 11px}
.wf-acct-id__face{
  display:flex;align-items:center;justify-content:center;
  width:38px;height:38px;flex:none;border-radius:50%;
  background:var(--active-bg);border:1px solid var(--border);
  color:var(--label-hover);font-size:13px;font-weight:600;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.45)}
.wf-acct-id__tx{flex:1;min-width:0;display:flex;
  flex-direction:column;gap:4px}
.wf-acct-id__tx b{font-weight:600;font-size:13px;color:var(--text);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.wf-acct-id__roles{display:flex;gap:5px;flex-wrap:wrap}
.wf-badge{
  display:inline-flex;align-items:center;height:18px;padding:0 8px;
  border-radius:999px;font-size:10.5px;font-weight:600;letter-spacing:.02em;
  background:var(--badge-bg);border:1px solid var(--badge-bd);
  color:var(--badge-fg);white-space:nowrap}
/* "acting as" banner pinned at the top of the menu while
   impersonating, with an inline revert */
.wf-acct-banner{
  display:flex;align-items:center;gap:9px;
  margin:0 2px 6px;padding:8px 10px;border-radius:9px;
  background:linear-gradient(var(--header-hover),var(--header-hover)),
             var(--surface);
  border:1px solid var(--accent-line)}
.wf-acct-banner__ic{display:flex;align-items:center;justify-content:center;
  width:16px;height:16px;flex:none;color:var(--accent)}
.wf-acct-banner__ic svg{width:15px;height:15px;display:block}
.wf-acct-banner__tx{flex:1;min-width:0;font-size:11.5px;
  color:var(--active-text);line-height:1.3}
.wf-acct-banner__tx b{font-weight:600;display:block;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.wf-acct-banner__x{
  border:0;background:transparent;color:var(--accent);cursor:pointer;
  font:inherit;font-size:11px;font-weight:600;padding:4px 6px;
  border-radius:6px;flex:none;white-space:nowrap;
  transition:background .12s}
.wf-acct-banner__x:hover{background:var(--row-hover)}

/* search field inside a popover (reuses the .wf-search look) */
.wf-bpop__search{
  display:flex;align-items:center;gap:8px;height:32px;
  margin:2px 2px 6px;padding:0 10px;
  border:1px solid var(--border);border-radius:var(--radius);
  background:transparent;color:var(--muted);
  transition:border-color .14s,background-color .14s}
.wf-bpop__search:focus-within{
  border-color:var(--search-bd-focus);background:var(--search-bg-focus)}
.wf-bpop__search svg{flex:none;width:14px;height:14px}
.wf-bpop__search input{flex:1;min-width:0;border:0;outline:0;
  background:transparent;font:inherit;font-size:13px;color:var(--text)}
.wf-bpop__search input::placeholder{color:var(--muted)}
.wf-bpop__list{max-height:240px;overflow-y:auto;overflow-x:hidden;
  scrollbar-width:thin}
.wf-bpop__list::-webkit-scrollbar{width:8px}
.wf-bpop__list::-webkit-scrollbar-thumb{
  background:rgba(90,112,100,.30);border-radius:8px;
  border:2px solid transparent;background-clip:content-box}
.wf-bpop__empty{padding:16px 10px;text-align:center;
  color:var(--muted);font-size:12.5px}
.wf-bpop__me{
  font-size:9.5px;font-weight:600;letter-spacing:.05em;
  text-transform:uppercase;color:var(--accent);
  border:1px solid var(--accent-line);border-radius:999px;
  padding:1px 7px;flex:none}

/* config rows (appearance popover): toggle rows + segmented */
.wf-cfg__row{
  display:flex;align-items:center;gap:10px;width:100%;
  padding:8px 9px;border:0;background:transparent;color:var(--text);
  border-radius:8px;cursor:pointer;font:inherit;font-size:13px;
  text-align:left;transition:background .12s}
.wf-cfg__row:hover{background:var(--row-hover)}
.wf-cfg__txt{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.wf-cfg__txt b{font-weight:500}
.wf-cfg__txt small{font-size:11px;color:var(--muted)}
.wf-switch{
  width:34px;height:20px;flex:none;border-radius:999px;
  background:rgba(90,112,100,.30);position:relative;
  transition:background .16s var(--ease)}
.wf-switch::after{
  content:"";position:absolute;top:2px;left:2px;width:16px;height:16px;
  border-radius:50%;background:#fff;
  box-shadow:0 1px 3px rgba(40,56,48,.30);
  transition:transform .18s var(--ease)}
.wf-cfg__row.is-on .wf-switch{background:var(--accent)}
.wf-cfg__row.is-on .wf-switch::after{transform:translateX(14px)}
.wf-seg{display:flex;gap:3px;padding:5px 9px 7px}
.wf-seg__b{
  flex:1;padding:7px 8px;border:1px solid var(--border);
  background:transparent;color:var(--muted);border-radius:7px;
  cursor:pointer;font:inherit;font-size:12px;font-weight:500;
  transition:background .12s,color .12s,border-color .12s}
.wf-seg__b:hover{color:var(--label-hover);background:var(--row-hover)}
.wf-seg__b.is-sel{
  color:var(--active-text);border-color:var(--accent-line);
  background:linear-gradient(var(--header-hover),var(--header-hover)),var(--surface)}

/* ============================================================
   TOOLTIP — identical mechanics to the nav's .wf-tip, below
   the element. (Intentionally a shared class with the sidebar:
   identical contract, each component owns its own node, only
   one ever visible — no conflict.)
   ============================================================ */

/* ===== FIGURE / DRAW flyout — a small draggable menu, portalled
        to <body>. Deliberately built to look like a PIECE OF THE
        CANVAS-TOOLS PALETTE: the exact same frosted glass
        (--surface / --blur / --bar-radius / --bar-shadow), the
        same drag grip (.wf-toolbar__grip), the same icon controls
        (.wf-tool--icon / --pin) and the same tool rows (.wf-tool).
        Drag by the grip, pin to remember its spot, ✕ to close. ===== */
.wf-fly{
  position:fixed;left:0;top:0;z-index:43;display:none;
  width:-webkit-max-content;width:-moz-max-content;width:max-content;
  min-width:210px;max-width:300px;padding:5px;
  border:1px solid var(--border,rgba(70,92,80,.20));
  border-radius:var(--bar-radius,16px);
  background-color:var(--surface,rgba(252,253,250,.94));
  -webkit-backdrop-filter:var(--blur,saturate(1.7) blur(16px));
          backdrop-filter:var(--blur,saturate(1.7) blur(16px));
  box-shadow:var(--bar-shadow,0 10px 30px rgba(20,50,38,.16));
  color:var(--text,#1F2D26);font-family:inherit;font-size:13px;line-height:1.4;
  opacity:0;transform:translateY(-5px) scale(.985);transform-origin:top left;
  transition:opacity .17s var(--ease,cubic-bezier(.4,0,.2,1)),
             transform .17s var(--ease,cubic-bezier(.4,0,.2,1));
}
.wf-fly.is-on{display:block}
.wf-fly.is-open{opacity:1;transform:none}
.wf-fly.is-dragging,.wf-fly.wf-noanim{transition:none}

/* leading bar: grip + title + pin + close — same row rhythm and
   the same control treatment as the palette's own bar */
.wf-fly__bar{display:flex;align-items:center;gap:2px;padding:0 0 4px;
  margin-bottom:3px}
.wf-fly__grip{                                  /* == .wf-toolbar__grip */
  flex:0 0 auto;width:26px;height:34px;display:flex;align-items:center;
  justify-content:center;color:var(--muted,#9aa8a0);border-radius:8px;
  cursor:grab;touch-action:none;background:transparent;border:0;padding:0;
  transition:background-color .12s,color .12s}
.wf-fly__grip:hover{background:var(--row-hover,rgba(56,120,90,.08));
  color:var(--label,#41584C)}
.wf-fly.is-dragging .wf-fly__grip{cursor:grabbing}
.wf-fly__grip svg{width:18px;height:16px;display:block;opacity:.85;
  pointer-events:none}
.wf-fly__title{flex:1 1 auto;font-size:13px;font-weight:600;
  color:var(--label,#41584C);white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis;padding:0 4px}
.wf-fly__ic{                                    /* == .wf-tool--icon/--pin */
  flex:0 0 auto;width:34px;height:34px;display:flex;align-items:center;
  justify-content:center;border-radius:8px;cursor:pointer;
  color:var(--label,#41584C);background:transparent;border:0;padding:0;
  transition:background-color .12s,color .12s}
.wf-fly__ic:hover{background:var(--header-hover,rgba(56,120,90,.09));
  color:var(--cta,#1C6240)}
.wf-fly__ic:focus-visible{outline:0;
  box-shadow:0 0 0 2px var(--focus,rgba(47,138,95,.45))}
.wf-fly__ic svg{width:17px;height:17px;display:block;pointer-events:none}
.wf-fly__pin.is-active{color:var(--active-text,#235446);
  background:var(--active-bg,rgba(255,255,255,.40))}

/* item rows: identical to a canvas tool button (.wf-tool) */
.wf-fly__list{display:flex;flex-direction:column;gap:2px}
.wf-fly__row{
  display:flex;align-items:center;gap:8px;height:32px;padding:0 12px;
  border:0;border-radius:9px;background:transparent;
  color:var(--label,#41584C);font:inherit;font-size:13px;cursor:pointer;
  white-space:nowrap;text-align:left;
  transition:background-color .12s,color .12s}
.wf-fly__row:hover{background:var(--row-hover,rgba(56,120,90,.10));
  color:var(--label-hover,#27543C)}
.wf-fly__row:focus-visible{outline:0;
  box-shadow:0 0 0 2px var(--focus,rgba(47,138,95,.45))}
.wf-fly__ric{display:flex;align-items:center;justify-content:center;
  width:18px;height:18px;flex:none}
.wf-fly__ric svg{width:18px;height:18px;display:block}
.wf-fly__row>span:last-child{flex:1;min-width:0;overflow:hidden;
  text-overflow:ellipsis}

.wf-tip{position:fixed;z-index:9999;display:none;pointer-events:none;
  font-family:inherit;font-size:12px;line-height:1.35;padding:7px 10px;
  border-radius:7px;max-width:240px;box-shadow:0 6px 22px rgba(0,0,0,.2);
  opacity:0;transition:opacity .12s}

/* ============================================================
   RESPONSIVE
   ============================================================ */
/* The is-narrow input-collapse used to live here: the input
   shrank to a 34px icon and re-expanded on click. That meant
   clicking the input grew the bar's max-content by ~200px and
   shifted Run/acct/chevron rightward every time — because the
   trigger (bar width < 1040) fires on the bar's intrinsic width,
   which is always under 1040, so the collapse was permanent and
   every click triggered the expansion. The input now stays at
   its full 208px in every state; no layout ripple on focus. */
/* Element HIDING is now measurement-driven (see fitHeader in JS).
   These CSS rules keep the responsive classes but only shape CONTENT
   (title ellipsis, input shrink, run flex behaviour) — never
   display:none, which can leave gaps between thresholds. */
.wf-bar.is-tight .wf-title{max-width:240px}
.wf-bar.is-compact .wf-title{max-width:140px}
/* Below 1040 the bar's actions row (.wf-run) must shrink to fit
   alongside the title — at default it's flex:none and overflows
   into the absolute-positioned acct slot. Letting it shrink AND
   making the input shrinkable inside it keeps everything visible:
   the input contracts down to ~120px (placeholder ellipsises) but
   the Run button keeps its full width so the primary action stays
   readable. */
.wf-bar.is-narrow .wf-run{flex:0 1 auto;min-width:0}
.wf-bar.is-narrow .wf-input{flex:1 1 208px;width:auto;min-width:120px}
.wf-bar.is-narrow .wf-cta-run{flex:none}
/* The very tight band — input goes to its irreducible minimum so
   the title still has breathing room. */
.wf-bar.is-compact .wf-input{min-width:60px}
/* Phone-tiny band (vw<480): every spare pixel matters. The bar
   gap drops from 18→8, input min from 60→40, title from 90→70 so
   the post-fitHeader content (title + gap + input + compact Run)
   actually fits the inner content area without colliding with the
   chevron. Without these tighter floors, fitHeader runs out of
   tiers and the last optional (compact Run) still overlaps the
   chevron by ~5–15px at vw=320–350. */
.wf-bar.is-mini .wf-bar__gap{width:8px;min-width:8px}
.wf-bar.is-mini .wf-input{min-width:40px}
.wf-bar.is-mini .wf-title{max-width:70px}
/* the breadcrumb (Project / name) stays visible at all widths;
   the workflow name itself ellipsises when space is short. */

/* ---- global reduced-motion override (parallels the nav) ---- */
.wf-bar.is-reduced,
.wf-bar.is-reduced *,
.wf-bar.is-reduced *::before,
.wf-bar.is-reduced *::after,
.wf-bpop.is-reduced,
.wf-bpop.is-reduced *{
  transition-duration:.001ms !important;
  animation-duration:.001ms !important;
  animation-delay:0s !important}

/* ============================================================
   AirWanderer integration overrides
   ------------------------------------------------------------
   Appended at the END of topbar.css so they win against the
   upstream rules above. Kept in one labelled block so a future
   upstream paste of topbar.css only has to re-apply this tail.
   ============================================================ */

/* Z-INDEX coordination with the AirWanderer modal layer.
   AirWanderer modals are at z-index 900 (backdrop) / 1000 (panel)
   in main.css. Upstream topbar tooltips (z:9999) and popovers
   (z:9000) would float OVER modals; lower them below the modal
   backdrop while keeping them above the rest of the shell
   (sidebar z:6/7, drawer z:6, canvas-toolbar z:5, limits panel
   popout z:60). */
.wf-tip   { z-index: 899 !important; }
.wf-bpop  { z-index: 850 !important; }

/* DEFERRED Run-configuration button.
   The upstream cfgBtn is reserved but currently has no click
   handler. Hide it for this slice rather than ship a visibly
   dead control. fitHeader will still account for its
   presence/absence correctly because the icon is style-hidden
   (display:none), not removed from the DOM. */
.wf-run__cfg { display: none !important; }

/* AirWanderer host-injected secondary icons.
   topbar-adapter.js inserts a Pause icon and a Drawer-toggle
   icon into the .wf-run cluster post-render. They inherit the
   .wf-bar__ic base styling. is-hidden is the host toggle for
   Pause (only visible when State.runStatus === 'running'). */
.wf-aw-pause.is-hidden,
.wf-aw-drawer.is-hidden { display: none !important; }

/* HEADER-LIVES-ON-CANVAS placement.
   The upstream demo `position: sticky; top: var(--bar-gap)` works
   because the demo page makes `<body>` the scroll container; in
   AirWanderer the shell `.app` is height:100vh with no scrolling,
   so sticky would resolve to relative and reserve a layout row
   above the canvas. To preserve the example's design intent (the
   pill floats ABOVE the canvas, not in a separate band beside it),
   override `.wf-bar` to position:fixed and lock its origin to the
   same visual corner the demo lands at (viewport y:14, x:68 — the
   68px logo-rail lane). Upstream `width: max-content` + max-width
   cap apply unchanged.
   This also matches the AirWanderer glass-nav pattern (.wf-nav is
   position:fixed on workflow routes via saas-shell.css). */
.wf-bar {
  position: fixed !important;
  top:  var(--bar-gap, 14px) !important;
  left: calc(2 * var(--bar-gap, 14px) + 40px) !important;
  margin: 0 !important;
}

/* Shared clearance variable for everything that sits below the
   floating topbar pill.
   The pill's VISUAL bottom edge depends on BOTH its height AND
   the transform translateY applied in the collapsed state
   (.wf-bar.is-collapsed sets `height: 40px; transform:
   translateY(4px)` — see topbar.css line ~221):
     • Expanded:  top 14 + transform 0 + height 48 = 62.
     • Collapsed: top 14 + transform 4 + height 40 = 58.
   The bar's visible bottom only moves 4px between states (not
   8px, as I had earlier). To keep the gap below the bar
   constant while the bar morphs, the variable must shift by the
   SAME 4px — otherwise the dependent rows (nav top, drawer
   top, canvas-toolbar top, page-root padding-top) slide twice
   as far as the bar's bottom edge and the two animations
   visibly desync. Set both states 16px below the bar's actual
   visible bottom so the breathing room stays at 16px through
   the whole .34s shrinkTile transition:
     • Expanded clearance  = 62 + 16 = 78.
     • Collapsed clearance = 58 + 16 = 74.
   topbar.js sets `wf-header-collapsed` on <html> when the bar
   collapses (see topbar.js applyHeader + handover §3.2), giving
   us a CSS selector to switch the variable. Every clearance
   rule below uses var(--aw-bar-bottom) so a single variable
   flip retargets all of them together. The 0.34s ease-bar
   transition on the affected properties keeps the layout
   sliding in lockstep with the bar's own collapse motion. */
:root {
  --aw-bar-bottom: 78px;
  /* ROUND 30: the chrome's one clock - every fold / unfold / glide of the bar, the nav, the zoom
     cluster, the help tile, the tools palette, the block bar, the dock tiles and the inspector
     tiles runs on this duration and curve, so a logo click moves them as one */
  --aw-chrome-dur: .32s;
  --aw-chrome-ease: cubic-bezier(.32, .72, 0, 1);
}
/* ROUND 30: the collapsed clearance is set on the four elements that read it, not on <html> - a
   root custom property changing restyled the whole page (38 ms inside the click, measured) */
html.wf-header-collapsed .canvas-toolbar,
html.wf-header-collapsed .app > .wf-nav,
html.wf-header-collapsed .page-root,
html.wf-header-collapsed .app-drawer {
  --aw-bar-bottom: 74px;
}

/* Canvas-toolbar clearance.
   The canvas-toolbar (zoom in/out/fit/help) is `position: absolute`
   inside .canvas-area at top:12 (panels.css). With the topbar
   overlaying, a top:12 toolbar would sit visually underneath the
   bar. Track --aw-bar-bottom so the toolbar slides up/down in
   sync with the bar's collapse/expand morph. */
.canvas-toolbar {
  /* `top` stays at its natural base value; coordinator offsets
     compose as the `translate` property (rule near the end of
     this file). translate is GPU-composited — no layout cost. */
  top: var(--aw-bar-bottom, 76px) !important;
  transition: top var(--aw-chrome-dur) var(--aw-chrome-ease),
              left .42s cubic-bezier(.32, .72, 0, 1);
}

/* Page-root clearance.
   The dashboard / workflows / library / settings / usage / admin
   pages render inside #page-root which sits in the grid's only
   row. Without extra top padding the first row of page content
   would be hidden underneath the bar. */
body.route-page .page-root {
  padding-top: var(--aw-bar-bottom, 76px);
  transition: padding-top var(--aw-chrome-dur) var(--aw-chrome-ease);
}

/* Sidebar nav clearance on page routes.
   The glass nav lives in the grid `nav` cell with align-self:start
   and a 12px top margin (saas-shell.css). With no header row in
   the grid, that puts the nav's top edge at y:12 — directly under
   the topbar's overlay band. Track --aw-bar-bottom. */
body.route-page .app > .wf-nav {
  margin-top: var(--aw-bar-bottom, 76px);
  transition: margin-top var(--aw-chrome-dur) var(--aw-chrome-ease);
}

/* Sidebar nav clearance on workflow routes.
   saas-shell.css positions the nav `position: fixed; top:
   var(--aw-nav-top, 72px)`, where --aw-nav-top is written by the
   sidebar layout bridge (sidebar.js) from the canvas-toolbar's
   live getBoundingClientRect().top. Now that the canvas-toolbar
   top tracks --aw-bar-bottom directly (above), we route the nav
   to the same variable instead of relying on the bridge to
   re-measure during the bar's 0.34s collapse/expand morph —
   otherwise the nav would snap to one terminal value while the
   toolbar slides smoothly between them. The bridge still owns
   --aw-toolbar-left (chip overlap + nav-width tracking) which
   is unaffected. */
body.route-workflow .app > .wf-nav {
  /* `top`/`left` stay at their natural base values; coordinator
     offsets compose as the `translate` property (rules at the
     end of this file). top/left only animate on rare sidebar-
     bridge writes — not on every CTM-drag frame. */
  top:  var(--aw-bar-bottom, 76px) !important;
  left: var(--shell-gap, 12px) !important;
  transition: top  var(--aw-chrome-dur) var(--aw-chrome-ease),
              left .42s cubic-bezier(.32, .72, 0, 1);
}

/* Right drawer clearance.
   The drawer lives in the grid `drawer` cell with an 8px top
   margin (panels.css). On workflow routes that puts its top
   edge under the topbar overlay; on page routes the drawer is
   display:none so the margin is harmless there. Apply
   unconditionally — only affects the workflow route in
   practice. */
.app-drawer {
  margin-top: var(--aw-bar-bottom, 76px);
  transition: margin-top var(--aw-chrome-dur) var(--aw-chrome-ease);
}

/* Narrow-screen (≤ 900px) clearances.
   The legacy small-screen rule in saas-shell.css positions the
   nav at `position: fixed; top: 52px` (matching the now-removed
   52px header card height) and panels.css zeroes the drawer's
   margins for an edge-to-edge layout. Both follow the same
   --aw-bar-bottom variable so the collapse/expand morph is in
   lockstep at narrow widths too. */
@media (max-width: 900px) {
  .app > .wf-nav {
    top:  var(--aw-bar-bottom, 76px) !important;
    transition: top var(--aw-chrome-dur) var(--aw-chrome-ease);
  }
  .app-drawer {
    margin-top: var(--aw-bar-bottom, 76px) !important;
  }
}

/* ============================================================
   Visual consistency with the collapsed top-bar tile.

   The collapsed top-bar pill is a 40×40 floating glass card:
     • glass surface  = rgba(255,255,255,.56)
     • 1px border     = rgba(255,255,255,.70)
     • backdrop-filter = blur(20px) saturate(180%) brightness(1.03)
     • box-shadow     = inset 0 1px 0 rgba(255,255,255,.6),
                        0 10px 30px rgba(55,72,64,.14)
     • border-radius  = 12px
   Three AirWanderer-side shell controls sit in the same visual
   row as the collapsed bar tile but ship with their own (older
   and less polished) chrome: the sidebar collapse pill (the rail
   card when the nav is in rail mode), the canvas zoom group,
   and the canvas help group. Re-skin all three to the same
   glass-card language so the row reads as one coherent control
   strip. The tokens are repeated literally because the bar's
   `--surface` / `--border` / `--blur` / `--bar-shadow` are
   scoped to `.wf-bar, .wf-bpop, .wf-fly, .wf-toolbar` (see the
   top of this file) and don't cascade to AirWanderer shell
   elements.
   ============================================================ */

/* The canvas zoom group (`.toolbar-group` with `−` `Fit` `+`)
   and the canvas help group (`.toolbar-group.toolbar-help` with
   `?`) — both rendered inside `.canvas-toolbar`. Each becomes a
   40px-tall glass pill matching the collapsed bar tile. The
   `.toolbar-help` group ends up at the bar-tile's 40×40 size
   because its single icon button is square. */
.canvas-toolbar .toolbar-group {
  background: rgba(255, 255, 255, 0.56) !important;
  border: 1px solid rgba(255, 255, 255, 0.70) !important;
  border-radius: 12px !important;
  -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(1.03) !important;
  backdrop-filter: blur(20px) saturate(180%) brightness(1.03) !important;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6),
              0 10px 30px rgba(55, 72, 64, 0.14) !important;
  height: 40px;
  padding: 0 4px;
  gap: 2px;
  /* Transitions declared on the BASE rule so the expanding
     path (is-zoom-collapsed removed) animates as smoothly as
     the collapsing path. Without this the gap snaps from 0 to
     2px on uncollapse. */
  transition: padding var(--aw-chrome-dur) var(--aw-chrome-ease),
              gap var(--aw-chrome-dur) var(--aw-chrome-ease);
}

/* Individual buttons inside the canvas toolbar groups need a
   touch larger hit-target (and transparent backgrounds so the
   glass-card surface shows through). The 30×30 hit-target lines
   the buttons up vertically inside the 40px group with 5px of
   breathing room on top and bottom.
   Hover/focus styling exactly mirrors the bar's `.wf-hdrtoggle`
   chevron (topbar.css lines 339-342):
     hover : background `--header-hover` (rgba 56,120,90,.09),
             color `--cta` (#1F6E45)
     focus : outline 0 + box-shadow 0 0 0 2px `--focus`
             (rgba 60,120,92,.55)
   So every clickable surface in the lower row reads as part of
   the same design system as the collapsed bar tile's chevron.
   Tokens are repeated literally (rather than referenced via
   var(--header-hover) etc.) because those variables are scoped
   to .wf-bar/.wf-bpop/.wf-fly/.wf-toolbar at the top of this
   file and don't cascade to AirWanderer shell elements. */
/* Neutralise the .btn base cascade.
   components.css's .btn rules add a white background, a 1-px
   border, a soft drop shadow, a translateY(-1px) hover lift, an
   active-press scale, and an `all 0.25s ease` transition. None of
   those belong on a button that lives inside the glass toolbar-
   group pill (the pill IS the surface; the buttons are flat
   glyphs inside). Reset to: transparent background, no border,
   no shadow, no transform on any state, and the
   .12s-on-color/background transition the bar's `.wf-hdrtoggle`
   uses so hover feels identical to the bar's collapsed-chevron
   hover. */
.canvas-toolbar .toolbar-group .btn {
  background: transparent !important;
  border: 0 !important;
  box-shadow: none !important;
  transform: none !important;
  filter: none !important;
  min-width: 32px;
  height: 32px;
  padding: 0 8px;
  border-radius: 12px;
  color: #41584C;
  text-shadow: none;
  /* Geometry properties (width / height / min-width / padding /
     margin / border-radius) morph at .26s on the shared
     cubic-bezier(.4,0,.2,1) curve. Opacity is intentionally
     SHORTER (.15s, same curve) so the icon strokes have fully
     faded out by the time the container is only ~57% of the way
     through its collapse — the user perceives the icons hiding
     first and the empty tile finishing the shrink afterwards,
     instead of the previous "icons linger while the container
     is already gone" feel. Border-width transitions live on the
     `.zoom-pct` block below (the `.btn` rule has `border: 0
     !important` so its border never changes). */
  transition:
    background-color .12s, color .12s,
    width        var(--aw-chrome-dur) var(--aw-chrome-ease),
    height       var(--aw-chrome-dur) var(--aw-chrome-ease),
    min-width    var(--aw-chrome-dur) var(--aw-chrome-ease),
    padding      var(--aw-chrome-dur) var(--aw-chrome-ease),
    margin       var(--aw-chrome-dur) var(--aw-chrome-ease),
    opacity      .15s cubic-bezier(.4, 0, .2, 1),
    border-radius var(--aw-chrome-dur) var(--aw-chrome-ease);
}
.canvas-toolbar .toolbar-group .btn.btn-icon {
  width: 32px;
  padding: 0;
}
.canvas-toolbar .toolbar-group .btn:hover {
  background: rgba(56, 120, 90, 0.09) !important;
  color: #1F6E45 !important;
  transform: none !important;
  filter: none !important;
}
.canvas-toolbar .toolbar-group .btn:active {
  transform: none !important;
  filter: none !important;
}
.canvas-toolbar .toolbar-group .btn:focus-visible {
  outline: 0;
  box-shadow: 0 0 0 2px rgba(60, 120, 92, 0.55);
}

/* Sidebar rail card (the nav in collapsed/rail mode on workflow
   routes) — saas-shell.css already gives it a glass-card look,
   but with an 8px border-radius and a 0.92-opacity background.
   Bump to the same 12px radius + 0.56 surface + matching border
   + matching shadow as the bar tile so the row reads coherent.
   Also drop the rail's 4px padding so the menu collapse button
   inside can fill the whole 40×40 card — same structure as the
   bar's chevron filling the collapsed bar tile. The rail's other
   children are display:none in rail mode (sidebar-nav.css), so
   only the collapse button is rendered inside. */
body.route-workflow .app > .wf-nav.is-rail {
  background: rgba(255, 255, 255, 0.56) !important;
  border: 1px solid rgba(255, 255, 255, 0.70) !important;
  border-radius: 12px !important;
  -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6),
              0 10px 30px rgba(55, 72, 64, 0.14) !important;
  padding: 0 !important;
  /* OWNER ASK H3 — the rail chip outranks the palette's invisible grip.
     The nav is `z-index: 6` on the workflow route (saas-shell.css:104)
     and the floating Canvas Tools palette is `z-index: 42`
     (topbar.css:458), so the palette's `.wf-toolbar__resize` grip — a
     30x30 hit box that reaches 22px PAST the palette's border box
     (topbar.css:637-639) and is invisible until hovered — used to win
     the hit test over anything the coordinator parked beside the
     palette. Pass A paid for that with a 34px gap, which is the gap the
     owner reported as too big; now that the chip lands on the row's
     14px pitch its bottom-left corner enters the grip's box, so the
     clearance is bought here instead.
     Scoped to `.is-rail` deliberately: the surface being lifted is a
     40x42 card, and matrix ruling 8 ("nothing may be fully occluded")
     cuts this way round — a 40x42 chip can be swallowed whole by a
     552px palette, while the palette can only ever lose a 40x42 patch.
     The expanded nav's stacking is untouched. */
  z-index: 43;
}

/* Sidebar collapse / menu button (.wf-iconbtn.wf-nav__collapse,
   the ≡ icon inside the rail card).
   Structure: the bar chevron is 40×40 with 12px radius filling
   the entire bar tile (topbar.css line 333-336). Mirror that for
   the menu button so it ALSO fills its 40×40 rail card with
   12px radius — the hover surface then covers the full tile and
   the visual reads as one card-with-icon, identical to the bar
   chevron's structure.
   Styling: hover + focus exactly match the bar chevron
   (topbar.css lines 339-342): rgba(56,120,90,.09) bg /
   #1F6E45 color on hover, 2px focus ring at rgba(60,120,92,.55).
   The .wf-nav.is-rail scope keeps the expanded-nav header
   layout untouched (the ≡ button stays at its component-side
   30×30 inside the wf-nav__tools row when the nav is expanded). */
.wf-nav.is-rail .wf-iconbtn.wf-nav__collapse {
  /* ROUND 30: 38 in the rail's 38 px box (it was 40, overflowing by 2 and making the rail 42
     tall); radius 11 inside the 12 px border, like the zoom toggle */
  width: 38px !important;
  height: 38px !important;
  border-radius: 11px !important;
  padding: 0 !important;
  transition: background-color .12s, color .12s;
}
/* Match the bar chevron's default icon color (#41584C = --label
   inside .wf-bar's token scope) so the ≡ glyph reads at the same
   weight as the bar's collapsed-chevron glyph. The upstream
   .wf-iconbtn uses --muted (#6E8077, a lighter gray-green) which
   left the ≡ visibly washed out next to the chevron. */
.wf-iconbtn.wf-nav__collapse {
  color: #41584C !important;
}
.wf-iconbtn.wf-nav__collapse:hover {
  background: rgba(56, 120, 90, 0.09) !important;
  color: #1F6E45 !important;
}
.wf-iconbtn.wf-nav__collapse:focus-visible {
  outline: 0;
  /* Override sidebar-nav.css's `box-shadow: inset 0 0 0 2px ...`
     for .wf-iconbtn:focus-visible (which would draw a 2px green
     line INSIDE the button) with the bar chevron's outset 2px
     focus ring at the same colour, matching how the bar focuses. */
  box-shadow: 0 0 0 2px rgba(60, 120, 92, 0.55) !important;
}

/* ============================================================
   Zoom percent input + collapsible zoom group.

   The toolbar's zoom group now holds:
     [−]   [ NN% input ]   [+]   [Fit]   [ chevron toggle ]
   The chevron toggle (`#btn-zoom-toggle`) flips
   `.toolbar-group#zoom-group.is-zoom-collapsed`. In collapsed
   state every child except the percent input and the chevron
   shrinks to zero width + zero opacity; the group itself
   contracts naturally because each hidden child loses its
   intrinsic width. The transition on the child elements
   (`width / min-width / padding / margin / opacity`) is the
   same var(--aw-chrome-dur) var(--aw-chrome-ease) used by the bar's
   `--ease-bar` family so the collapse motion reads as part of
   the same design language as the bar's chevron morph.
   The input value is two-way-bound to Canvas.scale by app.js
   (MutationObserver on #canvas-stage's transform attribute for
   live updates, Enter/blur → Canvas.setScale for user input).
   ============================================================ */

.zoom-pct {
  /* Inline input that sits inside the toolbar-group glass pill.
     Same height as the surrounding btn-icons (32px) so the row
     stays vertically aligned. Transparent until focused so the
     glass surface shows through; on focus the input lights up
     with the bar's green focus token.
     Border-width is animated separately from border-color so
     the 1-px focus frame can shrink to 0 during the collapse
     morph without an interim color flicker. */
  width: 56px;
  height: 32px;
  border-width: 1px;
  border-style: solid;
  border-color: transparent;
  background: transparent;
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
  font-weight: 500;
  text-align: center;
  padding: 0 4px;
  border-radius: 8px;
  outline: 0;
  /* Matches `.toolbar-group .btn` above: geometry (.26s) +
     short opacity (.15s) so the percent label fades out
     before the surrounding pill has finished shrinking, in
     lockstep with the buttons. */
  transition:
    background-color .12s, border-color .12s,
    width        var(--aw-chrome-dur) var(--aw-chrome-ease),
    padding      var(--aw-chrome-dur) var(--aw-chrome-ease),
    margin       var(--aw-chrome-dur) var(--aw-chrome-ease),
    opacity      .15s cubic-bezier(.4, 0, .2, 1),
    border-width var(--aw-chrome-dur) var(--aw-chrome-ease);
}
.zoom-pct:hover {
  background: rgba(56, 120, 90, 0.09);
}
.zoom-pct:focus {
  background: rgba(255, 255, 255, 0.7);
  border-color: rgba(60, 120, 92, 0.55);
}
html.wf-theme-night .zoom-pct {
  color: #E7EFE9;
}
html.wf-theme-night .zoom-pct:hover {
  background: rgba(120, 200, 160, 0.13);
}
html.wf-theme-night .zoom-pct:focus {
  background: rgba(255, 255, 255, 0.06);
  border-color: rgba(120, 200, 160, 0.6);
}

/* Zoom toggle icon — matched to the bar chevron + menu icon
   so the three sibling tiles in the same row read as one
   coherent control system:
     • Both `.wf-bar__ic svg` (bar chevron) and `.wf-iconbtn svg`
       (menu ≡) render their glyph at 18×18.
     • The bar chevron and menu icon use `#41584C` (`--label`
       inside the bar's token scope, and the menu's override
       above) as the icon stroke colour.
   The zoom toggle's SVG size + stroke + colour are pinned to
   those same values in both states so the icon sits at exactly
   the same visual weight as its siblings. */
.canvas-toolbar .toolbar-group .zoom-toggle {
  color: #41584C;
}
.canvas-toolbar .toolbar-group .zoom-toggle svg {
  width: 18px;
  height: 18px;
  display: block;
}
.toolbar-group#zoom-group.is-zoom-collapsed .zoom-toggle svg {
  width: 18px;
  height: 18px;
}

/* Collapsed state — the WHOLE zoom group reads as a single
   40×40 glass tile that matches the collapsed header bar tile
   and the sidebar rail tile. Every child except the magnifying-
   glass toggle shrinks to zero width + zero opacity; the
   percent input is hidden along with the zoom-in / zoom-out /
   Fit buttons (only the magnifier remains visible). The toggle
   itself expands to the 40×40 tile dimensions and the group's
   horizontal padding goes to 0 so the toggle fills the tile —
   identical structure to the bar's collapsed chevron filling
   the bar tile. */
.toolbar-group#zoom-group.is-zoom-collapsed > #btn-zoom-out,
.toolbar-group#zoom-group.is-zoom-collapsed > #btn-zoom-in,
.toolbar-group#zoom-group.is-zoom-collapsed > #btn-fit,
.toolbar-group#zoom-group.is-zoom-collapsed > .zoom-pct {
  /* `border-width: 0` (NOT `border: 0` shorthand) so the
     border smoothly shrinks from 1px → 0 in lockstep with the
     width/opacity/padding/margin transitions. The shorthand
     `border: 0` would also flip border-color from transparent
     to currentColor (per spec), introducing a one-frame color
     flicker during the collapse morph. */
  width: 0;
  min-width: 0;
  padding: 0;
  margin: 0;
  opacity: 0;
  overflow: hidden;
  pointer-events: none;
  border-width: 0;
}
.toolbar-group#zoom-group.is-zoom-collapsed {
  /* Lock the OUTER tile to 40×40 — same as the bar's
     collapsed tile (.wf-bar.is-collapsed). With box-sizing:
     border-box (main.css:67 sets it globally), the 40px width
     INCLUDES the 1px borders, leaving a 38×38 content area
     inside — identical to the bar tile's interior.
     `gap: 0` is critical: the group has `gap: 2px` between
     flex items. Even when the four left-side children
     (zoom-out, %, zoom-in, Fit) shrink to width:0 in
     collapsed state, the four 2px gaps remain — pushing the
     visible magnifier 8px right. Zeroing the gap keeps the
     toggle centred. */
  width: 40px;
  padding: 0;
  gap: 0;
  transition: padding var(--aw-chrome-dur) var(--aw-chrome-ease),
              gap var(--aw-chrome-dur) var(--aw-chrome-ease),
              width var(--aw-chrome-dur) var(--aw-chrome-ease);
}
.toolbar-group#zoom-group.is-zoom-collapsed > .zoom-toggle {
  /* The toggle is 38×38 with a 11px radius — exactly the
     interior of the 40×40 group (40 outer − 2×1px border = 38
     content; group's 12px outer radius − 1px border = 11px
     inner curve). The hover background then paints the
     entire inner content area with the matching corner
     rounding, leaving a uniform 1px-border whitespace all
     around the hover paint — same visual language as the
     bar's collapsed chevron tile but without `overflow:
     hidden`, which would clip any popover descendants. */
  width: 38px !important;
  height: 38px !important;
  min-width: 38px;
  border-radius: 11px;
}

/* Horizontal breathing room between the sidebar (in rail OR
   expanded) and the canvas-toolbar groups. The sidebar layout
   bridge in sidebar.js writes `--aw-toolbar-left` based on the
   nav rect + side-chip width with a small gap; for the
   expanded-nav state we add 8px to the bridge's value so the
   toolbar group sits one bar-gap unit further right of the
   expanded nav. */
body.route-workflow .canvas-toolbar {
  /* coordinator dx composes as `translate` (rule near the end
     of this file); `left` stays bridge-driven so the CSS
     transition handles bridge writes natively. */
  left: calc(var(--aw-toolbar-left, 262px) + 8px) !important;
  transition: top  var(--aw-chrome-dur) var(--aw-chrome-ease),
              left .42s cubic-bezier(.32, .72, 0, 1);
}

/* RAIL-mode alignment: when the sidebar nav is in rail mode
   (.app.has-nav-rail — set by sidebar.js layout bridge at
   sidebar.js:724 whenever the nav switches into the 40-px
   rail tile), force the canvas-toolbar's left edge to land at
   the SAME x-coordinate the floating bar starts at —
   `calc(2 * var(--bar-gap) + 40px)` = 68px. The bar uses that
   exact value in its own `left:` rule above (the `--bar-gap +
   logo-tile + --bar-gap` lane). With this override, the bar's
   left edge and the canvas-toolbar's left edge sit on the same
   vertical line — the zoom % tile lands directly under the
   expanded bar.
   In rail mode the nav is only 40px wide at left:14, so
   nav.right is 54px and there is a full 14-px gap before the
   toolbar starts at x:68. No overlap.
   When the nav is EXPANDED this override does not apply (the
   .has-nav-rail class is removed), so the bridge's wider
   value above takes over and the toolbar clears the expanded
   nav with its own dynamic gap. */
body.route-workflow .app.has-nav-rail .canvas-toolbar {
  left: calc(2 * var(--bar-gap, 14px) + 40px) !important;
}

/* DRAGGED-nav alignment: when the user has dragged the sidebar
   nav out of its default position (`.wf-nav.is-moved` — set
   inside sidebar-nav.js whenever the nav's drag offset is
   non-zero), align the canvas-toolbar's left edge with the
   LOGO RAIL's left edge — `var(--bar-gap, 14px)` = 14px. The
   logo rail is at `position: fixed; left: 14px;` (top of this
   file) so the toolbar's left edge and the logo's left edge
   now sit on the same vertical line.
   The bridge writes `SHELL_GAP` (12px) for the moved state
   (sidebar.js:797-798), which the base override would turn
   into 12+8=20 — close but not exactly the logo line. This
   :has()-based selector lands the toolbar at exactly 14,
   matching the logo and giving the dragged-nav state a clean
   left-edge gutter. */
body.route-workflow:has(.wf-nav.is-moved) .canvas-toolbar {
  left: var(--bar-gap, 14px) !important;
}
/* ROUND 29: a dragged nav keeps is-moved while it is a rail, but the rail docks at the default
   corner - the toolbar clears it there (the rule above used to win and the two overlapped until
   the coordinator shoved the toolbar after the motion) */
body.route-workflow:has(.wf-nav.is-moved) .app.has-nav-rail .canvas-toolbar {
  left: calc(2 * var(--bar-gap, 14px) + 40px) !important;
}

/* ============================================================
   .toolbar-help → lifted to the header row.
   The help group (`?` button) was sitting at y:78 (below the
   bar tile) on the right side of the canvas-toolbar. Lift it
   to the bar's own y:14 line so it joins the floating top row.
   `position: fixed` escapes the .canvas-toolbar's positioning
   context entirely and pins the tile to the top-right of the
   viewport. On Pages routes the help group is hidden anyway
   (its parent .canvas-area is `display: none !important`), so
   the fixed positioning has visual effect only on workflow
   routes — where it sits next to the bar without conflict
   because the bar is `width: max-content` capped to viewport-82
   and never extends past the help tile's left edge under any
   reasonable viewport. The .canvas-help-popover anchor (which
   uses .toolbar-help as its positioning ancestor) keeps
   working because position:fixed still establishes a
   positioned ancestor for absolute descendants. */
.canvas-toolbar .toolbar-group.toolbar-help {
  position: fixed;
  top: var(--bar-gap, 14px);
  right: var(--bar-gap, 14px);
  /* Lock outer to exactly 40×40 so the help tile renders at
     the same dimensions as the bar's collapsed chevron tile
     and the zoom group's collapsed tile. No `overflow:
     hidden` here — the .canvas-help-popover (which positions
     absolutely against .toolbar-help via `top: calc(100% +
     6px)`) would be clipped by it. The 38×38 inner button
     below produces the same visual result without needing the
     clip. */
  width: 40px;
  padding: 0;
  gap: 0;
}
.canvas-toolbar .toolbar-group.toolbar-help .btn {
  /* Same 38×38 / 11px-radius treatment as the zoom toggle:
     button fits exactly inside the group's 38×38 interior so
     hover paint has a uniform 1px-border whitespace all
     around. */
  width: 38px !important;
  min-width: 38px;
  height: 38px !important;
  border-radius: 11px;
}

/* ============================================================
   Slice 4H-4C-1 (expanded UX): canvas-shortcuts help group when
   LIFTED to be a direct child of .app (see index.html).
   The help group can no longer rely on the canvas-toolbar
   chrome rules above because it is no longer a descendant of
   .canvas-toolbar. This self-contained block applies the
   glass-card chrome + 40×40 tile geometry + button styling
   so the lifted tile renders identically to a collapsed bar
   tile / zoom group tile.
   Position: the tile sits at the same horizontal line as the
   floating .wf-bar topbar (top: var(--bar-gap)) and at the
   right edge with matching --bar-gap inset — so it visually
   joins the bar's row without overlapping the canvas-tools
   menu that floats to the right of the bar.
   The `.app > .toolbar-group.toolbar-help` selector keeps the
   styling scoped specifically to the lifted instance and
   does NOT affect the canvas-toolbar's zoom group (which
   stays styled by the unchanged `.canvas-toolbar .toolbar-group`
   rules above).
   ============================================================ */
.app > .toolbar-group.toolbar-help {
  /* Position — viewport-anchored on the same horizontal line
     as the topbar floating pill (.wf-bar at top: var(--bar-gap),
     height: var(--bar-h, 48px)). The help tile is 40px tall but
     the bar is 48px tall, so simply matching their `top` values
     would align their TOPS — leaving the help tile's CENTER 4px
     above the bar's center. Add half the height-difference to
     the inset so the tile's vertical CENTER sits on the bar's
     vertical center: 14 + (48-40)/2 = 18px (expanded). The same
     formula stays centered when the bar collapses (it morphs to
     height 40px + translateY(4px), so the bar's visual midline
     shifts to y=38; the tile at top:18px keeps its midline at
     y=38 as well — no extra rule needed for is-collapsed). */
  position: fixed;
  top: calc(var(--bar-gap, 14px) + (var(--bar-h, 48px) - 40px) / 2);
  right: var(--bar-gap, 14px);
  z-index: 30;
  /* Geometry — 40×40 floating glass card matching the zoom
     group's collapsed tile and the bar's collapsed chevron. */
  width: 40px;
  height: 40px;
  padding: 0;
  gap: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Glass card chrome — copy of `.canvas-toolbar .toolbar-group`
     (line ~1339 above) so the lifted tile reads identically. */
  background: rgba(255, 255, 255, 0.56);
  border: 1px solid rgba(255, 255, 255, 0.70);
  border-radius: 12px;
  -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6),
              0 10px 30px rgba(55, 72, 64, 0.14);
  /* No `overflow: hidden` here — the .canvas-help-popover (which
     positions absolutely against .toolbar-help via
     `top: calc(100% + 6px)`) would be clipped by it. */
}
.app > .toolbar-group.toolbar-help .btn {
  /* 38×38 inner button matching the existing canvas-toolbar
     button treatment — transparent so the glass surface shows
     through, no border, the group's 1px border is the visible
     chrome. */
  background: transparent !important;
  border: 0 !important;
  box-shadow: none !important;
  width: 38px !important;
  min-width: 38px;
  height: 38px !important;
  padding: 0;
  border-radius: 11px;
  color: #41584C;
  font-size: 16px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background-color .12s, color .12s;
}
.app > .toolbar-group.toolbar-help .btn:hover {
  background: rgba(56, 120, 90, 0.09) !important;
  color: #1F6E45;          /* ROUND 30: the same hover green as every other folded chip */
  transform: none;         /* no 1 px lift - the others sit still */
}
.app > .toolbar-group.toolbar-help .btn:focus-visible {
  outline: 2px solid rgba(60, 120, 92, 0.55);
  outline-offset: 1px;
}
/* Hide the help tile on Pages routes (Dashboard, Workflows,
   Library, Settings, Usage, Admin). Pre-lift, this was achieved
   by the parent .canvas-area being `display: none` on those
   routes; the lifted tile no longer inherits that hide so we
   replicate it explicitly. */
body.route-page .app > .toolbar-group.toolbar-help {
  display: none !important;
}
/* Night-theme variant — mirrors the night chrome the zoom
   group gets at `html.wf-theme-night .canvas-toolbar .toolbar-group`
   (line ~2087 below) so light/dark switch is consistent. */
html.wf-theme-night .app > .toolbar-group.toolbar-help {
  background: rgba(28, 36, 32, 0.56);
  border-color: rgba(255, 255, 255, 0.16);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10),
              0 10px 30px rgba(0, 0, 0, 0.30);
}
html.wf-theme-night .app > .toolbar-group.toolbar-help .btn {
  color: rgba(231, 239, 233, 0.80);
}
html.wf-theme-night .app > .toolbar-group.toolbar-help .btn:hover {
  background: rgba(150, 220, 200, 0.10) !important;
  color: rgba(231, 239, 233, 1);
}

/* ============================================================
   Pages-route header — simplified layout.
   On Pages routes (Dashboard, Workflows, Library, Settings,
   Usage, Admin) the bar's workflow controls have no meaning:
   there's no workflow open, so the breadcrumb, initial-input,
   Run CTA, Pause icon, drawer-toggle icon, canvas-tools
   launcher (already globally hidden), Run-config launcher
   (also already globally hidden), and the `…` overflow are
   all dropped. The visible controls become only:
     • .wf-bar__ic.wf-appearance (theme + view toggle)
     • .wf-acct                  (account / sign-in / sign-out)
     • .wf-bar__ic.wf-hdrtoggle  (header collapse chevron)
   Layout: the bar anchors to the right side of the viewport
   instead of the left (left:auto + right:bar-gap). This keeps
   the bar visually balanced opposite the logo rail on the
   left edge, with the empty page-root area between them.
   ============================================================ */
body.route-page .wf-bar .wf-title,
body.route-page .wf-bar .wf-bar__gap,
body.route-page .wf-bar .wf-input,
body.route-page .wf-bar .wf-cta-run,
body.route-page .wf-bar .wf-run__cfg,
body.route-page .wf-bar .wf-aw-pause,
body.route-page .wf-bar .wf-aw-drawer,
body.route-page .wf-bar .wf-toolsbtn,
body.route-page .wf-bar .wf-bar__more,
/* Hide the chevron (header collapse) on Pages routes — there's
   nothing meaningful to collapse here, the bar already only
   carries appearance + acct. The run-config sliders (.wf-run__cfg)
   also have no audience here (no run to configure), so they're
   in the hide list to keep the surviving bar a clean
   `appearance + avatar` pair. */
body.route-page .wf-bar .wf-hdrtoggle {
  display: none !important;
}
/* Right-align the bar with the page-root's content right edge.
   page-root horizontal padding is `clamp(16px, 4vw, 48px)`
   (saas-shell.css line ~219), so dashboard/list/admin content
   (and primary actions like Dashboard's `+ New workflow`) ends
   at `viewport_width - clamp(16px, 4vw, 48px)`. Match that
   exactly so the bar's right edge sits on the same vertical
   line as the page header's primary action. The bar then reads
   as part of the page's right gutter, not as a viewport-flush
   floating control. */
body.route-page .wf-bar {
  left: auto !important;
  right: clamp(16px, 4vw, 48px) !important;
}
/* Pages avatar — shift `.wf-acct`'s right anchor from the
   default 46px (which reserved a 12px gap to the chevron at
   right:0) all the way to 8px so the 34×34 face sits flush
   near the bar's right edge. The chevron is hidden here, so
   that 46→8 shift is purely a tightening, not a re-layout.
   Avatar now occupies the right strip from `right:8` to
   `right:42`. Inner padding-right must reserve that strip
   plus a small breathing gap so the appearance icon (the
   only surviving flex child in `.wf-run`) doesn't collide
   with it — 50px gives the avatar's 42px footprint + 8px
   gap. Without this fix the appearance icon overlapped the
   avatar (see the "AD"-with-ghost-ring overlap reported on
   Pages routes). */
body.route-page .wf-bar .wf-acct {
  right: 8px !important;
}
body.route-page .wf-bar .wf-bar__inner {
  padding-right: 50px !important;
}
/* Pages routes never collapse the bar. The native is-collapsed
   class might be carried over from a workflow-route session
   via persisted state (wf-bar-state-v2 → header.collapsed); we
   force the bar to render its expanded geometry on Pages by
   overriding every property the `.is-collapsed` rule sets
   (height, border-radius, transform, cursor). `width:
   max-content !important` reclaims control from the inline
   `style.width = 40px` that `shrinkTile` may have set when the
   bar last collapsed on a workflow route. The transition rule
   on the bar itself keeps the .34s curve so a route change
   while collapsed gracefully slides into the expanded shape. */
body.route-page .wf-bar.is-collapsed {
  height: var(--bar-h, 48px) !important;
  border-radius: var(--bar-radius, 16px) !important;
  transform: none !important;
  cursor: default !important;
  width: max-content !important;
}
/* ROUND 13 verify (track N, P8): the rule above forces the EXPANDED
   geometry on a bar that ARRIVES collapsed (collapsed on the workflow
   route by the logo or the chevron, then a Pages route by the nav; or a
   reload onto a page with header.collapsed persisted), but
   `.wf-bar.is-collapsed .wf-bar__cwrap` (line ~240) still hides its
   CONTENT: a 99 x 48 empty pill with 0 painted buttons (measured on the
   composed tree, hunt-dsf1 P8). And on a page the logo no longer drives
   the bar at all (topbar-adapter.js hookBrandClick: not a participant
   there) and the chevron is hidden (above), so nothing on the page can
   bring the content back -- the owner's "it gets empty and on uncollapse
   remains the same" by the carried-over path. A page never collapses the
   bar: its content is painted whatever the persisted state; the workflow
   route keeps the user's collapsed tile untouched. */
body.route-page .wf-bar.is-collapsed .wf-bar__cwrap {
  opacity: 1 !important;
  visibility: visible !important;
  pointer-events: auto !important;
}

/* ============================================================
   Sidebar tools row — remove the inherited `margin-left: 2px`
   on .wf-iconbtn buttons (sidebar-nav.css:121) inside the rail
   context. The 2px was the visual gap between consecutive icon
   buttons in the expanded tools row (collapse / pin / drag /
   reset). In rail mode there is only ONE visible button and
   it should sit flush with its tile centre — the inherited
   margin would push it 2px right. We scope the override to
   .wf-nav.is-rail only so the expanded-mode multi-button
   layout keeps its inter-button gap. */
.wf-nav.is-rail .wf-nav__tools > .wf-iconbtn {
  margin-left: 0 !important;
}

/* ============================================================
   Projects group — sticky stacking for project rows and their
   nested workflow rows.

   The sidebar's existing sticky design (sidebar-nav.css:363
   and :424) already pins:
     • `.wf-group__head` (section header) at top:0 (z:3).
     • `.wf-item--recent` (Recent workflow rows) at top:33px
       (z:2). Each Recent row stacks below the section header
       and hands off to the next as the user scrolls.
   The Projects group never got the same treatment — only the
   "PROJECTS" header pinned, and the project rows + their
   nested workflow rows scrolled away with the content. Add the
   same stacking pattern here at TWO nesting levels:
     • Project row (.wf-group[data-id="projects"] .wf-item that
       is NOT a child workflow and NOT a block row) pins
       directly under the section header at top:33px.
     • Project-child workflow row (.wf-cwf — the class
       sidebar-nav.js uses for workflows nested inside a
       project, see Slice 4E-7E block-tree work) pins under
       the project at top:65px (33px section-header offset +
       ~32px project-row height).

   Backing is painted via a full-bleed `::before` pseudo with
   `left:-11px; right:-11px` so it reaches into the nav's
   outer padding — same width as the upstream Recent substuck
   pattern (sidebar-nav.css:429) and the `.wf-group__head`
   section header. An earlier iteration used `background-color`
   directly on the row to sidestep the `.wf-blocks__inner`
   overflow:clip (sidebar-nav.css:344) but two problems came
   out of that:
     1. Workflow rows ended up visibly narrower than project
        rows (no `-11px` extension reaches into nav padding),
        breaking the "full width like sections" feel.
     2. The row inherits `transition: background .12s` from
        the upstream `.wf-item` rule (sidebar-nav.css:303), so
        when the JS observer toggles `is-aw-stuck`, the bg
        FADES IN over 120ms — leaving a semi-transparent
        window where the next-stuck workflow's text bled
        through the outgoing-stuck workflow's still-fading bg.
        That's the "title of one workflow overlapping another
        on scroll" effect the user reported.
   `::before` has no inherited transition, so the substuck bg
   paints INSTANTLY opaque when the class arrives — no fade,
   no bleed, no overlap. To let the workflow row's `::before`
   actually extend horizontally past `.wf-blocks__inner`, the
   block below overrides that container's `overflow:clip` to
   `overflow: visible clip` (horizontal visible, vertical clip
   so the expand/collapse height animation still has a
   clipping container) — mirroring the asymmetric clip the
   upstream itself uses on `.wf-group__inner` at
   sidebar-nav.css:392.

   ALL `.wf-cwf` rows are sticky (no `.is-open` gate) so the
   classic "next sticky covers previous sticky" handoff falls
   out of CSS layout for free. As the user scrolls past one
   workflow's block tree, the next workflow's natural position
   reaches the 65px boundary, becomes sticky there, and paints
   on top of the previously pinned row (later in DOM = later
   in paint order). With opaque `--substuck-bg` on every
   stuck workflow row, the previous one is fully occluded —
   visually reading as the next workflow pushing the previous
   out. The `.is-open` class still controls block-tree
   expansion below the row (sidebar-nav.css:343 keeps the
   `grid-template-rows: 1fr` rule on `.wf-item.is-open +
   .wf-blocks`); only ONE workflow has `.is-open` at a time
   per the topbar-adapter rule, so blocks never stack
   vertically beneath two open workflows. */
.wf-group[data-id="projects"] .wf-item:not(.wf-cwf):not(.wf-block) {
  position: sticky;
  top: var(--sub-top, 33px);
  z-index: 2;
}
.wf-group[data-id="projects"] .wf-cwf {
  position: sticky;
  top: calc(var(--sub-top, 33px) + 32px);
  z-index: 1;
}
/* Allow the workflow row's `::before` extension to escape the
   nested `.wf-blocks__inner` overflow:clip horizontally while
   keeping vertical clipping intact for the expand/collapse
   height animation. Same asymmetric-overflow pattern the
   upstream uses on `.wf-group__inner` (sidebar-nav.css:392).
   The `overflow:hidden` fallback line keeps older browsers
   (without `clip`/two-value shorthand support) clipping on
   both axes — same defensive ladder as the upstream rule. */
.wf-group[data-id="projects"] .wf-blocks__inner {
  overflow: hidden;
  overflow: visible clip;
}
/* Rotate the project row's chevron when the row is open. The
   upstream rotation rule at sidebar-nav.css:514 requires
   `.wf-hasacts` on the row, but AirWanderer's projects render
   in read-only mode (no mutation buttons) so sidebar-nav.js's
   makeItem skips the `.wf-hasacts` class for them. The chevron
   button (`.wf-rexp`) is still emitted by the readonly branch
   of `actCluster`, just with no rotation hook. Match the
   project rows directly so the `>` (closed) ↔ `v` (open) flip
   fires regardless of `wf-hasacts`. The 90° rotation matches
   the upstream value exactly, and `.wf-rexp` already carries
   `transition: transform var(--dur) var(--ease)` so the flip
   eases in the same .24s curve as the upstream chevrons. */
.wf-group[data-id="projects"] .wf-item:not(.wf-cwf):not(.wf-block).is-open > .wf-racts > .wf-rexp {
  transform: rotate(90deg);
}
/* Substuck backing only paints when the row is ACTUALLY pinned
   at the sticky boundary — the `is-aw-stuck` class is toggled
   by a small rAF-throttled scroll observer in topbar-adapter.js
   that compares each row's `getBoundingClientRect().top`
   against the current sticky threshold. At rest, no
   `is-aw-stuck` rows = no `::before` painted, so the nav stays
   calm and the project's children read as a clean list. During
   scroll, each row picks up the class the moment it pins; for
   the workflow-row handoff, multiple wf-cwf rows can briefly
   carry the class simultaneously (the outgoing one and the
   incoming one both stick at the same `top`), and CSS DOM
   order paints the incoming one over the outgoing one — the
   "next workflow pushes previous out" effect the user asked
   for. */
.wf-group[data-id="projects"] .wf-item:not(.wf-cwf):not(.wf-block).is-aw-stuck::before,
.wf-group[data-id="projects"] .wf-cwf.is-aw-stuck::before {
  content: "";
  position: absolute;
  /* `top: -100%` extends the backing UP by one row-height so a
     rising row covers the previously-pinned row's visible
     content during the overlap window — without this the
     rising row's `::before` started at its own top edge,
     leaving the outgoing row's text visible above the rising
     row's box (the "Ideation / My first workflow" overlap the
     user reported). `-100%` resolves to `-row-height` for an
     absolutely-positioned pseudo, so this scales correctly when
     a workflow row is taller (longer name wrapping a line). The
     row above (project row at z:2, section header at z:3) sits
     at a higher z-index than the workflow row at z:1, so any
     overshoot past their boundary is harmlessly painted under
     them. */
  top: -100%; bottom: 0;
  left: -11px; right: -11px;
  background-color: var(--substuck-bg, #F7F8F8);
  z-index: -1;
}
/* Hover/active feedback needs no extra rules here: the upstream
   `.wf-item:hover` (sidebar-nav.css:304) and `.wf-item.is-active`
   (sidebar-nav.css:305) paint their tints on the row directly,
   and because `::before` sits at `z-index:-1` BEHIND the row's
   content layer, the upstream's translucent row bg composites
   ON TOP of the opaque substuck `::before` automatically. The
   user sees substuck-color + hover/active tint, never the
   scrolling content behind. */

/* ============================================================
   Night-mode shell overrides.

   The topbar's Appearance popover toggles `wf-theme-night` on
   <html> (topbar.js's applyAppearance, see also handover §10.3).
   The upstream bar paints itself via its own scoped tokens; this
   block extends night-mode coverage to the surrounding shell so
   the canvas, sidebar rail, drawer, page-root, and toolbar
   groups all darken in concert when night is on. The palette
   reuses the same values declared on `.wf-bar.is-night` at the
   top of this file (rgba(28,36,32,.66) surface, .12 white
   border, #E7EFE9 text, etc.) so the entire frame reads as one
   coherent dark theme.

   Scope: this only adapts SHELL chrome (canvas-area background,
   toolbar groups, sidebar/rail/drawer cards, page-root) — it does
   NOT touch dashboard cards, block cards, modals, or any other
   component that would need its own per-surface night palette.
   Those remain on the future per-component theming track.
   ============================================================ */

/* Body / canvas surface. */
html.wf-theme-night body {
  background: #0E1411;
  color: #E7EFE9;
}
html.wf-theme-night .canvas-area {
  background-color: #0E1411;
  background-image:
    radial-gradient(circle, rgba(143, 211, 176, 0.10) 1px, transparent 1px),
    radial-gradient(circle, rgba(143, 211, 176, 0.04) 1px, transparent 1px);
}

/* Sidebar rail card — switch to the night glass palette. */
html.wf-theme-night body.route-workflow .app > .wf-nav.is-rail {
  background: rgba(28, 36, 32, 0.66) !important;
  border-color: rgba(255, 255, 255, 0.12) !important;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06),
              0 10px 30px rgba(0, 0, 0, 0.42) !important;
}
html.wf-theme-night .wf-iconbtn.wf-nav__collapse {
  color: #9FB4AA !important;
}
html.wf-theme-night .wf-iconbtn.wf-nav__collapse:hover {
  background: rgba(120, 200, 160, 0.13) !important;
  color: #CFE9DB !important;
}

/* Right drawer card. */
html.wf-theme-night .app-drawer {
  background: rgba(28, 36, 32, 0.66);
  border-color: rgba(255, 255, 255, 0.12);
  color: #E7EFE9;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06),
              0 10px 30px rgba(0, 0, 0, 0.42);
}

/* Page-root background (Dashboard / Workflows / Library / etc). */
html.wf-theme-night body.route-page .page-root {
  background: #0E1411;
  color: #E7EFE9;
}

/* Canvas-toolbar groups — switch to the night glass palette and
   re-skin the buttons inside so the hover/focus tokens match
   the bar's night-mode chevron. */
html.wf-theme-night .canvas-toolbar .toolbar-group {
  background: rgba(28, 36, 32, 0.66) !important;
  border-color: rgba(255, 255, 255, 0.12) !important;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06),
              0 10px 30px rgba(0, 0, 0, 0.42) !important;
}
html.wf-theme-night .canvas-toolbar .toolbar-group .btn {
  color: #E7EFE9;
}
html.wf-theme-night .canvas-toolbar .toolbar-group .btn:hover {
  background: rgba(120, 200, 160, 0.13);
  color: #CFE9DB;
}
html.wf-theme-night .canvas-toolbar .toolbar-group .btn:focus-visible {
  box-shadow: 0 0 0 2px rgba(120, 200, 160, 0.6);
}

/* Sidebar nav LEFT alignment with the logo rail.
   The logo tile (.wf-rail) is `position: fixed; left: 14px;`
   (top of this file). The sidebar nav on workflow routes uses
   `left: var(--shell-gap, 12px)` from saas-shell.css, and on
   page routes lives in the grid `nav` cell with a 12px left
   margin — both 2px to the left of the logo. Pull both to
   x:14 so the left edge of the rail (collapsed menu tile) sits
   directly beneath the logo's left edge. */
body.route-workflow .app > .wf-nav {
  /* Pin nav's left to the bar-gap base; the coordinator's dx
     composes as the `translate` property (rule at the bottom
     of this file). */
  left: var(--bar-gap, 14px) !important;
}
body.route-page .app > .wf-nav {
  margin-left: var(--bar-gap, 14px) !important;
}

/* ============================================================
   Canvas Tools — original mechanics, real registry (CT-2R).

   The upstream topbar component ships a designed Canvas Tools
   system: an entry-point icon (`.wf-toolsbtn`), an in-header
   tools-mode panel (`.wf-bar__mode--tools`) with Pin / Back /
   More, and a portalled draggable / resizable / collapsible
   floating palette (`.wf-toolbar`).
   CT-1 / earlier-CT-2 hid the whole thing because the upstream
   shipped it with a DEMO registry (Group frame / Draw / Simple
   frame / Comment / Figure / Erase / Ignore frame / Text) that
   dispatched `wf:canvas` events nothing listened for.

   CT-2R takes a different path: the registry is now hook-
   readable (`window.__WF_BAR_TOOL_DEFS__` in topbar.js), the
   AirWanderer adapter (topbar-adapter.js step 3b) injects a
   REAL three-tool registry (text_note / comment / simple_frame)
   that maps to CT-1's `canvas_items`, and the upstream's
   `fireCanvas(act)` now short-circuits to
   `HOOKS.canvasTool(act)` which arms placement mode. The demo
   `wf:canvas` dispatch survives ONLY as a standalone-component
   fallback and is unreachable in AirWanderer production.

   With the demo data path closed, the visual suppression rules
   for `.wf-toolsbtn`, `.wf-bar__mode--tools`, and `.wf-toolbar`
   are LIFTED here — the original UI mechanics are now visible
   and functional. The localStorage sanitizer in
   topbar-adapter.js keeps stripping pre-CT-2R blobs (legacy
   demo state) but preserves post-CT-2R real-toolbar geometry
   via the `aw_tools_version: "ct2r"` marker (stamped by
   topbar.js's `saveState`).

   What stays suppressed:
     • The floating `.wf-toolbar` is workflow-only — Pages
       routes don't render the canvas, so the floating palette
       has no audience there. Scoped Pages-route hide below.
     • The in-header `.wf-toolsbtn` and `.wf-bar__mode--tools`
       are already in the Pages-route hide list above (alongside
       `.wf-aw-pause`, `.wf-aw-drawer`, `.wf-bar__more`, etc.).
   ============================================================ */
body.route-page .wf-toolbar {
  display: none !important;
}

/* Placement-mode cursor (CT-2R).
   When `body.aw-tool-active` is set (an `enterToolMode` call
   armed a real tool), the user is in "click to place" mode —
   the canvas viewport and stage show a crosshair so the
   placement target is obvious. `!important` is required because
   `components.css` already sets per-element cursors on `.block`
   (move) and `.handle-*` (port-specific) that would otherwise
   win on specificity. The crosshair stays even when hovering
   blocks/handles, but the click itself is filtered at the JS
   level so block clicks never convert into item placements
   (see `topbar-adapter.js` `_ct2rOnPlacementClick`). */
body.aw-tool-active .canvas-viewport,
body.aw-tool-active .canvas-stage,
body.aw-tool-active .canvas-stage * {
  cursor: crosshair !important;
}

/* Armed-tool visual feedback (CT-2R).
   When a tool is armed, every `.wf-tool[data-tool="<type>"]`
   button that's currently in the DOM (in-header tools-mode row
   AND the floating palette slot — same node since `toolsContent`
   moves between them) gets `is-aw-armed`. Style matches the
   upstream `.wf-tool:hover` accent so the button reads as
   "selected" — a single-token visual cue that the next canvas
   click will fire this tool. Removed by `_ct2rExitMode`. */
.wf-tool.is-aw-armed {
  background: var(--row-hover, rgba(56, 120, 90, 0.10));
  color: var(--accent, #2F8A5F);
}
.wf-tool.is-aw-armed:hover {
  background: var(--row-hover, rgba(56, 120, 90, 0.14));
  color: var(--cta, #1C6240);
}

/* ============================================================
   Floating Canvas Tools palette ↔ host chrome coordinator.

   The coordinator (topbar-adapter.js) reacts to `wf:tools-bounds`
   and writes four CSS vars on `.app`:

     --aw-tools-nav-dx       --aw-tools-nav-dy
     --aw-tools-toolbar-dx   --aw-tools-toolbar-dy

   These are consumed below as the `translate` CSS property
   (independent of `transform`, composes with it). translate is
   GPU-composited — the browser updates a matrix, no layout, no
   paint. The base `top` / `left` from the !important rules
   above pin the chrome at its natural position; this rule
   offsets it via translate. The user's manual nav-drag inline
   `transform: translate(...)` composes additively with the CSS
   `translate` property — the final position is
   base-(top,left) + transform + translate.

   The transition on `translate` provides a soft glide for both
   live drag updates AND clear-to-natural events (palette unpin,
   route change). No layout cost during animation. */
body.route-workflow .app > .wf-nav {
  translate: var(--aw-tools-nav-dx, 0px) var(--aw-tools-nav-dy, 0px);
  /* will-change: translate promotes the nav to its own
     compositor layer so coordinator writes update the layer's
     transform matrix on the GPU — no main-thread layout, no
     paint, no style cascade across the nav's deep subtree.
     This is the single largest perf win for CTM-drag smoothness
     since the nav is the heaviest chrome subtree on the page. */
  will-change: translate;
  transition:
    top  var(--aw-chrome-dur) var(--aw-chrome-ease),
    left .42s cubic-bezier(.32, .72, 0, 1),
    translate var(--aw-chrome-dur) var(--aw-chrome-ease);
}
body.route-workflow .canvas-toolbar {
  translate: var(--aw-tools-toolbar-dx, 0px) var(--aw-tools-toolbar-dy, 0px);
  /* AirWanderer chrome (ruling 5, gap G26) — GIVE BACK WHAT THE TRANSLATE
     TAKES. `.canvas-toolbar` is anchored `left … right`, so it STRETCHES to
     the canvas edge; the coordinator's dx then slides the whole stretched
     box right and its right edge leaves the viewport by exactly dx.
     Measured on f8bc598 with the palette pinned and the nav collapsed
     (r3/chrome-t4-r31.json, `head` cells — every rect in this comment is in
     that file):
         1280x800 pass A  1200x40 at (576,78)  visibleFrac 0.587
         1100x900 pass A  1020x40 at (576,78)  visibleFrac 0.514
         1920x1080 pass B 1840x40 at (578,78)  visibleFrac 0.729
     Nothing is painted at that end today, which is why this was only latent
     — but this is the box the help tile's row rides, and any child placed at
     its right end would ride off with it.

     Adding the same dx to `right` shrinks the box by precisely what the
     translate then adds back: the LEFT edge still moves exactly as Pass C
     intends (the zoom cluster is the box's first flex child and the box is
     `justify-content: space-between` with that single child, so it stays
     left-aligned), and the right edge does not move at all. The same three
     cells on the fixed build read 618x40 at (650,78) / 438x40 at (650,78) /
     1256x40 at (652,78), visibleFrac 1.000 in all three.

     The base inset is NOT repeated here — not even as a var() fallback,
     which would be the same literal by another name. It is declared once,
     as --aw-tools-toolbar-right-base on `.canvas-toolbar` in panels.css:19,
     on the very element this rule matches, so the custom property is always
     in scope and the two declarations cannot drift apart. */
  right: calc(var(--aw-tools-toolbar-right-base) + var(--aw-tools-toolbar-dx, 0px));
  will-change: translate;
  /* translate is composed with the bridge-driven `left` (which
     handles ZC's "follow LNM" motion). Keep their transition
     curves IDENTICAL so the two glide as one unit during the
     brand-collapse/expand cascade — a faster translate curve
     would race ahead and produce a visible second-pass
     "settling" after `left` finishes catching up. */
  transition:
    top  var(--aw-chrome-dur) var(--aw-chrome-ease),
    left .42s cubic-bezier(.32, .72, 0, 1),
    right .42s cubic-bezier(.32, .72, 0, 1),
    translate .42s cubic-bezier(.32, .72, 0, 1);
}

/* CTM (floating Canvas Tools palette) — same translate-based
   pattern as nav and toolbar. The coordinator writes
   --aw-tools-ctm-dx/dy on the .wf-toolbar element directly
   (it's a portal-style floating element, not always inside
   .app's inheritance chain). Composes with the inline
   `transform` the upstream sets for is-collapsed etc.
   --------------------------------------------------------
   The `transition` here is a SHORTHAND that overrides the
   upstream .wf-toolbar rule earlier in this file (which has
   transitions on width/height/left/top/border-radius/etc.).
   We have to repeat all those properties here so the collapse
   morph still animates — otherwise the upstream rule's list
   is wiped and width/height/left/top snap instead of glide.
   --------------------------------------------------------
   Curve change: --ease-bar instead of --ease. Upstream's
   --ease (cubic-bezier(.32,.72,0,1)) is the global, heavily
   decelerating curve; --ease-bar (cubic-bezier(.4,0,.2,1)) is
   the more balanced curve the header collapse uses. Switching
   the CTM morph to --ease-bar makes its collapse feel like
   the header's instead of "settling slowly" at the end. */
.wf-toolbar {
  translate: var(--aw-tools-ctm-dx, 0px) var(--aw-tools-ctm-dy, 0px);
  transition:
    background-color .34s var(--ease-bar, cubic-bezier(.4, 0, .2, 1)),
    box-shadow       .34s var(--ease-bar, cubic-bezier(.4, 0, .2, 1)),
    border-color     .34s var(--ease-bar, cubic-bezier(.4, 0, .2, 1)),
    width            .34s var(--ease-bar, cubic-bezier(.4, 0, .2, 1)),
    height           .34s var(--ease-bar, cubic-bezier(.4, 0, .2, 1)),
    border-radius    .34s var(--ease-bar, cubic-bezier(.4, 0, .2, 1)),
    left             .34s var(--ease-bar, cubic-bezier(.4, 0, .2, 1)),
    top              .34s var(--ease-bar, cubic-bezier(.4, 0, .2, 1)),
    translate        .2s  var(--ease-bar, cubic-bezier(.4, 0, .2, 1));
}

/* USER-DRAGGED Limits widget counter-translate is written
   as an inline style by the coordinator (topbar-adapter.js,
   writeToolsChromeOffsets) on .wf-limitpanel.is-moved.
   Implemented in JS rather than CSS so it can't be lost to
   cascade / variable inheritance issues — inline style with
   'important' priority is the most deterministic path.
   --------------------------------------------------------
   ZERO-MOTION GUARANTEE when widget is dragged:
   .wf-nav's translate transitions over .2s normally, so a
   Pass-A shift glides. But when the user has dragged the
   widget away from LNM (lpanel.is-moved), we cannot rely on
   the browser to perfectly sync two separate elements'
   transitions — sub-frame timing differences produce the
   shake the user reported even with matched curves. So
   we SNAP both:
     • .wf-nav's translate (via :has() conditional rule
       below): no transition → instant jump on each update.
     • .wf-limitpanel's translate: no transition either.
   Both writes land in the same paint frame, both update
   instantly, the counter-translate cancels the parent
   translate exactly with no transitional gap. LNM appears
   to "snap" to its new position rather than glide, but that
   trade is intentional — the user has explicitly detached
   the widget and asked it to stay still no matter what. */
.wf-limitpanel.is-moved {
  transition: none !important;
}
body.route-workflow .app:has(.wf-limitpanel.is-moved) > .wf-nav {
  /* Omit `translate` from the transition so the coordinator's
     nav shifts snap instantly in lockstep with the widget
     counter — that's the whole point of this rule.
     --------------------------------------------------------
     IMPORTANT: no `!important` here. sidebar-nav.js's
     toggleRail() sets `nav.style.transition` INLINE with
     "width .3s, height .3s, padding .3s, transform .3s"
     during a rail collapse — that's what gives the LNM its
     smooth shrink animation. An `!important` rule would
     beat the inline transition and break that animation
     (producing a two-step height collapse the user
     reported). Without !important the cascade is:
     • Inline transition present (collapse in flight): inline
       wins. sidebar-nav.js owns the rail animation timing.
     • No inline transition (steady state): this rule wins
       via higher specificity over the base workflow rule.
       translate snaps as designed. */
  transition:
    top  var(--aw-chrome-dur) var(--aw-chrome-ease),
    left .42s cubic-bezier(.32, .72, 0, 1);
}
/* During an active drag the coordinator skips the CTM push so
   the user's input isn't fighting an auto-shift, but the
   `will-change` from .is-dragging already includes left/top —
   no special-case here for the translate. */

/* ============================================================
   Canvas items — passive render (Slice CT-1, Header Canvas Tools
   Foundation).

   Visual-only items rendered as `<div>` children of a single
   `.canvas-items-layer` wrapper that is always the FIRST child of
   `#canvas-stage`. Because the layer is the first child in DOM
   order, every item paints BEHIND the connections SVG (also a
   `#canvas-stage` child) and BEHIND every `.block` div (which are
   appended after). The stage's own
   `transform: translate(...) scale(...)` carries items through pan
   and zoom uniformly with blocks.

   CT-1 is render-only. Items don't accept pointer events here so
   they cannot intercept clicks meant for the canvas (pan), for
   blocks (select / drag), or for connections (right-click). CT-3
   will introduce `pointer-events: auto` plus per-type handlers for
   select / drag / resize / inline edit.

   Three visual variants:
     • is-text-note    — white card with subtle border. Reads as a
       neutral text note.
     • is-comment      — sticky-note look. Yellow surface, slight
       drop shadow, no border.
     • is-simple-frame — dashed, low-saturation outline with no
       fill. Reads as a decorative section frame for grouping
       blocks by eye — NOT a true group (no membership semantics).

   Night-mode variants live at the bottom of this block and
   activate via `html.wf-theme-night` (set by topbar.js's
   `applyAppearance`).
   ============================================================ */
.canvas-items-layer {
  position: absolute;
  inset: 0;
  /* The wrapper itself must never block canvas events. Items inside
     opt back in with `pointer-events: auto` in later slices. */
  pointer-events: none;
  /* Slice 4H-3B v2: the layer no longer sets `z-index: 0`. With
     position: absolute and z-index: auto it does NOT create a
     stacking context, so canvas items (with their own per-type
     z-index below) stack directly inside `#canvas-stage`'s
     context alongside `.connections-svg` (z-index: 1) and
     `.block` (z-index: 2). This is what lets Arrows promote
     ABOVE blocks via `.canvas-item.is-arrow { z-index: 3 }`
     without dragging every other canvas item along — frame /
     text_note / comment keep their existing z-index 0 / 1 / 1
     values, which still rank them below blocks. */
}
/* Phase 3 polish v2: per-type stacking. ``simple_frame`` acts as
   a visual container — anything the user places "inside" must
   paint ON TOP of it. So:
     • simple_frame → z-index: 0  (lowest)
     • text_note / comment → z-index: 1
     • blocks → z-index: 2 (set in components.css)
     • Slice 4H-3B v2: arrow → z-index: 3  (annotation overlay)
   These stack in `#canvas-stage`'s context now that the layer
   no longer creates its own stacking context (see comment on
   `.canvas-items-layer` above). */
/* Slice 4H-5F: simple_frame deliberately has NO z-index so it
   does NOT create its own stacking context. Two reasons:
     1. The frame's chip-row (z-index: 8) needs to live in the
        canvas-stage stacking context, where blocks (z-index: 2)
        and arrows (z-index: 3) sit, so the hidden hover-buttons
        render IN FRONT of any block that happens to overlap the
        chip-row's air space above the frame top edge.
     2. The v5E lift to z-index 30 has been removed for frames
        (it was pushing the frame's contained items/blocks
        BEHIND the frame body). Without a stacking context, the
        frame body stays at z-auto — below the per-type items
        (notes at 1, blocks at 2, arrows at 3) — so contents
        remain visible on top of the frame's tinted fill as a
        natural sibling stacking, regardless of hover/selection
        state. */
.canvas-item.is-text-note,
.canvas-item.is-comment {
  z-index: 1;
}
.canvas-item.is-arrow {
  z-index: 3;
}
.canvas-item {
  position: absolute;
  box-sizing: border-box;
  /* Slice 4G-1: canvas items accept pointer events so the user can
     click to select. text_note + comment cards are entirely
     hittable. simple_frame overrides this to ``none`` below and
     surfaces a small title-handle child as the only selection
     target, so the frame's empty interior never captures clicks
     meant for blocks INSIDE the frame or canvas pan. */
  pointer-events: auto;
  /* User cannot select text yet (no inline editing in 4G-1). */
  user-select: none;
  /* Sane defaults so a placed item is legible even before per-type
     styles override. */
  font-family: inherit;
  font-size: 13px;
  color: var(--text, #1F2A26);
  padding: 8px 10px;
  border-radius: 8px;
  line-height: 1.35;
  white-space: pre-wrap;       /* preserve line breaks in body */
  /* Slice 4G-2: must be ``visible`` so the corner resize handles
     (which sit at -14 px offsets to centre on the bounding-box
     corners) aren't clipped by the parent. Text-content ellipsis
     lives on ``.canvas-item__body`` (text_note + comment) and on
     ``.canvas-item__handle`` (frame chip), so neither needs the
     parent to clip. */
  overflow: visible;
  transition: background-color .12s, border-color .12s, color .12s,
              box-shadow .12s, outline-color .12s;
}
/* Slice 4G-1: selected canvas item — subtle accent ring + slight
   shadow lift. ``outline`` sits OUTSIDE the box, so the ring never
   eats into the item's own padded text. Matches the visual
   language used by block selection (no fill flash, just a quiet
   outline). */
/* Phase 3 polish v5: softer selection ring — the previous
   ``2px solid + 2px offset`` accent outline read as too prominent
   relative to the rest of the layout. A 1.5 px line with reduced
   opacity + a lighter shadow keeps the selection clearly visible
   without dominating the canvas. */
.canvas-item.is-selected {
  outline: 1.5px solid rgba(46, 125, 108, 0.55);
  outline-offset: 2px;
  box-shadow: 0 1px 3px rgba(46, 125, 108, 0.10),
              0 3px 8px rgba(46, 125, 108, 0.08);
}
/* Slice 4G-2: move cursor on the cards / chip / strips so the
   user has a sensory hint that these surfaces drag the item.
   Resize handles override this per-corner below. */
.canvas-item.is-text-note,
.canvas-item.is-comment {
  cursor: move;
}
.canvas-item.is-simple-frame .canvas-item__handle,
.canvas-item.is-simple-frame .canvas-item__edge {
  cursor: move;
}
/* During an active gesture, drop the smooth color/shadow
   transitions so the ring doesn't visibly lag the pointer. */
.canvas-item.is-dragging,
.canvas-item.is-resizing {
  transition: none;
}

/* Slice 4G-2: body wrapper for text_note + comment. Holds the
   text content so resize handles can sit alongside it without
   being wiped by a textContent reassignment. Inherits font, color,
   and pre-wrap from the parent ``.canvas-item``. The block-level
   default sizing fills the parent's content box naturally.

   Slice 4G-3A polish: ``overflow: auto`` is on at all times (not
   just during edit) so the scrollbar's presence / text wrap don't
   shift between view and edit modes. ``scrollbar-gutter: stable``
   reserves the scrollbar lane even when no bar is showing, so the
   text width is identical with and without a scrollbar — the
   visible text never re-wraps when content grows. Custom thin
   scrollbar styling keeps the bar quiet at rest. */
.canvas-item__body {
  width: 100%;
  height: 100%;
  pointer-events: inherit;
  white-space: inherit;
  overflow: auto;
  scrollbar-gutter: stable;
  /* Phase 3 polish: long unbroken strings (e.g. URLs, or
     no-space typed text) must wrap inside the body. Without
     this, ``white-space: pre-wrap`` only breaks at whitespace,
     so a long token typed without spaces renders as ONE LONG
     LINE that overflows horizontally after reload. The
     contenteditable wraps it visually during editing (the
     browser's own soft-wrap), but ``textContent`` doesn't carry
     soft-break markers, so on next render the wrap disappears.
     ``overflow-wrap: anywhere`` forces breaks inside long
     tokens at any character boundary when needed. */
  overflow-wrap: anywhere;
  word-break: break-word;
  /* User cannot select text yet (no inline editing in 4G-2). */
  user-select: none;
}

/* Slice 4G-2: invisible border strips for simple_frame. Each
   strip is a thin hit-zone overlaying one side of the dashed
   ring. The 12 px corner gaps belong to the resize handles, so
   middle-edge drag = move, corner drag = resize, no ambiguity.
   Strips are transparent (no visible appearance) — the dashed
   border on the parent does all the visual work. */
.canvas-item__edge {
  position: absolute;
  background: transparent;
  pointer-events: auto;
  z-index: 1;
}
.canvas-item__edge--top    { top: 0;    left: 12px; right: 12px;  height: 12px; }
.canvas-item__edge--right  { top: 12px; right: 0;   bottom: 12px; width: 12px; }
.canvas-item__edge--bottom { bottom: 0; left: 12px; right: 12px;  height: 12px; }
.canvas-item__edge--left   { top: 12px; left: 0;    bottom: 12px; width: 12px; }

/* Slice 4G-3A: inline-edit state. Applied to the parent
   ``.canvas-item`` while exactly one of its text children
   (``.canvas-item__body`` for text_note/comment, the
   ``.canvas-item__handle`` chip for simple_frame) is the active
   contenteditable target. Goals:
     - resize handles disappear (cannot be accidentally grabbed);
     - frame edge strips become inert (cannot start a move drag);
     - the parent itself drops ``cursor: move`` — the editable child
       now owns cursor behaviour with ``cursor: text``;
     - the editable child opts back into native text selection.

   Phase 1 polish (2026-05-28): the inset accent outline that
   previously decorated the editable body during edit has been
   removed. It caused a visible inward shift of the text content
   on edit-enter (the outline-offset:-1px + the visible ring made
   the body look smaller). The parent's ``.is-selected`` outline +
   the native caret are now the sole edit signal. */
.canvas-item.is-editing {
  cursor: default;
  transition: none;
}
.canvas-item.is-editing .canvas-item__resize-handle {
  display: none;
}
.canvas-item.is-editing .canvas-item__edge {
  pointer-events: none;
}
.canvas-item.is-editing .canvas-item__body,
.canvas-item.is-editing .canvas-item__handle {
  user-select: text;
  cursor: text;
  /* Phase 3 polish v5: suppress the browser's default focus
     outline on the contenteditable region — the parent's
     ``.is-selected`` ring already signals what's active, and a
     second nested outline reads as visually noisy. */
  outline: none;
}
/* Frame chip in edit mode: allow it to grow vertically + wrap
   instead of clipping. Keeps the chip compact when not editing,
   but readable as a real input while typing. */
.canvas-item.is-simple-frame.is-editing .canvas-item__handle {
  white-space: pre-wrap;
  max-width: calc(100% - 16px);
}

/* Slice 4G-3A: placeholder for empty bodies. ``:empty::before``
   renders the per-type hint text (set on ``data-placeholder`` by
   ``applyCanvasItemNode``). Hidden during edit so a freshly-
   focused empty body shows the caret, not the hint. */
.canvas-item__body:empty::before,
.canvas-item__handle:empty::before {
  content: attr(data-placeholder);
  opacity: 0.45;
  font-style: italic;
  pointer-events: none;
  user-select: none;
}
.canvas-item.is-editing .canvas-item__body:empty::before,
.canvas-item.is-editing .canvas-item__handle:empty::before {
  content: none;
}

/* Slice 4H-4A polish (Issue 2): suppress the empty-body
   placeholder for Text notes entirely. The placeholder hint
   text was useful for Comment (which has structural chrome
   surrounding the body) but read as "stale fake content" on
   Text notes that the user intends to leave blank — e.g. a
   Text note repurposed as a colored shape/card. Selection +
   move + edit still work because the wrapper itself remains
   hittable. Comment placeholder is preserved (no change to
   `.is-comment` rule above). Frame title placeholder is also
   preserved. */
.canvas-item.is-text-note .canvas-item__body:empty::before {
  content: none;
}

/* Slice 4G-3A Phase 1: thin scrollbar styling for the canvas-item
   body. ``overflow: auto`` is on at all times so the scrollbar
   gutter doesn't reserve space differently between view and edit
   modes (see ``.canvas-item__body`` for the ``scrollbar-gutter:
   stable`` declaration). These rules keep the bar quiet — a thin
   muted track that's visible but doesn't shout. WebKit and
   Firefox use different syntaxes, both declared below. */
.canvas-item__body {
  scrollbar-width: thin;
  scrollbar-color: rgba(60, 74, 67, 0.22) transparent;
}
.canvas-item__body::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}
.canvas-item__body::-webkit-scrollbar-track {
  background: transparent;
}
.canvas-item__body::-webkit-scrollbar-thumb {
  background: rgba(60, 74, 67, 0.22);
  border-radius: 3px;
}
.canvas-item__body::-webkit-scrollbar-thumb:hover {
  background: rgba(60, 74, 67, 0.42);
}
html.wf-theme-night .canvas-item__body {
  scrollbar-color: rgba(231, 239, 233, 0.22) transparent;
}
html.wf-theme-night .canvas-item__body::-webkit-scrollbar-thumb {
  background: rgba(231, 239, 233, 0.22);
}
html.wf-theme-night .canvas-item__body::-webkit-scrollbar-thumb:hover {
  background: rgba(231, 239, 233, 0.42);
}

/* Slice 4G-3A polish: toolchip row ABOVE the canvas item.
   Mirrors the LNM ``.wf-motion__box`` pattern
   (sidebar-nav.css:787-799) — a 32×32 glass chip with surface
   bg, white-ish border, soft shadow, and backdrop blur — but
   WITHOUT the second-state hover expand (no ``__txt`` label
   that fades in). Single state.

   The chip row floats above the item's top edge (like LNM's
   hidden-button reveal pattern). When an item carries multiple
   chips (e.g. simple_frame has Colors + Close), they sit
   side-by-side at the top, right-aligned. */
.canvas-item__chip-row {
  position: absolute;
  /* Slice 4H-4B anchor pass: keep the chip-row's BOTTOM edge a
     constant 10 screen-px above the item's top regardless of zoom.
     box.bottom_stage = -10 / scale  ⇒  box.top_stage = -10/scale - 32
     (32 = chip CSS height that drives the flex container height).
     The stage transform multiplies back, leaving a stable 10-px
     visual gap. Replaces the old static `top: -42px`. */
  top: calc(-10px * var(--aw-canvas-ui-screen-px, 1) - 32px);
  right: 0;
  display: flex;
  gap: 6px;
  z-index: 8;
  pointer-events: none;
  opacity: 0;
  /* Slice 4H-4A polish (Issue 4) + 4H-4B anchor pass: chip-row
     composes the pre-existing reveal translateY animation WITH the
     canvas-zoom inverse scale so the close + colors chips stay
     approximately constant in screen size as the canvas zooms.
     `transform-origin: 100% 100%` anchors scaling at the bottom-
     right of the chip-row (which is glued to the wrapper's right
     edge by `right: 0`), so the scaled chip-row stays pinned to
     the wrapper's top-right corner regardless of zoom. The reveal
     translateY value is multiplied by `var(--screen-px)` so the
     4-px slide-in is a constant 4 screen-px at every zoom level. */
  transform: translateY(calc(4px * var(--aw-canvas-ui-screen-px, 1)))
             scale(var(--aw-canvas-ui-inverse-scale, 1));
  transform-origin: 100% 100%;
  transition: opacity .26s var(--ease, cubic-bezier(.32, .72, 0, 1)),
              transform .26s var(--ease, cubic-bezier(.32, .72, 0, 1));
}
.canvas-item.is-selected > .canvas-item__chip-row,
.canvas-item:hover > .canvas-item__chip-row {
  opacity: 1;
  transform: translateY(0) scale(var(--aw-canvas-ui-inverse-scale, 1));
  pointer-events: auto;
}

/* Slice 4H-5E: chip-row reachability bridge. The chip-row sits a
   constant 10 screen-px above the canvas-item's top edge (see the
   ``top: calc(-10px * screen-px - 32px)`` rule above). Without
   anything occupying that 10-px strip, the cursor traveling from
   the card up to the chips briefly enters a "dead zone" where no
   descendant catches pointer events: `.canvas-item:hover` turns
   OFF, the chip-row hides, and the chips become unreachable. A
   pseudo-element bridge fills the strip so hover survives the
   transit. The bridge is `pointer-events: none` by default — it
   does NOT initiate hover for nearby items hovering air-space
   above an unselected item — and flips to `pointer-events: auto`
   only while the same item is already hovered or selected. This
   matches the chip-row's own reveal rule.

   Arrows are excluded via :not(.is-arrow): they have
   `chip-row { display: none }` (see line ~3461) so there is no
   chip-row to reach, and an arrow's bounding box can span a
   large portion of the canvas — a 10-px strip atop a hovered
   arrow's bbox at the lifted z-index would silently intercept
   clicks on workflow blocks underneath. */
.canvas-item:not(.is-arrow)::before {
  content: "";
  position: absolute;
  top: calc(-10px * var(--aw-canvas-ui-screen-px, 1));
  left: 0;
  right: 0;
  height: calc(10px * var(--aw-canvas-ui-screen-px, 1));
  pointer-events: none;
}
.canvas-item:not(.is-arrow):hover::before,
.canvas-item:not(.is-arrow).is-selected::before {
  pointer-events: auto;
}

/* Slice 4H-5E: lift any interacting canvas-item above the
   neighbouring blocks/items so its chip-row (z-index 8 within
   the item's stacking context) renders ON TOP of everything
   on the canvas. Without this lift, a workflow `.block` at
   z-index 2 sitting visually in front of a canvas-item could
   obscure the item's hidden buttons even when revealed, because
   the chip-row's z-index is bounded by its parent's stacking
   context (the canvas-item itself at z-index 0/1/3 per type).
   The lift uses the same per-type specificity as the per-type
   z-index rules above so it actually wins without `!important`,
   and stays well below the floating chrome (toolbar at 42,
   header at 40+) so the chips never overlap the topbar/palette.
   Arrows are excluded for the same reason as the bridge above:
   they have no chip-row, so lifting their z-index would only
   produce a click-blocking band over neighbouring blocks.

   Slice 4H-5F: simple_frame is ALSO excluded. Lifting a frame to
   z-index 30 on hover/selection visibly pushed the frame's
   contained items/blocks BEHIND the frame's tinted body. v5F
   solves this differently: by removing the frame's per-type
   `z-index: 0` (above), the frame no longer creates a stacking
   context, so its chip-row at z-index 8 escapes into the
   canvas-stage context and renders above blocks WITHOUT
   needing to lift the whole frame. */
.canvas-item.is-text-note:hover,
.canvas-item.is-text-note.is-selected,
.canvas-item.is-comment:hover,
.canvas-item.is-comment.is-selected {
  z-index: 30;
}
.canvas-item.is-editing > .canvas-item__chip-row {
  opacity: 0;
  transform: translateY(calc(4px * var(--aw-canvas-ui-screen-px, 1)))
             scale(var(--aw-canvas-ui-inverse-scale, 1));
  pointer-events: none;
}

/* Individual chip (delete OR color picker) — shared visuals.
   Matches ``.wf-motion__box`` collapsed-state exactly:
     width:32px, surface bg, border, shadow, backdrop blur,
     border-radius:9px. */
.canvas-item__chip {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  margin: 0;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.55);
  border-radius: 9px;
  box-shadow: 0 2px 9px rgba(55, 72, 64, 0.10),
              0 1px 2px rgba(55, 72, 64, 0.06);
  -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  color: var(--muted, #6E8077);
  cursor: pointer;
  /* Slice 4H-5D: chip inherits pointer-events from its container
     (.canvas-item__chip-row). Previously this was `auto` always,
     which meant an invisible chip-row's chips were STILL pointer
     targets — hovering the air space above an unselected item
     fired the chip's :hover, propagated to .canvas-item:hover, and
     revealed the chip-row. With adjacent items, that air space
     could overlap a neighbour's visual card, so hovering the
     neighbour also revealed THIS item's hidden buttons. Inheriting
     fixes the bleed cleanly: chips are pointer-transparent when
     the chip-row is hidden (default) and pointer-active when the
     row reveals via the :hover/.is-selected rule above. */
  pointer-events: inherit;
  transition: color .12s, background-color .12s, border-color .12s;
}
.canvas-item__chip:hover {
  color: var(--label-hover, #27543C);
}
.canvas-item__chip svg {
  width: 16px;
  height: 16px;
  display: block;
  pointer-events: none;
}

/* Hide the delete chip entirely for foreign comments — the
   parent ``.is-foreign`` modifier removes the close affordance
   even when the row is otherwise revealed. */
.canvas-item.is-foreign .canvas-item__chip--delete {
  display: none;
}

html.wf-theme-night .canvas-item__chip {
  background: rgba(28, 36, 32, 0.88);
  border-color: rgba(255, 255, 255, 0.16);
  color: rgba(231, 239, 233, 0.66);
  box-shadow: 0 2px 9px rgba(0, 0, 0, 0.32),
              0 1px 2px rgba(0, 0, 0, 0.20);
}
html.wf-theme-night .canvas-item__chip:hover {
  color: rgba(150, 220, 200, 0.95);
}

/* Slice 4G-2: 4 corner resize handles. Always present in the DOM
   for every canvas item; CSS gates visibility on ``.is-selected``
   so unselected items never expose handles.

   Visual pattern adopted from LNM (``.wf-grip``) and CTM
   (``.wf-toolbar__resize``): a large invisible hit box that sits
   centred on the bounding-box corner with a small inner glyph
   (``.canvas-item__rico``) that fades in on hover or while the
   gesture is active. The glyph is opacity:0 + scale(.55) at rest
   and opacity:.85 + scale(1) on hover/active — same easing values
   as the chrome grips. Active handle gets ``.is-active`` from JS
   so the glyph stays lit even when the cursor races outside the
   hit box during a fast drag.

   ``pointer-events: auto`` opts the hit box back in for
   simple_frame (whose body is ``pointer-events: none``). Each
   corner uses the matching nwse/nesw resize cursor. */
.canvas-item__resize-handle {
  position: absolute;
  width: 28px;
  height: 28px;
  background: transparent;
  border: 0;
  padding: 0;
  box-sizing: border-box;
  pointer-events: auto;
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 7;
  touch-action: none;
  color: var(--muted, #7C9384);
}
/* Phase 3 redo: hover OR selection reveals the resize handles
   so the user doesn't need to click-first-then-resize. Matches
   the LNM ``.wf-grip`` reveal pattern (hover → grip icon fades
   in). */
.canvas-item.is-selected .canvas-item__resize-handle,
.canvas-item:hover .canvas-item__resize-handle {
  display: flex;
}
/* The original -22 px offsets mirrored CTM's ``.wf-toolbar__resize``
   exactly: a 28 px hit box positioned so the inner glyph sits ~8 px
   past the bounding-box corner, with a slim 6 px grab-zone overlap
   inside the parent. LNM uses the same pattern at a slightly larger
   scale (34 px box, -26 px offset).
   Slice 4H-4B anchor pass: the static -22 px offsets drifted as
   stage_scale * -22 px on screen (e.g. -44 screen-px at zoom 2x),
   floating the handles far away from item corners. Calibrated
   formula: position the box so its CENTER lands at a constant 8
   screen-px outside the item corner regardless of zoom.
     box.center_stage = -8 / stage_scale  (origin-50%-50% preserves it)
     box.top_stage    = box.center - 14   (28 px box, half = 14)
                      = -8 * var(--screen-px) - 14
   The stage transform multiplies back, leaving a stable
   handle-center-to-corner gap of 8 screen-px at every zoom. */
.canvas-item__resize-handle--nw {
  top:  calc(-8px * var(--aw-canvas-ui-screen-px, 1) - 14px);
  left: calc(-8px * var(--aw-canvas-ui-screen-px, 1) - 14px);
  cursor: nwse-resize;
}
.canvas-item__resize-handle--ne {
  top:   calc(-8px * var(--aw-canvas-ui-screen-px, 1) - 14px);
  right: calc(-8px * var(--aw-canvas-ui-screen-px, 1) - 14px);
  cursor: nesw-resize;
}
.canvas-item__resize-handle--se {
  bottom: calc(-8px * var(--aw-canvas-ui-screen-px, 1) - 14px);
  right:  calc(-8px * var(--aw-canvas-ui-screen-px, 1) - 14px);
  cursor: nwse-resize;
}
.canvas-item__resize-handle--sw {
  bottom: calc(-8px * var(--aw-canvas-ui-screen-px, 1) - 14px);
  left:   calc(-8px * var(--aw-canvas-ui-screen-px, 1) - 14px);
  cursor: nesw-resize;
}

/* Inner glyph — mirrors ``.wf-grip__ico`` / ``.wf-toolbar__rico``.
   Fades + scales in on hover or when JS adds ``.is-active`` to
   the parent handle (during a resize gesture). */
.canvas-item__rico {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  opacity: 0;
  transform: scale(.55);
  transform-origin: center;
  transition: opacity .18s var(--ease, cubic-bezier(.4, 0, .2, 1)),
              transform .18s var(--ease, cubic-bezier(.4, 0, .2, 1));
  pointer-events: none;
}
.canvas-item__rico svg {
  width: 14px;
  height: 14px;
  display: block;
}
.canvas-item__resize-handle:hover .canvas-item__rico,
.canvas-item__resize-handle.is-active .canvas-item__rico {
  opacity: .85;
  transform: scale(1);
}
.canvas-item__resize-handle.is-active {
  color: var(--accent, #2E7D6C);
}
/* NE / SW corners flip the NWSE-diagonal glyph horizontally so the
   arrows point along the NESW diagonal instead. The hover/active
   scale-up rule above flips its multiplier too, so both rest and
   active states stay mirrored. */
.canvas-item__resize-handle--ne .canvas-item__rico,
.canvas-item__resize-handle--sw .canvas-item__rico {
  transform: scale(.55) scaleX(-1);
}
.canvas-item__resize-handle--ne:hover .canvas-item__rico,
.canvas-item__resize-handle--ne.is-active .canvas-item__rico,
.canvas-item__resize-handle--sw:hover .canvas-item__rico,
.canvas-item__resize-handle--sw.is-active .canvas-item__rico {
  transform: scale(1) scaleX(-1);
}

/* --- Text note: white card, subtle border, gentle shadow --- */
.canvas-item.is-text-note {
  background: rgba(255, 255, 255, 0.95);
  border: 1px solid var(--border, rgba(60, 74, 67, 0.16));
  box-shadow: 0 1px 2px rgba(55, 72, 64, 0.06),
              0 4px 12px rgba(55, 72, 64, 0.06);
}

/* --- Slice 4H-4A: Text note style presets. Six preset families
   layered on top of the base `.is-text-note` rule so a Text note
   can also serve as a lightweight visual shape / card. Presets
   are class-based (no inline CSS, no free color picker) and
   preserve the pre-v4A visual exactly when applied at their
   "default" / current values. --- */

/* Fill (background) presets. */
.canvas-item.is-text-note.is-fill-default { background: rgba(255, 255, 255, 0.95); }
.canvas-item.is-text-note.is-fill-yellow  { background: #FFF9C4; }
.canvas-item.is-text-note.is-fill-green   { background: #DCEDC8; }
.canvas-item.is-text-note.is-fill-blue    { background: #BBDEFB; }
.canvas-item.is-text-note.is-fill-purple  { background: #E1BEE7; }
.canvas-item.is-text-note.is-fill-red     { background: #FFCDD2; }
.canvas-item.is-text-note.is-fill-none    { background: transparent; }
/* Slice 4H-4A polish (Issue 1): transparent fill drops the card
   shadow too — a note with `is-fill-none` should read as bare
   text on the canvas, not as an invisible card casting a
   shadow. Border / selection ring continue to apply unchanged. */
.canvas-item.is-text-note.is-fill-none    { box-shadow: none; }
html.wf-theme-night .canvas-item.is-text-note.is-fill-none { box-shadow: none; }
html.wf-theme-night .canvas-item.is-text-note.is-fill-default { background: rgba(28, 36, 32, 0.78); }
html.wf-theme-night .canvas-item.is-text-note.is-fill-yellow  { background: rgba(255, 235, 59, 0.18); }
html.wf-theme-night .canvas-item.is-text-note.is-fill-green   { background: rgba(76, 175, 80, 0.18); }
html.wf-theme-night .canvas-item.is-text-note.is-fill-blue    { background: rgba(33, 150, 243, 0.18); }
html.wf-theme-night .canvas-item.is-text-note.is-fill-purple  { background: rgba(156, 39, 176, 0.18); }
html.wf-theme-night .canvas-item.is-text-note.is-fill-red     { background: rgba(244, 67, 54, 0.18); }
html.wf-theme-night .canvas-item.is-text-note.is-fill-none    { background: transparent; }

/* Border color presets. */
.canvas-item.is-text-note.is-border-default { border-color: var(--border, rgba(60, 74, 67, 0.16)); }
.canvas-item.is-text-note.is-border-muted   { border-color: rgba(60, 74, 67, 0.40); }
.canvas-item.is-text-note.is-border-accent  { border-color: var(--accent, #0FA46A); }
.canvas-item.is-text-note.is-border-blue    { border-color: #2D7CDB; }
.canvas-item.is-text-note.is-border-orange  { border-color: #E08540; }
.canvas-item.is-text-note.is-border-red     { border-color: #D9534F; }
.canvas-item.is-text-note.is-border-purple  { border-color: #8B5CF6; }
html.wf-theme-night .canvas-item.is-text-note.is-border-default { border-color: rgba(255, 255, 255, 0.14); }
html.wf-theme-night .canvas-item.is-text-note.is-border-muted   { border-color: rgba(255, 255, 255, 0.30); }
html.wf-theme-night .canvas-item.is-text-note.is-border-accent  { border-color: var(--accent, #2DC78D); }

/* Border width presets. `0` removes the border entirely; the
   other three set widths directly. */
.canvas-item.is-text-note.is-bwidth-0 { border-width: 0; }
.canvas-item.is-text-note.is-bwidth-1 { border-width: 1px; }
.canvas-item.is-text-note.is-bwidth-2 { border-width: 2px; }
.canvas-item.is-text-note.is-bwidth-3 { border-width: 3px; }

/* Corner radius presets. Missing key → no class → CSS base 8 px
   from the `.canvas-item` rule remains in force, so legacy text
   notes render unchanged. */
.canvas-item.is-text-note.is-radius-0  { border-radius: 0; }
.canvas-item.is-text-note.is-radius-6  { border-radius: 6px; }
.canvas-item.is-text-note.is-radius-12 { border-radius: 12px; }
.canvas-item.is-text-note.is-radius-18 { border-radius: 18px; }
.canvas-item.is-text-note.is-radius-24 { border-radius: 24px; }

/* Font size presets. Applied to `.canvas-item__body` so the
   inline-editable content scales without affecting toolbar /
   chip-row chrome. Missing key → CSS base 13 px from the
   `.canvas-item` rule remains. */
.canvas-item.is-text-note.is-fsize-12 .canvas-item__body { font-size: 12px; }
.canvas-item.is-text-note.is-fsize-14 .canvas-item__body { font-size: 14px; }
.canvas-item.is-text-note.is-fsize-16 .canvas-item__body { font-size: 16px; }
.canvas-item.is-text-note.is-fsize-18 .canvas-item__body { font-size: 18px; line-height: 1.35; }
.canvas-item.is-text-note.is-fsize-22 .canvas-item__body { font-size: 22px; line-height: 1.30; }
.canvas-item.is-text-note.is-fsize-28 .canvas-item__body { font-size: 28px; line-height: 1.25; }

/* Font family presets — system font stacks only, no external
   font files. Applied to `.canvas-item__body` so toolbar +
   chip-row chrome stay on the wrapper's inherited stack. */
.canvas-item.is-text-note.is-ffamily-sans .canvas-item__body {
  font-family: inherit;
}
.canvas-item.is-text-note.is-ffamily-serif .canvas-item__body {
  font-family: Georgia, "Times New Roman", Times, serif;
}
.canvas-item.is-text-note.is-ffamily-mono .canvas-item__body {
  font-family: ui-monospace, "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace;
}

/* --- Slice 4H-4A: Text note style toolbar. Two horizontal rows
   stacked vertically. Anchored below the wrapper bbox by default
   (`.is-below`), flips above when JS detects viewport-bottom
   clip. Hidden by default and during inline edit; visible only
   when the parent is `.is-text-note.is-selected` AND NOT
   `.is-editing`. Glass-card surface matches the Arrow toolbar
   visual vocabulary. --- */
.canvas-item__text-toolbar {
  position: absolute;
  display: none;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid rgba(255, 255, 255, 0.55);
  box-shadow: 0 1px 2px rgba(55, 72, 64, 0.08),
              0 8px 24px rgba(55, 72, 64, 0.16),
              0 0 0 1px rgba(60, 74, 67, 0.06);
  -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  z-index: 5;
  pointer-events: auto;
  white-space: nowrap;
  color: var(--text, #1F2A26);
  user-select: none;
}
/* Slice 4H-4A polish (Issue 4): each placement variant composes
   the default translate with a `scale(var(--aw-canvas-ui-inverse-scale))`
   so the toolbar stays approximately constant in SCREEN size as
   the canvas zooms. `transform-origin` is set per-variant to the
   anchor point on the toolbar's own box (top-center for is-below,
   bottom-center for is-above) so the scale grows/shrinks around
   that anchor — keeping the visual anchor at the JS-set
   left/top stage-pixel coordinate. */
.canvas-item__text-toolbar.is-below {
  transform: translate(-50%, 0) scale(var(--aw-canvas-ui-inverse-scale, 1));
  transform-origin: 50% 0;
}
.canvas-item__text-toolbar.is-above {
  transform: translate(-50%, -100%) scale(var(--aw-canvas-ui-inverse-scale, 1));
  transform-origin: 50% 100%;
}
/* Slice 4H-4A polish (Issue 3): grip handle on the left edge of
   the toolbar. Mousedown on the grip starts a session-local
   reposition; preset-button mousedown is unaffected. The 3 dots
   glyph is the same visual vocabulary used elsewhere for drag
   surfaces. */
.canvas-item__text-toolbar-grip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 10px;
  height: 22px;
  margin-right: 2px;
  border-radius: 4px;
  color: rgba(60, 74, 67, 0.45);
  cursor: grab;
  flex-shrink: 0;
  user-select: none;
  touch-action: none;
}
.canvas-item__text-toolbar-grip:hover {
  color: rgba(60, 74, 67, 0.80);
  background: rgba(60, 74, 67, 0.06);
}
.canvas-item__text-toolbar-grip:active { cursor: grabbing; }
.canvas-item__text-toolbar-grip svg { display: block; pointer-events: none; }
html.wf-theme-night .canvas-item__text-toolbar-grip {
  color: rgba(231, 239, 233, 0.45);
}
html.wf-theme-night .canvas-item__text-toolbar-grip:hover {
  color: rgba(231, 239, 233, 0.80);
  background: rgba(231, 239, 233, 0.06);
}
/* Slice 4H-4C-1 (expanded UX): the text style toolbar no longer
   auto-shows on selection. The `.is-style-open` class is added to
   the canvas-item only when the user clicks the "Aa" chip in the
   chip-row. Requiring BOTH classes here keeps the toolbar hidden
   on every plain click and surfaces it only on an explicit user
   gesture. See the matching `.is-style-open` chip + multi-mode
   hover rules near the end of this file. */
.canvas-item.is-text-note.is-selected.is-style-open:not(.is-editing) .canvas-item__text-toolbar {
  display: inline-flex;
}
.canvas-item__text-toolbar-row {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}
.canvas-item__text-toolbar-group {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}
.canvas-item__text-toolbar-sep {
  width: 1px;
  height: 18px;
  margin: 0 4px;
  background: rgba(60, 74, 67, 0.18);
}
.canvas-item__text-tool-btn {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 2px;
  margin: 0;
  min-width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: inherit;
  line-height: 0;
  font-size: 11px;
  transition: background-color 80ms, border-color 80ms;
}
.canvas-item__text-tool-btn:hover {
  background: rgba(60, 74, 67, 0.08);
}
.canvas-item__text-tool-btn.is-active {
  background: rgba(46, 125, 108, 0.12);
  border-color: rgba(46, 125, 108, 0.45);
}
.canvas-item__text-tool-btn svg { display: block; pointer-events: none; }

/* Fill swatches mirror the corresponding `.is-fill-*` color. The
   `.is-fill-none` swatch shows a checkerboard hint. */
.canvas-item__text-swatch-dot {
  display: block;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 1px solid rgba(60, 74, 67, 0.18);
}
.canvas-item__text-swatch.is-fill-default .canvas-item__text-swatch-dot { background: rgba(255, 255, 255, 0.95); }
.canvas-item__text-swatch.is-fill-yellow  .canvas-item__text-swatch-dot { background: #FFF9C4; }
.canvas-item__text-swatch.is-fill-green   .canvas-item__text-swatch-dot { background: #DCEDC8; }
.canvas-item__text-swatch.is-fill-blue    .canvas-item__text-swatch-dot { background: #BBDEFB; }
.canvas-item__text-swatch.is-fill-purple  .canvas-item__text-swatch-dot { background: #E1BEE7; }
.canvas-item__text-swatch.is-fill-red     .canvas-item__text-swatch-dot { background: #FFCDD2; }
.canvas-item__text-swatch.is-fill-none    .canvas-item__text-swatch-dot {
  background:
    linear-gradient(45deg, rgba(60,74,67,0.25) 25%, transparent 25%, transparent 75%, rgba(60,74,67,0.25) 75%),
    linear-gradient(45deg, rgba(60,74,67,0.25) 25%, transparent 25%, transparent 75%, rgba(60,74,67,0.25) 75%);
  background-position: 0 0, 4px 4px;
  background-size: 8px 8px;
}
/* Border swatches use a ring glyph at the preset's color. */
.canvas-item__text-swatch-ring {
  display: block;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: transparent;
  border: 2px solid rgba(60, 74, 67, 0.40);
}
.canvas-item__text-swatch.is-border-swatch-default .canvas-item__text-swatch-ring { border-color: var(--border, rgba(60, 74, 67, 0.16)); }
.canvas-item__text-swatch.is-border-swatch-muted   .canvas-item__text-swatch-ring { border-color: rgba(60, 74, 67, 0.55); }
.canvas-item__text-swatch.is-border-swatch-accent  .canvas-item__text-swatch-ring { border-color: var(--accent, #0FA46A); }
.canvas-item__text-swatch.is-border-swatch-blue    .canvas-item__text-swatch-ring { border-color: #2D7CDB; }
.canvas-item__text-swatch.is-border-swatch-orange  .canvas-item__text-swatch-ring { border-color: #E08540; }
.canvas-item__text-swatch.is-border-swatch-red     .canvas-item__text-swatch-ring { border-color: #D9534F; }
.canvas-item__text-swatch.is-border-swatch-purple  .canvas-item__text-swatch-ring { border-color: #8B5CF6; }

/* Font-size buttons render the numeric value as a tiny label. */
.canvas-item__text-fsize-label {
  display: inline-block;
  min-width: 18px;
  text-align: center;
  font-size: 11px;
  line-height: 1;
  font-weight: 500;
}
/* Font-family buttons render an "Aa" glyph in the corresponding
   font stack so the visual cue matches the resulting font. */
.canvas-item__text-ffamily-label {
  display: inline-block;
  min-width: 18px;
  text-align: center;
  font-size: 12px;
  line-height: 1;
  font-weight: 500;
}
.canvas-item__text-ffamily-label.is-ffamily-sans  { font-family: inherit; }
.canvas-item__text-ffamily-label.is-ffamily-serif { font-family: Georgia, "Times New Roman", Times, serif; }
.canvas-item__text-ffamily-label.is-ffamily-mono  { font-family: ui-monospace, "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace; }

/* Night-theme variants for the toolbar surface + buttons. */
html.wf-theme-night .canvas-item__text-toolbar {
  background: rgba(31, 42, 38, 0.96);
  border-color: rgba(255, 255, 255, 0.10);
  color: var(--text, #E7EFE9);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.30),
              0 8px 24px rgba(0, 0, 0, 0.40),
              0 0 0 1px rgba(255, 255, 255, 0.06);
}
html.wf-theme-night .canvas-item__text-toolbar-sep {
  background: rgba(231, 239, 233, 0.18);
}
html.wf-theme-night .canvas-item__text-tool-btn:hover {
  background: rgba(231, 239, 233, 0.08);
}
html.wf-theme-night .canvas-item__text-tool-btn.is-active {
  background: rgba(45, 199, 141, 0.18);
  border-color: rgba(45, 199, 141, 0.55);
}
html.wf-theme-night .canvas-item__text-swatch-dot,
html.wf-theme-night .canvas-item__text-swatch-ring {
  border-color: rgba(231, 239, 233, 0.20);
}

/* --- Slice 4H-3A: Arrow tool — visual annotation only, NOT a
   workflow execution edge. The bounding box is transparent (no
   background, no border, no shadow) so the arrow reads as a
   pure annotation glyph rather than a card. Color is a neutral
   muted text tone, deliberately distinct from the accent that
   workflow connection wires use. The inline ``.canvas-item__arrow``
   SVG fills the bounding box and is pointer-events: none so it
   never intercepts clicks meant for the wrapper (the wrapper
   still owns selection / move / resize via the existing
   .canvas-item event handlers). The selection ring continues to
   come from the shared .canvas-item.is-selected rule above. --- */
.canvas-item.is-arrow {
  background: transparent;
  border: none;
  box-shadow: none;
  color: var(--text-dim, rgba(60, 74, 67, 0.70));
  /* Slice 4H-3B v2: the wrapper itself is pointer-inert so its
     diagonal-bounding-box transparent area does NOT block clicks
     on items underneath. Interactive children opt back in:
       - the SVG (`.canvas-item__arrow`) catches clicks on its
         visible stroke + on the wider invisible hit-line built
         into `_buildArrowSvgMarkup`
       - resize handles + chip-row use their existing
         `pointer-events: auto`, redeclared below to be defensive
         against future rule re-ordering
       - endpoint handles use their existing selection-gated
         `pointer-events: auto` rule already in this file
     Combined with `z-index: 3` above (annotation overlay), the
     Arrow visually sits ABOVE blocks but doesn't intercept clicks
     anywhere except on the visible stroke + the handles. */
  pointer-events: none;
}
.canvas-item.is-arrow .canvas-item__arrow {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* Slice 4H-3B v5: SVG root is pointer-inert. Earlier (v2)
     reasoning relied on SVG `pointer-events: visiblePainted`
     defaults applying to the `<svg>` root — but when an `<svg>`
     is embedded in HTML, browsers treat the root box as a
     full-rectangle HTML hit target under `pointer-events: auto`,
     not as `visiblePainted`. That made the empty bbox area click-
     selectable in practice. v5 flips the root to `pointer-events:
     none` and explicitly opts each child stroke element back in
     with its own `pointer-events` attribute (see
     `_buildArrowSvgMarkup`). The defensive wrapper mousedown
     guard in `renderCanvasItem` further filters `e.target` so a
     stray SVG-root hit can never start a whole-item drag. */
  pointer-events: none;
  display: block;
}
.canvas-item.is-arrow .canvas-item__resize-handle,
.canvas-item.is-arrow .canvas-item__chip-row {
  /* Slice 4H-3B v2: defensive re-declaration so the wrapper's
     `pointer-events: none` never accidentally inerts the resize
     grips or the close chip. These already have effective auto
     from their own per-class rules (resize handle hit box at
     lines 2737-2740, chip row default), but re-asserting here
     pairs the layering change with explicit intent. v3 below
     additionally hides both for Arrows entirely (display: none)
     so the line-annotation selection chrome doesn't read as a
     rectangle. The pointer-events rules stay as a safe baseline
     in case a future variant un-hides them. */
  pointer-events: auto;
}
html.wf-theme-night .canvas-item.is-arrow {
  color: var(--text-dim, rgba(231, 239, 233, 0.70));
}

/* --- Slice 4H-3B v3: Arrow selection chrome — line-centric,
   not rectangle-centric. Three coordinated overrides:

   1. Suppress the generic ``.canvas-item.is-selected`` outline +
      box-shadow ring for Arrows. The ring is correct visual
      feedback for a rectangular card (text_note / comment /
      simple_frame) but reads as "selected rectangle" around a
      line, which is the wrong mental model for a line
      annotation.

   2. Hide the four corner resize handles for Arrows entirely.
      Endpoint handles (start / end circles) are the correct
      resize / edit affordance for a line — a corner resize on
      an arrow has no semantically meaningful behavior beyond
      what endpoint handles already provide. The selector
      includes :hover and .is-selected because the generic
      reveal rules at lines 2739-2741 explicitly un-hide on
      those states; without the .is-arrow override those rules
      would still surface the corners.

   3. Hide the chip-row (close chip) for Arrows. The chip-row
      is anchored at top: -42px relative to the bounding box,
      which for a long diagonal arrow floats it far from the
      visible line and reinforces the "I am a rectangle" feel.
      Delete / Backspace removes the selected Arrow — same
      mechanism the keyboard handler in app.js already uses for
      every selected canvas item. If a future styling slice
      adds an arrow-specific mini-toolbar, that toolbar can
      anchor near the line midpoint.

   Visual selection feedback: the wrapper's ``color`` flips to
   ``var(--accent, ...)`` so the SVG ``stroke="currentColor"``
   shaft + arrowhead turn accent green. Coherent with the
   endpoint handles, which already paint in accent. No filter
   / drop-shadow to keep the per-rAF endpoint-edit redraw cheap.
   Night-theme variant covers dark mode. --- */
.canvas-item.is-arrow.is-selected {
  outline: none;
  box-shadow: none;
  /* Slice 4H-3C: the accent-on-selection color override was
     removed. Stroke color is now user-controlled via the
     `.is-stroke-{preset}` classes below, so flipping to accent on
     every selection would clobber the user's choice. Selection
     feedback now comes from the endpoint handles + the floating
     style toolbar appearing under `.is-arrow.is-selected` — both
     are clearly arrow-specific affordances. The outline /
     box-shadow zeroing stays so a selected Arrow still doesn't
     read as a selected rectangle (v3 line-centric chrome). */
}
.canvas-item.is-arrow .canvas-item__resize-handle,
.canvas-item.is-arrow:hover .canvas-item__resize-handle,
.canvas-item.is-arrow.is-selected .canvas-item__resize-handle {
  display: none;
}
.canvas-item.is-arrow .canvas-item__chip-row {
  display: none;
}

/* --- Slice 4H-3B: Arrow endpoint handles. Two small draggable
   circles anchored on the arrow start / end points. Visible only
   when the parent .canvas-item.is-arrow is .is-selected; otherwise
   hidden + pointer-events:none so an unselected arrow never
   intercepts clicks on its endpoint positions. `margin: -7px 0 0
   -7px` centers the 12 px circle on the (left, top) coord written
   in JS (the absolute pixel position of the endpoint within the
   item's bounding box). The 2 px white border + soft shadow match
   the visual language of the Frame title-handle chip and resize
   grips. Hover/active state slightly scales up for affordance.
   Z-index sits above the SVG (which is pointer-events:none anyway)
   and above the resize handles so endpoint editing wins for clicks
   that fall on the handle. --- */
.canvas-item__arrow-handle {
  position: absolute;
  width: 12px;
  height: 12px;
  margin: -7px 0 0 -7px;
  border-radius: 50%;
  background: var(--accent, #0FA46A);
  border: 2px solid #FFFFFF;
  box-shadow: 0 1px 2px rgba(55, 72, 64, 0.20),
              0 0 0 1px rgba(60, 74, 67, 0.10);
  pointer-events: none;
  opacity: 0;
  z-index: 4;
  cursor: grab;
  transition: opacity 100ms, transform 100ms;
}
.canvas-item.is-arrow.is-selected .canvas-item__arrow-handle {
  opacity: 1;
  pointer-events: auto;
}
.canvas-item.is-arrow.is-selected .canvas-item__arrow-handle:hover,
.canvas-item.is-arrow.is-selected .canvas-item__arrow-handle.is-active {
  cursor: grabbing;
  transform: scale(1.18);
}
html.wf-theme-night .canvas-item__arrow-handle {
  background: var(--accent, #2DC78D);
  border-color: #1F2A26;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.40),
              0 0 0 1px rgba(231, 239, 233, 0.10);
}

/* --- Slice 4H-3C: Arrow stroke color presets. Each preset is a
   class on the `.canvas-item.is-arrow` wrapper; the SVG's
   `stroke="currentColor"` picks up the resolved `color` via
   inheritance. `muted` is the v1/v3 default — its rule lives
   here AFTER the v3 `.is-arrow` base rule so the preset key
   always wins, even for legacy arrows that resolve to "muted"
   via `_arrowStrokePreset`. Per-class color values use the
   project accent variable where appropriate; hex fallbacks
   ensure visibility when CSS variables aren't loaded. --- */
.canvas-item.is-arrow.is-stroke-muted,
.canvas-item__arrow-swatch.is-stroke-muted  { color: var(--text-dim, rgba(60, 74, 67, 0.70)); }
.canvas-item.is-arrow.is-stroke-accent,
.canvas-item__arrow-swatch.is-stroke-accent { color: var(--accent, #0FA46A); }
.canvas-item.is-arrow.is-stroke-blue,
.canvas-item__arrow-swatch.is-stroke-blue   { color: #2D7CDB; }
.canvas-item.is-arrow.is-stroke-orange,
.canvas-item__arrow-swatch.is-stroke-orange { color: #E08540; }
.canvas-item.is-arrow.is-stroke-red,
.canvas-item__arrow-swatch.is-stroke-red    { color: #D9534F; }
.canvas-item.is-arrow.is-stroke-purple,
.canvas-item__arrow-swatch.is-stroke-purple { color: #8B5CF6; }
.canvas-item.is-arrow.is-stroke-dark,
.canvas-item__arrow-swatch.is-stroke-dark   { color: #2E3A33; }
html.wf-theme-night .canvas-item.is-arrow.is-stroke-muted,
html.wf-theme-night .canvas-item__arrow-swatch.is-stroke-muted  { color: var(--text-dim, rgba(231, 239, 233, 0.70)); }
html.wf-theme-night .canvas-item.is-arrow.is-stroke-accent,
html.wf-theme-night .canvas-item__arrow-swatch.is-stroke-accent { color: var(--accent, #2DC78D); }
html.wf-theme-night .canvas-item.is-arrow.is-stroke-dark,
html.wf-theme-night .canvas-item__arrow-swatch.is-stroke-dark   { color: #D0D7D3; }

/* --- Slice 4H-3C: Arrow style toolbar. A small floating row that
   appears near the Arrow when selected — v3C v2 places it BELOW
   the lower endpoint by default with a 14 px gap, falling back
   to ABOVE the upper endpoint when below would clip the
   viewport bottom. Hosts four preset groups (color / width /
   pattern / arrowhead) separated by thin vertical dividers.
   Hidden by default (`display: none`); `.canvas-item.is-arrow.is-selected`
   flips it to `inline-flex`. Positioned absolutely relative to
   the arrow wrapper; `left` + `top` are set in JS each render
   from the line midpoint + the lower/upper endpoint pixel coord.
   The placement direction is set by the `.is-below` / `.is-above`
   variant classes (transform `translate(-50%, 0)` or
   `translate(-50%, -100%)`) — see the JS placement block in
   `applyCanvasItemNode`. The wrapper has `pointer-events: none`
   (v2), so the toolbar re-asserts `pointer-events: auto`. Glass-
   card surface matches the canvas-item card vocabulary used
   elsewhere. --- */
.canvas-item__arrow-toolbar {
  position: absolute;
  display: none;
  /* Slice 4H-3C v2: transform direction is set by the placement
     class (`.is-below` / `.is-above`) so the JS-computed `top`
     value (in CSS pixels relative to the wrapper) stays semantic
     — `top` is the BOTTOM edge of the toolbar when above, and
     the TOP edge when below. JS picks the class per render
     based on viewport bounds + arrow visual extent. */
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.96);
  border: 1px solid rgba(255, 255, 255, 0.55);
  box-shadow: 0 1px 2px rgba(55, 72, 64, 0.08),
              0 8px 24px rgba(55, 72, 64, 0.16),
              0 0 0 1px rgba(60, 74, 67, 0.06);
  -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  z-index: 5;
  pointer-events: auto;
  white-space: nowrap;
  color: var(--text, #1F2A26);
  user-select: none;
}
/* Slice 4H-3C v2: placement variants. `is-below` (default) sits
   under the lower endpoint with the JS-set `top` value as the
   toolbar's TOP edge; `is-above` flips upward so the JS-set
   `top` value is the BOTTOM edge (translate-Y -100%). Horizontal
   centering is the same in both: translate-X -50% so the
   JS-set `left` aligns to the line midpoint. */
/* Slice 4H-4A polish (Issue 4): zoom-stable scaling. Compose the
   default translate with `scale(var(--aw-canvas-ui-inverse-scale))`
   and set `transform-origin` to the visual anchor on the
   toolbar's own box (top-center for is-below, bottom-center for
   is-above) so the toolbar stays approximately constant in SCREEN
   size as the canvas zooms while its anchor stays glued to the
   JS-set stage-pixel coordinate. */
.canvas-item__arrow-toolbar.is-below {
  transform: translate(-50%, 0) scale(var(--aw-canvas-ui-inverse-scale, 1));
  transform-origin: 50% 0;
}
.canvas-item__arrow-toolbar.is-above {
  transform: translate(-50%, -100%) scale(var(--aw-canvas-ui-inverse-scale, 1));
  transform-origin: 50% 100%;
}
.canvas-item.is-arrow.is-selected .canvas-item__arrow-toolbar {
  display: inline-flex;
}
.canvas-item__arrow-toolbar-group {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}
.canvas-item__arrow-toolbar-sep {
  width: 1px;
  height: 18px;
  margin: 0 4px;
  background: rgba(60, 74, 67, 0.18);
}
.canvas-item__arrow-tool-btn {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 2px;
  margin: 0;
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: inherit;
  line-height: 0;
  transition: background-color 80ms, border-color 80ms, transform 80ms;
}
.canvas-item__arrow-tool-btn:hover {
  background: rgba(60, 74, 67, 0.08);
}
.canvas-item__arrow-tool-btn.is-active {
  background: rgba(46, 125, 108, 0.12);
  border-color: rgba(46, 125, 108, 0.45);
}
.canvas-item__arrow-tool-btn svg {
  display: block;
  pointer-events: none;
}
/* --- Color swatches: pure CSS, no glyph. The .is-stroke-* class
   on the swatch shares the same rule with the wrapper above, so
   the swatch dot is filled with the corresponding preset color.
   We render the inner dot via a small span to keep the button
   focus ring on the OUTER 24x24 box while showing a 12x12 colored
   circle inside. --- */
.canvas-item__arrow-swatch {
  /* Swatch button extends the base tool button — same hit box. */
}
.canvas-item__arrow-swatch-dot {
  display: block;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: currentColor;
  border: 1px solid rgba(60, 74, 67, 0.18);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.20);
}
.canvas-item__arrow-swatch.is-active .canvas-item__arrow-swatch-dot {
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55),
              0 0 0 2px rgba(46, 125, 108, 0.45);
}
/* Night-theme variants for the toolbar surface + buttons. */
html.wf-theme-night .canvas-item__arrow-toolbar {
  background: rgba(31, 42, 38, 0.96);
  border-color: rgba(255, 255, 255, 0.10);
  color: var(--text, #E7EFE9);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.30),
              0 8px 24px rgba(0, 0, 0, 0.40),
              0 0 0 1px rgba(255, 255, 255, 0.06);
}
html.wf-theme-night .canvas-item__arrow-toolbar-sep {
  background: rgba(231, 239, 233, 0.18);
}
html.wf-theme-night .canvas-item__arrow-tool-btn:hover {
  background: rgba(231, 239, 233, 0.08);
}
html.wf-theme-night .canvas-item__arrow-tool-btn.is-active {
  background: rgba(45, 199, 141, 0.18);
  border-color: rgba(45, 199, 141, 0.55);
}
html.wf-theme-night .canvas-item__arrow-swatch-dot {
  border-color: rgba(231, 239, 233, 0.20);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.30);
}

/* --- Slice 4H-3A v2: drag-to-draw preview overlay. Lives directly
   under .canvas-stage so it inherits the same stage transform and
   uses stage-local coordinates without conversion. Pointer-events
   none so it doesn't intercept the in-flight drag. Slight opacity +
   muted color keeps the preview visually distinct from a committed
   arrow without flashing solid color on every mousemove tick. --- */
.aw-arrow-preview {
  position: absolute;
  pointer-events: none;
  color: var(--text-dim, rgba(60, 74, 67, 0.70));
  opacity: 0.7;
}
.aw-arrow-preview svg {
  width: 100%;
  height: 100%;
  display: block;
}
html.wf-theme-night .aw-arrow-preview {
  color: var(--text-dim, rgba(231, 239, 233, 0.70));
}

/* --- Comment: white glass card, same palette as text_note +
   the LNM (sidebar-nav.css ``--surface`` / ``--border``) ---
   The user explicitly asked for comments to match the text_note
   and the LNM surface tone. ``rgba(255,255,255,0.92)`` mirrors
   the text_note card; the subtle ``rgba(255,255,255,0.55)``
   border + a soft shadow match the LNM glass language. */
.canvas-item.is-comment {
  background: rgba(255, 255, 255, 0.92);
  color: #1F2A26;
  border: 1px solid rgba(255, 255, 255, 0.55);
  box-shadow: 0 1px 2px rgba(55, 72, 64, 0.08),
              0 6px 16px rgba(55, 72, 64, 0.10),
              0 0 0 1px rgba(60, 74, 67, 0.06);
  -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
}

/* --- Simple frame: dashed outline, pale tinted background,
       title in top-left ---
   Phase 3 polish: each frame now has a colored background +
   matching dashed border. The repeat-group inspired diagonal
   stripe pattern (components.css:1097-1106) keeps the area
   visually distinct without competing with enclosed blocks. The
   color comes from a per-frame ``.is-color-*`` class applied by
   JS based on ``style.color``. Base rules below define the
   structure; per-color overrides further down set the tint hues. */
.canvas-item.is-simple-frame {
  --frame-tint: rgba(46, 125, 108, 1);          /* default teal */
  --frame-tint-bg-a: rgba(46, 125, 108, 0.06);  /* base wash */
  --frame-tint-bg-b: rgba(46, 125, 108, 0.10);  /* stripe wash */
  --frame-tint-border: rgba(46, 125, 108, 0.45);
  --frame-tint-chip-bg: rgba(46, 125, 108, 0.12);
  --frame-tint-chip-fg: rgba(46, 125, 108, 1);
  background:
    repeating-linear-gradient(
      135deg,
      var(--frame-tint-bg-a) 0,
      var(--frame-tint-bg-a) 10px,
      var(--frame-tint-bg-b) 10px,
      var(--frame-tint-bg-b) 20px
    );
  border: 1.5px dashed var(--frame-tint-border);
  color: rgba(60, 74, 67, 0.75);
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 0.02em;
  /* The frame body is decorative — the title lives in a
     ``.canvas-item__handle`` child positioned at the top-left.
     The frame's interior stays ``pointer-events: none`` so it
     never captures clicks meant for blocks inside the frame or
     for canvas pan. Children that need pointer events (handle,
     edges, picker, resize handles) opt back in individually. */
  pointer-events: none;
  padding: 6px 10px;
  border-radius: 12px;
  /* Allow the user's eye to focus on enclosed blocks; the body
     text shouldn't compete. */
  align-content: flex-start;
}
/* Frame title-handle: only frame-typed canvas items render one.
   Tinted with the frame's color via CSS vars so the chip reads
   as belonging to the frame. */
.canvas-item.is-simple-frame .canvas-item__handle {
  position: absolute;
  /* Slice 4H-4B anchor pass: keep the chip's TOP-LEFT visually
     anchored 4 screen-px down and 8 screen-px right of the frame's
     top-left corner regardless of zoom. With transform-origin 0 0
     (top-left), scale preserves the visual top-left → setting the
     CSS top/left to (N/scale) stage-local px produces N screen-px
     after the stage transform multiplies back. Replaces the
     static `top: 4px; left: 8px` that drifted as stage_scale*N
     screen-px (e.g. 8/16 instead of 4/8 at zoom 2x). */
  top:  calc(4px * var(--aw-canvas-ui-screen-px, 1));
  left: calc(8px * var(--aw-canvas-ui-screen-px, 1));
  max-width: calc(100% - 16px);
  padding: 2px 8px;
  border-radius: 6px;
  background: var(--frame-tint-chip-bg);
  color: var(--frame-tint-chip-fg);
  pointer-events: auto;
  font-weight: 600;
  font-size: 12px;
  line-height: 1.35;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: move;
}

/* ----- Per-color rules. Each sets the four CSS vars for the
   matching ``.is-color-*`` class. The CSS-var pattern means the
   structural rules above don't need to be duplicated per color. */
.canvas-item.is-simple-frame.is-color-teal {
  --frame-tint: #2E7D6C;
  --frame-tint-bg-a: rgba(46, 125, 108, 0.05);
  --frame-tint-bg-b: rgba(46, 125, 108, 0.10);
  --frame-tint-border: rgba(46, 125, 108, 0.45);
  --frame-tint-chip-bg: rgba(46, 125, 108, 0.14);
  --frame-tint-chip-fg: #1F4D3F;
}
.canvas-item.is-simple-frame.is-color-blue {
  --frame-tint: #3B82F6;
  --frame-tint-bg-a: rgba(59, 130, 246, 0.05);
  --frame-tint-bg-b: rgba(59, 130, 246, 0.10);
  --frame-tint-border: rgba(59, 130, 246, 0.45);
  --frame-tint-chip-bg: rgba(59, 130, 246, 0.14);
  --frame-tint-chip-fg: #1E40AF;
}
.canvas-item.is-simple-frame.is-color-purple {
  --frame-tint: #8B5CF6;
  --frame-tint-bg-a: rgba(139, 92, 246, 0.05);
  --frame-tint-bg-b: rgba(139, 92, 246, 0.10);
  --frame-tint-border: rgba(139, 92, 246, 0.45);
  --frame-tint-chip-bg: rgba(139, 92, 246, 0.14);
  --frame-tint-chip-fg: #5B21B6;
}
.canvas-item.is-simple-frame.is-color-pink {
  --frame-tint: #EC4899;
  --frame-tint-bg-a: rgba(236, 72, 153, 0.05);
  --frame-tint-bg-b: rgba(236, 72, 153, 0.10);
  --frame-tint-border: rgba(236, 72, 153, 0.45);
  --frame-tint-chip-bg: rgba(236, 72, 153, 0.14);
  --frame-tint-chip-fg: #9D174D;
}
.canvas-item.is-simple-frame.is-color-orange {
  --frame-tint: #F59E0B;
  --frame-tint-bg-a: rgba(245, 158, 11, 0.05);
  --frame-tint-bg-b: rgba(245, 158, 11, 0.10);
  --frame-tint-border: rgba(245, 158, 11, 0.50);
  --frame-tint-chip-bg: rgba(245, 158, 11, 0.16);
  --frame-tint-chip-fg: #92400E;
}
.canvas-item.is-simple-frame.is-color-green {
  --frame-tint: #10B981;
  --frame-tint-bg-a: rgba(16, 185, 129, 0.05);
  --frame-tint-bg-b: rgba(16, 185, 129, 0.10);
  --frame-tint-border: rgba(16, 185, 129, 0.45);
  --frame-tint-chip-bg: rgba(16, 185, 129, 0.14);
  --frame-tint-chip-fg: #065F46;
}
.canvas-item.is-simple-frame.is-color-slate {
  --frame-tint: #64748B;
  --frame-tint-bg-a: rgba(100, 116, 139, 0.05);
  --frame-tint-bg-b: rgba(100, 116, 139, 0.10);
  --frame-tint-border: rgba(100, 116, 139, 0.45);
  --frame-tint-chip-bg: rgba(100, 116, 139, 0.16);
  --frame-tint-chip-fg: #334155;
}

/* Slice 4H-5B: group-mode Frame visual differentiator.
   A Frame in group-mode still looks like a Frame (same dashed
   border, same color tints, same title chip), but it adds:
     - a stronger 2px dashed border so the group reads as a
       deliberate container at a glance;
     - a small uppercase "Group" badge appended to the title chip
       via ``::after`` so no DOM change is needed.
   The badge inherits the per-color ``--frame-tint-chip-*`` vars
   so every Frame color reads correctly. It rides the title chip's
   existing inverse-scale transform (the chip itself is scaled
   stable at every zoom level by the 4H-4A rule below). Simple-
   mode Frames are untouched. */
.canvas-item.is-simple-frame.is-group-mode {
  border-width: 2px;
}
.canvas-item.is-simple-frame.is-group-mode .canvas-item__handle::after {
  /* Slice 4H-5E: subtler "group" label. The pre-v5E pill badge
     (uppercase 'GROUP', rounded background, 1-px border) felt
     visually heavy compared to the title chip it sat inside.
     v5E drops the pill chrome entirely and uses a lowercase
     inline accent that reads as a subtitle rather than a stamp.
     The 2-px dashed border on the frame body (rule above) carries
     the primary group-mode affordance; the label is a secondary
     reinforcement. */
  content: '\00B7 group';   /* '· group' — middle dot + label */
  display: inline-block;
  margin-left: 6px;
  padding: 0;
  font-size: 10px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.02em;
  text-transform: lowercase;
  vertical-align: middle;
  color: var(--frame-tint-chip-fg, #334155);
  opacity: 0.65;
  background: transparent;
  border: none;
  border-radius: 0;
}
/* Edit-mode title chip should NOT show the Group badge inline
   with the editable text (it would slip into the contenteditable
   buffer and confuse caret placement). */
.canvas-item.is-simple-frame.is-group-mode.is-editing .canvas-item__handle::after {
  display: none;
}

/* ----- Color picker popover (Phase 3 polish) -----
   Floating panel anchored to the colors chip in the chip-row
   above the frame. Opens on chip click, closes on outside-click
   / Escape. Hosts 7 round color dot buttons; the active color
   is ringed via ``.is-current``. Click a dot to apply. */
.canvas-item__frame-colors-popover {
  position: absolute;
  top: 38px;        /* below the chip-row (32 px chip + 6 px gap) */
  right: 0;
  display: flex;
  gap: 6px;
  pointer-events: auto;
  z-index: 9;       /* above the chip-row */
  padding: 8px 10px;
  background: rgba(255, 255, 255, 0.92);
  border: 1px solid rgba(255, 255, 255, 0.55);
  border-radius: 9px;
  box-shadow: 0 4px 14px rgba(55, 72, 64, 0.14),
              0 1px 2px rgba(55, 72, 64, 0.08);
  -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  animation: canvas-item-popover-in .14s ease-out;
}
@keyframes canvas-item-popover-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}
.canvas-item__frame-color-dot {
  width: 16px;
  height: 16px;
  padding: 0;
  margin: 0;
  border-radius: 50%;
  border: 1.5px solid rgba(255, 255, 255, 0.80);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.20);
  cursor: pointer;
  transition: transform .12s, box-shadow .12s;
  flex: 0 0 auto;
}
.canvas-item__frame-color-dot:hover {
  transform: scale(1.18);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.28);
}
.canvas-item__frame-color-dot.is-current {
  border-color: rgba(255, 255, 255, 0.95);
  box-shadow: 0 0 0 2px currentColor, 0 1px 2px rgba(0, 0, 0, 0.20);
  color: rgba(60, 74, 67, 0.85);
}

html.wf-theme-night .canvas-item__frame-colors-popover {
  background: rgba(28, 36, 32, 0.88);
  border-color: rgba(255, 255, 255, 0.16);
}
html.wf-theme-night .canvas-item__frame-color-dot {
  border-color: rgba(255, 255, 255, 0.55);
}
html.wf-theme-night .canvas-item__frame-color-dot.is-current {
  border-color: rgba(255, 255, 255, 0.85);
  color: rgba(231, 239, 233, 0.85);
}
/* Selection ring on the frame still uses the shared ``.is-selected``
   rule above; the dashed border + outline read clearly together. */

/* ----- Night-mode variants ----- */
html.wf-theme-night .canvas-item.is-text-note {
  background: rgba(28, 36, 32, 0.78);
  border-color: rgba(255, 255, 255, 0.14);
  color: #E7EFE9;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.30),
              0 4px 12px rgba(0, 0, 0, 0.30);
}
html.wf-theme-night .canvas-item.is-comment {
  /* Dark glass surface matching the LNM-night and text_note-night
     palette. Same glass language as the light theme. */
  background: rgba(28, 36, 32, 0.82);
  color: #E7EFE9;
  border-color: rgba(255, 255, 255, 0.14);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.30),
              0 6px 16px rgba(0, 0, 0, 0.30);
}
html.wf-theme-night .canvas-item.is-simple-frame {
  border-color: rgba(255, 255, 255, 0.28);
  color: rgba(231, 239, 233, 0.72);
}
/* Frame title-handle on night theme — quiet chip on the dark
   surface so it stays legible without flaring. */
html.wf-theme-night .canvas-item.is-simple-frame .canvas-item__handle {
  background: rgba(28, 36, 32, 0.78);
  color: rgba(231, 239, 233, 0.86);
}
/* Slice 4H-5E: night-theme variant for the group-mode label.
   The v5E label has no pill chrome (transparent background, no
   border) — only the per-color text tint differs. On night theme
   we just override the color so the lowercase "· group" accent
   reads against the dark surface. The pre-v5E background and
   border-color overrides have been removed so this rule no longer
   re-introduces pill chrome that the base rule strips. */
html.wf-theme-night .canvas-item.is-simple-frame.is-group-mode .canvas-item__handle::after {
  color: rgba(231, 239, 233, 0.86);
}
/* Selection ring tone-adjusted for the dark surface. */
html.wf-theme-night .canvas-item.is-selected {
  outline-color: rgba(150, 220, 200, 0.55);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.30),
              0 3px 8px rgba(0, 0, 0, 0.22);
}
/* Slice 4G-2: resize handles on the dark surface. Glyph stays
   muted by default and brightens to the night accent on hover /
   while actively resizing. Mirrors the dark-theme treatment of
   ``.wf-grip`` / ``.wf-toolbar__resize``. */
html.wf-theme-night .canvas-item__resize-handle {
  color: rgba(231, 239, 233, 0.55);
}
html.wf-theme-night .canvas-item__resize-handle.is-active {
  color: rgba(150, 220, 200, 0.95);
}

/* ====================================================================
   Slice 4G-3A Phase 2: comment chrome (author header + threaded replies)
   ====================================================================

   The comment item becomes a vertical flex container:
     [header chip]   ← flex: 0 0 auto
     [body]          ← flex: 1 1 0   (the editable text region)
     [replies]       ← flex: 0 0 auto, max-height capped, scrolls
   text_note and simple_frame are unaffected — these rules scope on
   ``.canvas-item.is-comment`` only.

   The parent's existing ``padding: 8px 10px`` is replaced with 0 so
   each section can manage its own padding. The body keeps the
   thin-scrollbar styling from Phase 1.
*/
/* Phase 3 polish v13: comment uses an INNER scroll-wrapper that
   fills the canvas-item's box and holds the header + body +
   replies. When the user manually resizes the comment shorter
   than the natural content height, the wrapper's ``overflow:
   auto`` shows a scrollbar — content stays INSIDE the box
   instead of escaping below the white card. ``.canvas-item``
   itself remains ``overflow: visible`` (required for the chip-row
   at ``top: -42px`` and the resize handles at corner offsets). */
.canvas-item.is-comment {
  padding: 0;
}
.canvas-item.is-comment .canvas-item__inner {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
  overflow: auto;
  scrollbar-gutter: stable;
  border-radius: inherit;
}
/* Thin scrollbar inside the wrapper (matches the existing body
   thin-scrollbar style). */
.canvas-item.is-comment .canvas-item__inner {
  scrollbar-width: thin;
  scrollbar-color: rgba(60, 74, 67, 0.22) transparent;
}
.canvas-item.is-comment .canvas-item__inner::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
.canvas-item.is-comment .canvas-item__inner::-webkit-scrollbar-track {
  background: transparent;
}
.canvas-item.is-comment .canvas-item__inner::-webkit-scrollbar-thumb {
  background: rgba(60, 74, 67, 0.22);
  border-radius: 4px;
}
.canvas-item.is-comment .canvas-item__inner::-webkit-scrollbar-thumb:hover {
  background: rgba(60, 74, 67, 0.42);
}
/* Body is now natural-size with min-height only — no max-height,
   because the inner wrapper handles overflow for the whole
   comment uniformly. Body's own scroll is suppressed. */
.canvas-item.is-comment .canvas-item__body {
  width: auto;
  height: auto;
  flex: 0 0 auto;
  min-height: 24px;
  overflow: visible;
  padding: 6px 12px;
}

/* ---- Author header chip ----
   Compact horizontal row: 22 px avatar + name + timestamp.
   ``padding: 8px 12px`` gives the chip presence without crowding;
   the bottom border separates it cleanly from the body.
   Phase 3 polish v8: ``flex-wrap: wrap`` lets the timestamp drop
   to a second row when the comment is narrow — the author name
   stays readable instead of being squeezed to "..." or zero. */
.canvas-item__header {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
  padding: 8px 12px;
  border-bottom: 1px solid rgba(60, 74, 67, 0.10);
  /* Header is non-text; cursor reflects that drag from here moves
     the comment, matching the parent ``cursor: move``. */
  cursor: move;
  user-select: none;
}
.canvas-item__avatar {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(46, 125, 108, 0.30);
  color: #FFFFFF;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  text-transform: uppercase;
  box-shadow: 0 1px 2px rgba(55, 72, 64, 0.18);
  user-select: none;
}
.canvas-item__avatar--sm {
  width: 18px;
  height: 18px;
  font-size: 9px;
}
.canvas-item__author {
  flex: 1 1 auto;
  /* Phase 3 polish v8: minimum visible width for the author name so
     it stays readable even when the comment is at its narrow floor.
     The header's ``flex-wrap: wrap`` ensures the timestamp wraps to
     a second row instead of stealing this minimum. */
  min-width: 80px;
  font-size: 12px;
  font-weight: 600;
  color: inherit;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.canvas-item__author--sm {
  font-size: 11px;
  font-weight: 600;
}
.canvas-item__timestamp {
  flex: 0 0 auto;
  font-size: 10.5px;
  opacity: 0.65;
  font-weight: 400;
  white-space: nowrap;
}
.canvas-item__timestamp--sm {
  font-size: 10px;
}
/* Phase 3 polish v4: edited-time inside the timestamp.
   Plain text, same color as the rest of the timestamp string,
   slightly lighter weight for the bullet separator. The
   user-asked: no italic, no accent green — match the rest of
   the timestamp tone so it reads as plain metadata. */
.canvas-item__timestamp-sep {
  opacity: 0.55;
}
.canvas-item__timestamp-edited {
  /* inherits color + font-style from .canvas-item__timestamp */
}

/* Phase 3 redo: foreign comments (authored by another user) are
   read-only — body cursor reads as text (so the user can still
   select+copy the text) rather than ``move``, signalling "I can
   read but not edit". Drag still works via the header chip. */
.canvas-item.is-comment.is-foreign .canvas-item__body {
  cursor: text;
  user-select: text;
}

/* ---- Replies section ---- */
.canvas-item__replies {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* Phase 3 polish v9: dropped ``max-height: 50%`` + ``overflow:
     auto``. Those caps made the replies section's ``offsetHeight``
     report the CAPPED visible height (not the natural content
     height), which under-grew the comment when ``_growItemToFitIfNeeded``
     measured children — the compose form fell out of the comment's
     box. The auto-grow + Show more chip already cap the comment's
     total height; the replies section can simply size to its
     content now. */
  padding: 8px 12px 10px 12px;
  border-top: 1px solid rgba(60, 74, 67, 0.10);
  font-size: 11.5px;
  /* Thin scrollbar — match the body's Phase 1 styling. */
  scrollbar-width: thin;
  scrollbar-color: rgba(60, 74, 67, 0.22) transparent;
}
.canvas-item__replies::-webkit-scrollbar { width: 6px; height: 6px; }
.canvas-item__replies::-webkit-scrollbar-track { background: transparent; }
.canvas-item__replies::-webkit-scrollbar-thumb {
  background: rgba(60, 74, 67, 0.22);
  border-radius: 3px;
}
.canvas-item__replies::-webkit-scrollbar-thumb:hover {
  background: rgba(60, 74, 67, 0.42);
}

.canvas-item__replies-count {
  font-size: 10.5px;
  opacity: 0.65;
  padding: 2px 0 4px 0;
  font-weight: 500;
  user-select: none;
}
.canvas-item__replies-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

/* ---- Single reply card ---- */
.canvas-item__reply {
  display: flex;
  flex-direction: column;
  padding: 6px 8px;
  background: rgba(255, 255, 255, 0.65);
  border-radius: 6px;
  border: 1px solid rgba(60, 74, 67, 0.10);
}
/* Phase 3 polish v10/v14: thread-style indent on a FLAT list of
   reply cards. Each card keeps the comment's full width minus an
   indent per depth. Depth caps at 2 visually (3 levels: 0/1/2);
   the data tree can nest deeper but visually all extra levels sit
   at the same indent. A subtle accent-tinted left border on
   indented replies reads as the conversation thread guide-line. */
.canvas-item__reply[data-depth="0"] { margin-left: 0; }
.canvas-item__reply[data-depth="1"] { margin-left: 20px; }
.canvas-item__reply[data-depth="2"] { margin-left: 40px; }
.canvas-item__reply:not([data-depth="0"]) {
  border-left: 2px solid rgba(46, 125, 108, 0.20);
}
/* Same indent for an inline compose form so it sits where a new
   reply at that depth would land after submit. */
.canvas-item__compose-form[data-depth="0"] { margin-left: 0; }
.canvas-item__compose-form[data-depth="1"] { margin-left: 20px; }
.canvas-item__compose-form[data-depth="2"] { margin-left: 40px; }

.canvas-item__reply-header {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 2px;
}
.canvas-item__reply-header .canvas-item__author {
  font-size: 11px;
}
.canvas-item__reply-delete {
  flex: 0 0 auto;
  background: transparent;
  border: 0;
  padding: 2px;
  margin: 0;
  border-radius: 4px;
  cursor: pointer;
  color: rgba(60, 74, 67, 0.40);
  opacity: 0;
  transition: opacity .12s, color .12s, background-color .12s;
  display: flex;
  align-items: center;
  justify-content: center;
}
.canvas-item__reply:hover .canvas-item__reply-delete {
  opacity: 1;
}
.canvas-item__reply-delete:hover {
  color: #C24E47;
  background: rgba(194, 78, 71, 0.10);
}
.canvas-item__reply-body {
  font-size: 11.5px;
  line-height: 1.4;
  color: inherit;
  white-space: pre-wrap;
  word-wrap: break-word;
  overflow-wrap: anywhere;
  word-break: break-word;
  padding: 1px 0 2px 0;
}
/* Phase 3 polish v5: edit-mode for the reply body. Quiet
   affordance — text cursor + native selection, no harsh outline
   or accent fill. The active editing region inherits the rest
   of the comment's visual rhythm. */
.canvas-item__reply.is-editing-reply .canvas-item__reply-body {
  cursor: text;
  user-select: text;
  outline: none;
}
.canvas-item__reply-actions {
  display: flex;
  gap: 4px;
  margin-top: 2px;
}
.canvas-item__reply-action {
  background: transparent;
  border: 0;
  padding: 2px 6px;
  margin: 0;
  font-size: 10.5px;
  font-weight: 500;
  color: rgba(60, 74, 67, 0.65);
  border-radius: 4px;
  cursor: pointer;
  transition: background-color .12s, color .12s;
}
.canvas-item__reply-action:hover {
  background: rgba(60, 74, 67, 0.08);
  color: var(--accent, #2E7D6C);
}
/* Phase 3 polish v10: ``.canvas-item__reply-children`` is no
   longer rendered — replies are now a flat list. Selector kept
   here only to neutralize any stale instances; no styles applied. */
.canvas-item__reply-children { display: none; }

/* ---- Compose form (root reply OR reply-to-reply) ----
   Phase 3 polish v6: simpler trigger — a plain text affordance,
   no dashed pill border (the pill looked alarming amid the calm
   glass surface of the comment). Hover gets a quiet row-hover
   wash + accent text. */
.canvas-item__reply-compose {
  display: flex;
  flex-direction: column;
  margin: 0;
}
.canvas-item__reply-trigger {
  align-self: flex-start;
  background: transparent;
  border: 0;
  padding: 4px 8px;
  margin: 0;
  font: inherit;
  font-size: 11px;
  font-weight: 500;
  color: rgba(60, 74, 67, 0.65);
  border-radius: 6px;
  cursor: pointer;
  transition: background-color .12s, color .12s;
}
.canvas-item__reply-trigger:hover {
  background: rgba(60, 74, 67, 0.06);
  color: var(--accent, #2E7D6C);
}
.canvas-item__compose-form {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 6px;
  padding: 6px;
  background: rgba(255, 255, 255, 0.80);
  border: 1px solid rgba(60, 74, 67, 0.12);
  border-radius: 6px;
}
.canvas-item__compose-input {
  width: 100%;
  /* Phase 3 polish v7: ``resize: none`` because the textarea now
     auto-grows in JS (input listener sets ``height = scrollHeight``).
     A user-drag-resize would fight the next keystroke's grow pass.
     ``max-height`` caps the auto-grow; beyond it the textarea
     scrolls internally. */
  resize: none;
  min-height: 38px;
  max-height: 120px;
  padding: 6px 8px;
  margin: 0;
  font: inherit;
  font-size: 11.5px;
  line-height: 1.4;
  color: inherit;
  background: rgba(255, 255, 255, 0.85);
  border: 1px solid rgba(60, 74, 67, 0.18);
  border-radius: 4px;
  box-sizing: border-box;
  outline: none;
  overflow: auto;
  transition: border-color .12s, background-color .12s;
}
/* Phase 3 polish v6: softened focus border — the previous solid
   accent border read as the same too-prominent "green frame" the
   user flagged on the comment body. A subtle accent tint instead. */
.canvas-item__compose-input:focus {
  border-color: rgba(46, 125, 108, 0.45);
  background: #FFFFFF;
}
.canvas-item__compose-actions {
  display: flex;
  gap: 4px;
  justify-content: flex-end;
}
.canvas-item__compose-send,
.canvas-item__compose-cancel {
  background: transparent;
  border: 1px solid rgba(60, 74, 67, 0.20);
  padding: 2px 10px;
  margin: 0;
  font: inherit;
  font-size: 10.5px;
  font-weight: 500;
  color: rgba(60, 74, 67, 0.75);
  border-radius: 4px;
  cursor: pointer;
  transition: background-color .12s, color .12s, border-color .12s;
}
.canvas-item__compose-send {
  background: var(--accent, #2E7D6C);
  border-color: var(--accent, #2E7D6C);
  color: #FFFFFF;
}
.canvas-item__compose-send:hover {
  filter: brightness(1.08);
}
.canvas-item__compose-cancel:hover {
  background: rgba(60, 74, 67, 0.08);
  color: inherit;
}

/* ---- Night-mode overrides ---- */
html.wf-theme-night .canvas-item__header {
  border-bottom-color: rgba(255, 255, 255, 0.10);
}
html.wf-theme-night .canvas-item__replies {
  border-top-color: rgba(255, 255, 255, 0.10);
}
html.wf-theme-night .canvas-item__replies::-webkit-scrollbar-thumb {
  background: rgba(231, 239, 233, 0.22);
}
html.wf-theme-night .canvas-item__replies::-webkit-scrollbar-thumb:hover {
  background: rgba(231, 239, 233, 0.42);
}
html.wf-theme-night .canvas-item__reply {
  background: rgba(255, 255, 255, 0.04);
  border-color: rgba(255, 255, 255, 0.08);
}
html.wf-theme-night .canvas-item__reply-action {
  color: rgba(231, 239, 233, 0.70);
}
html.wf-theme-night .canvas-item__reply-action:hover {
  background: rgba(255, 255, 255, 0.10);
  color: rgba(150, 220, 200, 0.95);
}
html.wf-theme-night .canvas-item__reply-trigger {
  color: rgba(231, 239, 233, 0.70);
}
html.wf-theme-night .canvas-item__reply-trigger:hover {
  background: rgba(255, 255, 255, 0.08);
  color: rgba(150, 220, 200, 0.95);
}
html.wf-theme-night .canvas-item__compose-form {
  background: rgba(255, 255, 255, 0.06);
  border-color: rgba(255, 255, 255, 0.12);
}
html.wf-theme-night .canvas-item__compose-input {
  background: rgba(28, 36, 32, 0.75);
  border-color: rgba(255, 255, 255, 0.18);
  color: rgba(231, 239, 233, 0.90);
}
html.wf-theme-night .canvas-item__compose-input:focus {
  background: rgba(28, 36, 32, 0.92);
  border-color: rgba(150, 220, 200, 0.85);
}
html.wf-theme-night .canvas-item__compose-cancel {
  border-color: rgba(255, 255, 255, 0.20);
  color: rgba(231, 239, 233, 0.70);
}
html.wf-theme-night .canvas-item__compose-cancel:hover {
  background: rgba(255, 255, 255, 0.08);
  color: rgba(231, 239, 233, 0.90);
}
html.wf-theme-night .canvas-item__reply-delete:hover {
  color: #FF8A82;
  background: rgba(255, 138, 130, 0.10);
}

/* ====================================================================
   Slice 4G-3A Phase 3 polish v2: expand/collapse is now a CHIP in
   the chip-row above the item (sibling of close and colors). The
   standalone bottom-of-body button is gone. The chip uses the
   shared ``.canvas-item__chip`` visuals and only its visibility
   is gated below: it shows ONLY when the parent has the
   ``.is-overflowing`` class (set by ``_markOverflowing`` after a
   layout-settled measurement).
   ==================================================================== */

/* Phase 3 polish v12: expand chip ALWAYS visible inside the
   chip-row for text_note + comment items (regardless of overflow
   state). The toggle stays reachable on hover so the user can
   compact / expand any item at will, even after a resize.
   simple_frame doesn't get one. Hidden during edit so it doesn't
   compete with the active caret. */
.canvas-item__chip-row > .canvas-item__chip--expand {
  display: none;
}
.canvas-item.is-text-note >
  .canvas-item__chip-row > .canvas-item__chip--expand,
.canvas-item.is-comment >
  .canvas-item__chip-row > .canvas-item__chip--expand {
  display: flex;
}
.canvas-item.is-editing >
  .canvas-item__chip-row > .canvas-item__chip--expand {
  display: none;
}
/* Phase 3 polish v15: chevron rotation animates the
   collapse/expand toggle. The SVG is rendered once (down-chevron)
   and persists across renders; CSS rotates it 180° when the item
   is NOT collapsed. The ``cubic-bezier`` matches the LNM motion
   curve so the chevron flip feels at home with the rest of the
   chrome's interactions. */
.canvas-item__chip--expand > svg {
  display: block;
  transition: transform .22s cubic-bezier(.32, .72, 0, 1);
  transform-origin: 50% 50%;
}
.canvas-item:not(.is-collapsed) >
  .canvas-item__chip-row >
  .canvas-item__chip--expand > svg {
  transform: rotate(180deg);
}

/* Phase 3 polish v14: reset chip — always visible for text_note +
   comment in the chip-row. Same visibility gating as the expand
   chip. */
.canvas-item__chip-row > .canvas-item__chip--reset {
  display: none;
}
.canvas-item.is-text-note >
  .canvas-item__chip-row > .canvas-item__chip--reset,
.canvas-item.is-comment >
  .canvas-item__chip-row > .canvas-item__chip--reset {
  display: flex;
}
.canvas-item.is-editing >
  .canvas-item__chip-row > .canvas-item__chip--reset {
  display: none;
}

/* Phase 3 polish v14: collapsed preview state. Hides replies
   entirely and truncates the body to two lines with an ellipsis,
   so a comment "Show less" shows a real card preview (header +
   snippet of body) instead of a thin white sliver. Inner wrapper's
   scroll suppressed in preview mode. */
.canvas-item.is-collapsed.is-comment .canvas-item__inner {
  overflow: hidden;
}
.canvas-item.is-collapsed.is-comment .canvas-item__replies {
  display: none;
}
.canvas-item.is-collapsed .canvas-item__body {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  text-overflow: ellipsis;
  max-height: 40px;
}

/* ===========================================================
   Slice 4H-4A polish (Issue 4): zoom-stable chrome.

   The canvas stage applies `transform: translate(...) scale(s)`
   to all children, so chrome elements (toolbars, endpoint
   handles, resize grips, chips, Frame title chip) would
   normally scale with the canvas — at zoom 200% they double
   in screen size, at zoom 40% they shrink to 40%. The Arrow +
   Text-note toolbars feel especially intrusive at high zoom.

   Each chrome element below opts into the inverse via
   `transform: <existing> scale(var(--aw-canvas-ui-inverse-scale))`
   so its screen size stays approximately constant across zoom
   levels. The CSS variable is set on `#canvas-stage` from
   `applyTransform` in canvas.js. For items where the chrome
   has a clear "anchor" on its own box (toolbar top-center for
   is-below, chip-row bottom-center, etc.) `transform-origin`
   is set so scaling stays glued to that anchor — preventing
   visual drift across zoom.

   The toolbar rules above (`.canvas-item__arrow-toolbar.is-below`,
   `.canvas-item__text-toolbar.is-below`, plus their .is-above
   variants) already include the inverse-scale. Below: endpoint
   handles, resize handles, chip-row, Frame title chip.

   Note: at extreme zoom levels (very low or very high) the
   resize-handle CENTER stays put but the handle's visual edge
   no longer hugs the wrapper corner with sub-pixel precision.
   Acceptable for v1 polish; the handles remain visible and
   clickable. =========================================== */
.canvas-item__arrow-handle {
  /* Center-anchored — default transform-origin (50% 50%) is
     already the visual anchor since the handle's `margin:
     -7px 0 0 -7px` centers it on its (left, top) coord. */
  transform: scale(var(--aw-canvas-ui-inverse-scale, 1));
}
.canvas-item.is-arrow.is-selected .canvas-item__arrow-handle:hover,
.canvas-item.is-arrow.is-selected .canvas-item__arrow-handle.is-active {
  /* Preserve the v3B hover/active scale-up by composing with
     the inverse-scale. Without this override the v3B 1.18 scale
     would replace the inverse-scale and the handle would jump
     back to canvas-zoom size on hover. */
  transform: scale(calc(var(--aw-canvas-ui-inverse-scale, 1) * 1.18));
}

.canvas-item__resize-handle {
  /* Center-anchored — handles are positioned at -22 px offsets
     and are 28×28; their natural center is the same point on
     the canvas regardless of scale, and scaling around the
     center keeps the handle in the same screen position
     relative to the wrapper corner with only sub-pixel drift
     at extreme zoom. */
  transform: scale(var(--aw-canvas-ui-inverse-scale, 1));
}

/* NOTE: `.canvas-item__chip-row` inverse-scale lives at its
   pre-existing reveal rules above (translateY + scale composed
   together with transform-origin: 100% 100%). A bare rule here
   would be overridden by those higher-specificity selectors. */

.canvas-item.is-simple-frame .canvas-item__handle {
  /* Frame title chip — anchored at the frame's top-left
     corner. Scaling around its top-left keeps it pinned to
     the corner regardless of zoom. */
  transform: scale(var(--aw-canvas-ui-inverse-scale, 1));
  transform-origin: 0 0;
}

/* ============================================================
   Slice 4H-4C-1 (multi-select foundation + expanded UX).
   The rules below were inadvertently dropped when topbar.css
   was reverted to HEAD during the help-button positioning
   rebuild. Restoring them here in one block so the slice's
   visual contracts are intact:
     • marquee selection rectangle
     • body.is-marquee-active suppresses native text-selection
       page-wide while the marquee gesture is active
     • .is-multi-selected outline on non-primary canvas-item
       multi-members (and the arrow exception)
     • hover-reveal rules so multi-selected items surface their
       chrome only on hover (per the "no chrome auto-shows in
       multi-mode" UX)
     • text-note "Aa" style-toggle chip + the `.is-style-open`
       gate that requires an explicit click to surface the
       text style toolbar (per "no styling menu auto-on-click")
   ============================================================ */

/* Marquee selection rectangle. The JS appends
   `<div class="canvas-marquee">` to the viewport during a
   drag gesture; this rule gives it a visible dashed border
   + translucent fill. pointer-events:none so the box never
   intercepts the in-flight mousemove. */
.canvas-marquee {
  position: absolute;
  pointer-events: none;
  border: 1px dashed rgba(46, 125, 108, 0.65);
  background: rgba(46, 125, 108, 0.08);
  border-radius: 2px;
  z-index: 50;
}
html.wf-theme-night .canvas-marquee {
  border-color: rgba(150, 220, 200, 0.65);
  background: rgba(150, 220, 200, 0.06);
}

/* During a marquee gesture the body gains `is-marquee-active`
   (added on mousedown, removed on mouseup). Suppress native
   text-selection globally so dragging the marquee across the
   right drawer or any other selectable text doesn't leave a
   ghost native-selection behind. */
body.is-marquee-active,
body.is-marquee-active * {
  -webkit-user-select: none !important;
  user-select: none !important;
}

/* Non-primary members of a multi-selection. Same accent hue as
   `.is-selected` (line ~2440 of this file's history before the
   slice) at lower alpha so the user sees at a glance which item
   is the primary (drives chrome) versus the rest of the group.
   Arrow exception: arrows signal selection via their endpoint
   handles + floating toolbar; the bbox outline is suppressed. */
.canvas-item.is-multi-selected {
  outline: 1.5px solid rgba(46, 125, 108, 0.40);
  outline-offset: 2px;
  box-shadow: 0 1px 3px rgba(46, 125, 108, 0.08);
}
.canvas-item.is-arrow.is-multi-selected {
  outline: none;
  box-shadow: none;
}
html.wf-theme-night .canvas-item.is-multi-selected {
  outline-color: rgba(150, 220, 200, 0.40);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.22);
}

/* Multi-mode hover-reveal: per the UX rule "chrome only shows
   on hover in multi-mode," these rules let hovering a specific
   multi-selected item surface its style toolbar / endpoint
   handles. Without these the chrome would never appear during
   multi-mode because `.is-selected` is suppressed by the JS
   class-sync (see canvas.js _syncCanvasItemSelectionDom). */
.canvas-item.is-text-note.is-multi-selected.is-style-open:hover:not(.is-editing) .canvas-item__text-toolbar {
  display: inline-flex;
}
.canvas-item.is-arrow.is-multi-selected:hover .canvas-item__arrow-toolbar {
  display: inline-flex;
}
.canvas-item.is-arrow.is-multi-selected:hover .canvas-item__arrow-handle {
  opacity: 1;
  pointer-events: auto;
}

/* Text-note style toolbar — click-to-reveal gating. The
   toolbar is HIDDEN by default on selection; the user clicks
   the "Aa" chip in the chip-row to flip `.is-style-open` on
   the canvas-item. Without `.is-style-open` the toolbar never
   shows. Replaces the auto-on-selection behaviour where the
   toolbar would surface every time a text note was clicked. */
.canvas-item.is-text-note.is-selected.is-style-open:not(.is-editing) .canvas-item__text-toolbar {
  display: inline-flex;
}

/* "Aa" style-toggle chip — text_note only. Same chip-row
   visibility gates as the expand / reset chips: visible when
   the canvas-item is text_note, hidden during inline edit.
   `.is-active` accent-tints the chip when the toolbar is open
   so the user has a clear affordance for which chip toggles
   styling. */
.canvas-item__chip-row > .canvas-item__chip--style {
  display: none;
}
.canvas-item.is-text-note >
  .canvas-item__chip-row > .canvas-item__chip--style {
  display: flex;
}
.canvas-item.is-editing >
  .canvas-item__chip-row > .canvas-item__chip--style {
  display: none;
}
.canvas-item__chip--style.is-active {
  background: rgba(46, 125, 108, 0.18);
  color: rgba(46, 125, 108, 0.95);
  border-color: rgba(46, 125, 108, 0.30);
}
html.wf-theme-night .canvas-item__chip--style.is-active {
  background: rgba(150, 220, 200, 0.18);
  color: rgba(220, 240, 232, 0.95);
  border-color: rgba(150, 220, 200, 0.30);
}


/* Slice 4H-4C-2 polish: copy-confirmation pill — small glass chip
   that appears at top-center just below the topbar when the user
   copies a canvas selection (keyboard Ctrl/Cmd+C or right-click
   Copy from the multi-select menu). Lives outside the bottom-right
   .toast-host because the standard toast positioning felt awkward
   for a near-instant local-state action with no async confirmation.
   Glass chrome matches the .toolbar-group family from the canvas
   chrome family (4H-4B / 4H-4C-1) so it reads as canvas UI, not as
   a global notification. Pointer-events:none so it never blocks a
   click. Position calc keys off --bar-h / --bar-gap from the topbar
   so the pill stays in the right place across collapse/expand. */
.canvas-copy-pill {
  position: fixed;
  top: calc(var(--bar-gap, 14px) + var(--bar-h, 48px) + 14px);
  left: 50%;
  transform: translate(-50%, -6px);
  padding: 7px 14px;
  font-size: 13px;
  font-weight: 500;
  line-height: 1.25;
  color: rgba(30, 38, 48, 0.95);
  background: rgba(255, 255, 255, 0.78);
  -webkit-backdrop-filter: blur(14px) saturate(180%);
          backdrop-filter: blur(14px) saturate(180%);
  border: 1px solid rgba(0, 0, 0, 0.08);
  border-radius: 999px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08), 0 1px 0 rgba(255, 255, 255, 0.6) inset;
  opacity: 0;
  pointer-events: none;
  z-index: 70;
  transition: opacity 150ms ease, transform 200ms ease;
}
.canvas-copy-pill.is-visible {
  opacity: 1;
  transform: translate(-50%, 0);
}
html.wf-theme-night .canvas-copy-pill {
  color: rgba(231, 239, 233, 0.95);
  background: rgba(20, 28, 42, 0.78);
  border-color: rgba(255, 255, 255, 0.10);
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.32), 0 1px 0 rgba(255, 255, 255, 0.04) inset;
}
/* Hide on routes that don't show the canvas (Pages list, etc.) so
   the pill never leaks into non-canvas screens. Audit fix: the router
   sets ``route-page`` (singular) — the old ``route-pages`` selector
   matched nothing. */
body.route-page .canvas-copy-pill { display: none !important; }

/* ============================================================
   Slice 4H-CH1 — coordinated help tile + LNM height containment.

   Owner requests C1 / C2. The help "?" tile used to be pinned by
   static CSS at `right: var(--bar-gap)` — the SAME corner the
   PlanFlow inspector docks in. A collapsed inspector chip covered
   38x40 of its 40x40 at z-index 101 against the tile's 30, so the
   "?" was not merely unclickable, it was not visible anywhere on
   the page. Nothing in the coordinator knew the tile existed
   (`grep -c toolbar-help topbar-adapter.js` was 0).

   The fix is positional, not a z-index bump: the tile becomes a
   first-class coordinated surface. Pass D (topbar-adapter.js,
   after Pass C) anchors it to the POST-Pass-C zoom cluster and
   writes --aw-tools-help-dx/-dy on .app; the tile's `top`/`right`
   stay exactly as they were and the offset composes as the
   `translate` property, the same contract .wf-nav, .canvas-toolbar
   and .wf-toolbar already use.

   Why `translate` and not top/left: `translate` is GPU-composited,
   composes additively with any `transform`, and — critically —
   leaves the base CSS rule the single source of truth for the
   tile's NATURAL position, which is what lets the coordinator
   reconstruct that position from tokens instead of measuring an
   element it writes to.

   TRANSITION CURVE — deliberately the zoom group's WIDTH curve
   (.26s cubic-bezier(.4,0,.2,1), topbar.css ~1642), not the
   canvas-toolbar's `left` curve (.42s cubic-bezier(.32,.72,0,1)).

   Two different animations move the tile's anchor:
     · the cluster GLIDES sideways  — `.canvas-toolbar { left }`, .42s
     · the cluster GROWS/SHRINKS    — `#zoom-group { width }`, .26s,
       left edge fixed, right edge sweeping 202px

   The tile tracks the cluster's RIGHT edge, so the growth case is the
   dangerous one: with the slower .42s curve the tile was still only
   part-way through its 204px trip when the cluster had finished
   expanding, and the cluster's right edge overtook it — reproduced as
   a full 40x40 help-over-zoom overlap during the expand. Matching the
   width curve exactly makes that case perfect: both edges interpolate
   identically, so the 14px gap between them is CONSTANT for the whole
   animation. In the glide case the tile now arrives slightly early,
   i.e. ahead of the cluster rather than under it — the safe side of
   the error. ============================================= */
.app > .toolbar-group.toolbar-help {
  translate: var(--aw-tools-help-dx, 0px) var(--aw-tools-help-dy, 0px);
  will-change: translate;
  /* --aw-tools-help-delay is a one-shot, direction-aware start delay
     written by the coordinator: 0s when the tile moves RIGHT (it must
     lead the cluster's growing right edge) and .2s when it moves LEFT
     (its destination is inside the cluster's CURRENT box, so it has to
     let the cluster clear the lane first). Timing only — the offsets
     themselves are identical either way. */
  /* ROUND 30: on the chrome clock, no direction-aware delay - with the cluster and the tile on
     one curve the tile's lane is vacated at the rate it is entered */
  transition: translate var(--aw-chrome-dur) var(--aw-chrome-ease);
}

/* The canvas-shortcuts popover is `position: absolute; z-index: 7`
   INSIDE .toolbar-help's own stacking context (fixed + z 30), so 7
   could never beat an inspector's 101 — opening Help produced a
   dialog the user could not read. Pass D keeps the tile away from
   the inspector dock geometrically, which is the real fix; this
   rule covers the remaining case where the user has dragged a
   panel across the tile's row. It is scoped to the OPEN state, so
   it is a dialog-on-top rule, not a static bump used to paper over
   a positioning bug. */
.app > .toolbar-group.toolbar-help:has(#canvas-help-popover.open) {
  z-index: 8600;
}

/* LNM height containment (matrix ruling 5, and the second half of
   owner request C5).

   `.wf-nav`'s base rule is `max-height: calc(100vh - 110px)`,
   which assumes the nav starts at its natural y. Once the nav is
   pushed DOWN — by Pass A when the tools bar wraps to its own row,
   or by the user's own drag — that assumption breaks and the nav
   hangs off the bottom (measured visibleFrac 0.724 at 1280x540).

   --aw-tools-nav-maxh is written ONLY when the room actually below
   the nav is tighter than the base rule, so the default layout is
   unchanged; the fallback below is the base expression verbatim.
   `.wf-scroll` already scrolls (`flex: 1; min-height: 0;
   overflow-y: auto`), so the nav shrinks instead of escaping —
   which is exactly ruling 5's small-viewport clause: containment
   wins, the surface shrinks and scrolls internally.

   `!important` is unavoidable here and is NOT a z-index-style
   shortcut: the nav's own resize grip writes `max-height: none` as
   an INLINE style (sidebar-nav.js bindResize) so a hand-dragged
   height is not clipped by the base cap. Without `!important`, a
   nav the user had grown kept running off the bottom (measured
   visibleFrac 0.634 at 1280x540) because that inline `none` beat
   this rule. The grip's contract stays intact because the
   coordinator ECHOES the inline value into --aw-tools-nav-maxh
   whenever it has no clamp of its own — so `none` still means
   none, and the user's dragged height is only ever overridden
   when it would leave the viewport. */
body.route-workflow .app > .wf-nav {
  max-height: var(--aw-tools-nav-maxh, calc(100vh - 110px)) !important;
}

/* ============================================================
   ROUND 23 (owner report 5, track H) - the collapsed-chrome Run tile
   and the dock column it shares with the collapsed palette.

   "when all menu elements are collapsed, add above the collapsed-tiles
    stack a button, aligned with the design and similar to the collapsed
    icons, that would start the workflow - but only when the Run CTA is
    not shown".

   #aw-dockcol is a fixed column at the top-right corner, on the same
   16 px edge and 16 px top as the inspector dock (planflow-inspector.js
   AW_DOCK_EDGE / AW_DOCK_TOP) with the dock's 14 px pitch, so a tile
   here and a collapsed inspector tile below it read as ONE column
   (owner ruling: the dock is a vertical column). The coordinator
   publishes the column's box as a chrome obstacle, so the inspector
   dock starts underneath it. Only the flag paints any of this
   (body.aw-newblocks); the tiles are display:none until their state
   says otherwise. Tile recipe = the help tile's / the collapsed zoom
   cluster's glass card: 40 x 40, 12 px radius, .56 white, blur. */
body.aw-newblocks #aw-dockcol {
  position: fixed;
  top: 18px;               /* ROUND 30: the logo's row (it sat 2 px above it) */
  right: 14px;             /* the bar gap, like the left edge */
  z-index: 61;             /* one above the inspector dock's tiles (60) */
  display: flex;
  flex-direction: column;
  gap: 16px;               /* ROUND 30: 18 + 40 + 16 = 74, the second row when the bar is folded */
  pointer-events: none;    /* the gaps are not a hit surface */
}
body.aw-newblocks #aw-dockcol > * { pointer-events: auto; }
body.aw-newblocks #aw-dockcol:empty { display: none; }
body.aw-newblocks .wf-runtile {
  display: none;
  width: 40px;
  height: 40px;
  padding: 0;
  margin: 0;
  box-sizing: border-box;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  font: inherit;
  color: #41584C;
  background: rgba(255, 255, 255, 0.56);
  border: 1px solid rgba(255, 255, 255, 0.70);
  border-radius: 12px;
  -webkit-backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  backdrop-filter: blur(20px) saturate(180%) brightness(1.03);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6),
              0 10px 30px rgba(55, 72, 64, 0.14);
  transition: background-color .12s, color .12s, transform .12s var(--ease, cubic-bezier(.4, 0, .2, 1));
  animation: wf-group-in var(--aw-chrome-dur) var(--aw-chrome-ease);
}
body.aw-newblocks .wf-runtile.is-shown { display: flex; }
body.aw-newblocks .wf-runtile.is-hiding { animation: wf-group-out var(--aw-chrome-dur) var(--aw-chrome-ease) forwards; pointer-events: none; }
@keyframes wf-group-out { to { transform: scale(.86) translateY(5px); opacity: 0; } }
body.aw-newblocks .wf-runtile:hover { background: rgba(56, 120, 90, 0.09); color: #1F6E45; }
body.aw-newblocks .wf-runtile:active { transform: translateY(1px); }
body.aw-newblocks .wf-runtile:focus-visible { outline: 0; box-shadow: 0 0 0 2px #fff, 0 0 0 4px rgba(60, 120, 92, .55); }
body.aw-newblocks .wf-runtile svg { width: 18px; height: 18px; display: block; }
/* the CTA's own states, mirrored: a running tile carries the stop glyph in the
   CTA's green, a paused one the play glyph again */
body.aw-newblocks .wf-runtile.is-running { color: #1C6240; background: rgba(56, 120, 90, 0.14); }
body.aw-newblocks .wf-runtile.is-paused { color: #1C6240; }
/* night mode (REVIEW FIX 2, round 23): the same night recipe the help tile
   gets above (`html.wf-theme-night .app > .toolbar-group.toolbar-help`, the
   night glass at rgba(28,36,32,.56) with the .80 light glyph) so the Run tile
   reads like the tile beside it. Before: the day glass over the night canvas,
   glyph at 2.64:1 (spec 36 arm K, red run). */
html.wf-theme-night body.aw-newblocks .wf-runtile {
  background: rgba(28, 36, 32, 0.56);
  border-color: rgba(255, 255, 255, 0.16);
  color: rgba(231, 239, 233, 0.80);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10),
              0 10px 30px rgba(0, 0, 0, 0.30);
}
html.wf-theme-night body.aw-newblocks .wf-runtile:hover { background: rgba(150, 220, 200, 0.10); color: rgba(231, 239, 233, 1); }
html.wf-theme-night body.aw-newblocks .wf-runtile:focus-visible { box-shadow: 0 0 0 2px #0E1411, 0 0 0 4px rgba(127, 211, 168, .6); }
html.wf-theme-night body.aw-newblocks .wf-runtile.is-running { color: #7FD3A8; background: rgba(120, 200, 160, 0.14); }
html.wf-theme-night body.aw-newblocks .wf-runtile.is-paused { color: #7FD3A8; }
@keyframes wf-group-in { from { transform: scale(.86) translateY(5px); } to { transform: none; } }

/* ROUND 39: Undo in the bar — muted while there is nothing to reverse */
.wf-bar .wf-bar__ic.wf-undo.is-muted{opacity:.38}
.wf-bar .wf-bar__ic.wf-undo[disabled]{cursor:default}
.wf-bar .wf-bar__ic.wf-undo[disabled]:hover{background:transparent}

/* round 67: the workspace chip beside the account avatar */
.wf-acct-ws{display:inline-flex;align-items:center;height:22px;padding:0 9px;margin-right:8px;border-radius:999px;
  border:1px solid rgba(47,107,77,.28);background:rgba(255,255,255,.55);color:#2F6B4D;font:600 11px/1 ui-monospace,Menlo,monospace;
  letter-spacing:.02em;text-decoration:none;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}
.wf-acct-ws:hover{background:rgba(255,255,255,.85)}
.wf-acct-ws.is-personal{color:rgba(44,106,77,.7);border-style:dashed}

/* round 70: the presence chip before the workspace chip — who else is here / running */
.wf-acct-presence{display:inline-flex;align-items:center;height:22px;padding:0 9px;margin-right:8px;border-radius:999px;
  border:1px solid rgba(47,107,77,.28);background:rgba(255,255,255,.55);color:#2F6B4D;font:600 11px/1 ui-monospace,Menlo,monospace;
  letter-spacing:.02em;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:default}
.wf-acct-presence::before{content:"";width:7px;height:7px;border-radius:50%;background:#3E8E6E;margin-right:6px;flex:none}
.wf-acct-presence.is-alone{color:rgba(44,106,77,.6);border-style:dashed}
.wf-acct-presence.is-alone::before{background:rgba(62,142,110,.45)}
.wf-acct-presence.is-running::before{background:#D08A2E;animation:wfPresencePulse 1.2s ease-in-out infinite}
@keyframes wfPresencePulse{0%,100%{opacity:1}50%{opacity:.35}}
