/* client-v2/styles.css — ported to match prod's dark purple/blue theme.
   Design tokens (from prod index.html):
     --bg:        #1a1a2e   main canvas background (no grid)
     --panel:     #16213e   header / auth panel
     --panel2:    #0f172a   deeper container (nodes, toolbars)
     --border:    #0f3460   medium-blue borders
     --accent:    #533483   primary purple (buttons, selection, perimeter)
     --accent-lt: #a78bfa   light purple (hover, highlight text)
     --ok:        #22c55e   green
     --text:      #e0e0e0   body text
     --muted:     #94a3b8   secondary text
*/

* { box-sizing: border-box; }

[hidden] { display: none !important; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: #1a1a2e;
  color: #e0e0e0;
}

#auth-screen {
  max-width: 360px;
  margin: 10vh auto;
  padding: 2rem;
  background: #16213e;
  border: 1px solid #0f3460;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.4);
}

h1 {
  margin: 0 0 1rem;
  font-size: 1.5rem;
  text-align: center;
  color: #a78bfa;
}

#env-badge {
  position: fixed;
  top: 8px;
  left: 8px;
  z-index: 1100;
  font-size: 0.7rem;
  font-weight: 700;
  padding: 0.15rem 0.5rem;
  border-radius: 3px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  pointer-events: none;
}
#env-badge[data-env="dev"] {
  color: #c4b5fd;
  background: rgba(167, 139, 250, 0.15);
  border: 1px solid rgba(167, 139, 250, 0.4);
}
#env-badge[data-env="prod"] {
  color: #6ee7b7;
  background: rgba(110, 231, 183, 0.15);
  border: 1px solid rgba(110, 231, 183, 0.4);
}
#env-badge[data-env="legacy"] {
  color: #94a3b8;
  background: rgba(148, 163, 184, 0.12);
  border: 1px solid rgba(148, 163, 184, 0.35);
}

.auth-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: 1rem;
}

.auth-tabs button {
  flex: 1;
  padding: 0.5rem;
  border: 1px solid #0f3460;
  background: #0f172a;
  color: #e0e0e0;
  cursor: pointer;
  border-radius: 4px;
  font-size: 0.95rem;
}

.auth-tabs button.active {
  background: #533483;
  color: #fff;
  border-color: #a78bfa;
}

form label {
  display: block;
  margin-bottom: 0.75rem;
  font-size: 0.9rem;
}

form input {
  width: 100%;
  padding: 0.5rem;
  border: 1px solid #0f3460;
  border-radius: 4px;
  font-size: 1rem;
  margin-top: 0.25rem;
  font-family: inherit;
  background: #0f172a;
  color: #e0e0e0;
}

form input:focus { outline: none; border-color: #533483; }

#submit-btn {
  width: 100%;
  padding: 0.6rem;
  background: #533483;
  color: #fff;
  border: 1px solid #a78bfa;
  border-radius: 4px;
  font-size: 1rem;
  cursor: pointer;
  font-family: inherit;
}

#submit-btn:hover { background: #6b3fa0; }
#submit-btn:disabled { opacity: 0.6; cursor: wait; }

.msg {
  margin: 0.75rem 0 0;
  font-size: 0.9rem;
}
.msg.error { color: #ef4444; }
.msg.info  { color: #4ade80; }

#toast.toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  padding: 0.6rem 1rem;
  border-radius: 6px;
  font-size: 0.9rem;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
  z-index: 1000;
  max-width: 80vw;
  transition: opacity 200ms;
  opacity: 1;
}
#toast.toast.error { background: #b91c1c; color: #fff; }
#toast.toast.info  { background: #16a34a; color: #fff; }
#toast.toast.hidden { opacity: 0; pointer-events: none; }

/* Undo-last-delete toast. Positioned bottom-center like the status
   toast but with a pill shape and an inline Undo action. Lives 6s
   unless the user clicks it or a new delete replaces it. */
.undo-toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  background: #0f172a;
  border: 1px solid #334155;
  border-radius: 999px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5);
  color: #e2e8f0;
  font-size: 0.9rem;
  z-index: 1020;
}
.undo-toast .undo-toast-btn {
  background: #533483;
  border: 1px solid #a78bfa;
  color: #fff;
  cursor: pointer;
  padding: 4px 12px;
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  transition: background 120ms;
}
.undo-toast .undo-toast-btn:hover {
  background: #6d28d9;
}

#app-screen {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

#app-screen header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem 1rem;
  background: #16213e;
  border-bottom: 1px solid #0f3460;
}

#greeting { font-weight: 600; color: #a78bfa; }

.header-actions {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

.header-actions button {
  padding: 0.4rem 0.8rem;
  border: 1px solid #533483;
  background: #0f3460;
  color: #e0e0e0;
  cursor: pointer;
  border-radius: 4px;
  font-family: inherit;
  font-size: 0.9rem;
}

.header-actions button:hover { background: #533483; }

#add-node-btn {
  background: #533483;
  color: #fff;
  border-color: #a78bfa;
}

#add-node-btn:hover { background: #6b3fa0; }

#board-picker {
  padding: 0.35rem 0.6rem;
  border: 1px solid #533483;
  background: #0f3460;
  color: #e0e0e0;
  border-radius: 4px;
  font-family: inherit;
  font-size: 0.9rem;
  max-width: 220px;
}

#board-picker:focus { outline: 1px solid #a78bfa; }

.share-popup {
  position: fixed;
  z-index: 1030;
  min-width: 280px;
  /* Wider cap (was 340) so a row like "Trevor Lawrence — trevor.law33@gmail.com"
     fits without truncating. Capped to viewport so it never overflows. */
  max-width: min(520px, 92vw);
  background: #0f172a;
  border: 1px solid #533483;
  border-radius: 8px;
  padding: 12px;
  color: #e2e8f0;
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.55);
  font-size: 0.9rem;
}
.share-popup h3 {
  font-size: 13px;
  margin: 0 0 10px;
  color: #a78bfa;
  font-weight: 600;
}
.share-popup .share-row {
  display: flex;
  gap: 6px;
  align-items: center;
}
.share-popup .share-email {
  flex: 1;
  min-width: 0;
  padding: 6px 8px;
  background: #1e293b;
  border: 1px solid #334155;
  border-radius: 4px;
  color: #e2e8f0;
  font: inherit;
}
.share-popup .share-email:focus { outline: 1px solid #a78bfa; }
.share-popup .share-tier {
  padding: 6px 6px;
  background: #1e293b;
  border: 1px solid #334155;
  border-radius: 4px;
  color: #e2e8f0;
  font: inherit;
}
.share-popup .share-send {
  padding: 6px 12px;
  background: #533483;
  border: 1px solid #a78bfa;
  color: #fff;
  border-radius: 4px;
  cursor: pointer;
  font: inherit;
}
.share-popup .share-send:hover { background: #6d28d9; }
.share-popup .share-send:disabled { opacity: 0.5; cursor: not-allowed; }
.share-popup .share-msg {
  margin-top: 8px;
  font-size: 12px;
  color: #f87171;
}
.share-popup .share-msg.info { color: #22c55e; }
.share-popup .share-list {
  margin-top: 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 180px;
  overflow-y: auto;
}
.share-popup .share-empty {
  color: #64748b;
  font-size: 12px;
  font-style: italic;
}
.share-popup .share-entry {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 6px;
  background: #1e293b;
  border-radius: 4px;
}
.share-popup .share-who {
  flex: 1;
  min-width: 0;
  /* Allow long names+emails to wrap to a second line instead of
     truncating with an ellipsis. break-word keeps URL-style strings
     from blowing out the popup. */
  white-space: normal;
  overflow-wrap: anywhere;
  word-break: break-word;
  line-height: 1.3;
}
.share-popup .share-tier-label {
  font-size: 11px;
  color: #94a3b8;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.share-popup .share-remove {
  background: transparent;
  border: 0;
  color: #94a3b8;
  cursor: pointer;
  padding: 0 4px;
  font-size: 16px;
  line-height: 1;
}
.share-popup .share-remove:hover { color: #ef4444; }
.share-popup .share-status {
  font-size: 10px;
  letter-spacing: 0.03em;
  padding: 1px 5px;
  border-radius: 3px;
  white-space: nowrap;
}
.share-popup .share-status-active .share-status {
  color: #6ee7b7;
  background: rgba(110, 231, 183, 0.1);
}
.share-popup .share-status-pending_signup .share-status {
  color: #fbbf24;
  background: rgba(251, 191, 36, 0.1);
}
.share-popup .share-status-email_failed .share-status {
  color: #fca5a5;
  background: rgba(248, 113, 113, 0.12);
}

/* Workspace = tray + canvas, side-by-side under the header. */
#workspace {
  display: flex;
  flex: 1;
  min-height: 0;
}

/* Drag-and-drop toolbox — left column below the header. Same
   Photoshop-toolbox pattern Trevor signed off on for visualwills,
   ported to grooveboard. Keeps the right-click context menu
   working in parallel. */
#toolbox-tray {
  width: 168px;
  flex-shrink: 0;
  background: #0f172a;
  border-right: 1px solid #0f3460;
  padding: 12px;
  overflow-y: auto;
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
  user-select: none;
  box-sizing: border-box;
}

.toolbox-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 144px;
  padding: 10px 6px;
  border-radius: 8px;
  background: transparent;
  border: 1px solid transparent;
  cursor: grab;
  transition: background .12s, border-color .12s;
  flex-shrink: 0;
}
.toolbox-slot:hover {
  background: rgba(167, 139, 250, 0.12);
  border-color: rgba(167, 139, 250, 0.35);
}
.toolbox-slot:active { cursor: grabbing; }
.toolbox-slot img {
  /* Natural-aspect sizing — matches visualwills' fix for
     html2canvas object-fit issues, and looks correct here too. */
  height: 92px;
  width: auto;
  max-width: 76px;
  pointer-events: none;
}
/* Text-only slot — for specs without a figure (Note, List, etc.).
   A square bordered box with the spec name centered. */
.toolbox-slot .toolbox-text-icon {
  width: 76px;
  height: 76px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(83, 52, 131, 0.2);
  border: 1px solid rgba(167, 139, 250, 0.35);
  border-radius: 6px;
  color: #c4b5fd;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.toolbox-slot .toolbox-label {
  font-size: 13px;
  color: #cbd5e1;
  text-align: center;
  margin-top: 6px;
  line-height: 1.2;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 132px;
}

/* Ghost preview that follows the mouse during drag-to-spawn. */
.toolbox-ghost {
  position: fixed;
  pointer-events: none;
  z-index: 1500;
  opacity: 0.85;
  background: rgba(167, 139, 250, 0.18);
  border: 1px solid rgba(167, 139, 250, 0.6);
}

#canvas {
  flex: 1;
  position: relative;
  overflow: hidden;
  background: #1a1a2e;
  cursor: grab;
}

#canvas.panning { cursor: grabbing; }
#canvas.draw-perimeter { cursor: crosshair; }
#canvas.tray-drag-over {
  box-shadow: inset 0 0 0 3px rgba(167, 139, 250, 0.55);
}

.zoom-controls {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.2rem 0.4rem;
  border: 1px solid #0f3460;
  border-radius: 4px;
  background: #0f172a;
  font-size: 0.85rem;
}

.zoom-controls button {
  border: 0;
  background: transparent;
  color: #e0e0e0;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.3rem;
  font-family: inherit;
}

.zoom-controls button:hover { background: #533483; border-radius: 3px; }

#zoom-label {
  min-width: 3em;
  text-align: center;
  font-variant-numeric: tabular-nums;
  color: #94a3b8;
}

#nodes-layer {
  position: relative;
  /* Effective "world size." Nodes can be placed anywhere in this area and
     connections (SVG below) are sized to match. If we ever need more, bump
     both this and the SVG width/height in index.html in lockstep. */
  width: 20000px;
  height: 20000px;
}

.node {
  position: absolute;
  background: #1e293b;
  border: 1px solid #334155;
  border-radius: 6px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
  display: flex;
  flex-direction: column;
  user-select: none;
  z-index: 2;
  color: #e0e0e0;
}

/* Shared perimeters (Google-Sheets model): perimeter appears on my
   canvas at my personal anchor; contents are canonical shared data
   from the owner's board, translated at render time. Visual parity
   with own content — same purple-dashed perimeter, same node styling
   — with a small "shared by X" badge on each shared node as the
   only distinguishing mark. Phase A: interactive handlers are not
   attached, so writes can't accidentally fan out to the owner's
   board before edit-tier wiring lands. */
/* Cursor parity: edit-tier shared nodes ARE draggable/editable, so the
   header shows the grab cursor just like an own-node header. View-tier
   shared nodes get `node-readonly` from the renderer, which overrides
   back to default. */
.node-shared.node-readonly .node-header {
  cursor: default;
}
.node-shared.node-readonly .node-shared-body {
  cursor: default;
}
.node-shared-badge {
  position: absolute;
  top: -18px;
  right: 0;
  font-size: 10px;
  padding: 2px 6px;
  background: rgba(147, 112, 219, 0.25);
  color: #d6c7f5;
  border-radius: 3px;
  white-space: nowrap;
  pointer-events: none;
}
.node-shared-body {
  user-select: none;
}
/* Shared wires render identically to own wires — no dash, full opacity.
   The "shared by X" label on the perimeter + owner-name badges on nodes
   carry the "this is shared" signal; dashing the wire added visual
   noise without information. */
.wire-shared {
  /* intentionally empty — parity with .wire */
}

/* Connector ports. Per Entity 4 (Cleat), each spec declares its cleat
   layout; the default is 4 at side midpoints (top/right/bottom/left).
   Ports are always faintly visible so users know they exist; opacity goes
   full on hover. Offset 15px outward from the node edge so they don't
   crowd future spec body content. */
.node-port {
  position: absolute;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #533483;
  border: 2px solid #a78bfa;
  cursor: crosshair;
  opacity: 0.55;
  transition: opacity 120ms, background 120ms;
  z-index: 3;
}

.node-port-right  { right:  -23px; top:  50%;   transform: translateY(-50%); }
.node-port-left   { left:   -23px; top:  50%;   transform: translateY(-50%); }
.node-port-top    { top:    -23px; left: 50%;   transform: translateX(-50%); }
.node-port-bottom { bottom: -23px; left: 50%;   transform: translateX(-50%); }

/* Interior latch: sits on top of the image at (xPct, yPct). Inline
   left/top are set from JS; the translate centers the 12px dot on
   that point. Smaller + more visible than edge ports because it
   competes with the image content underneath. */
.node-port-interior {
  width: 12px;
  height: 12px;
  transform: translate(-50%, -50%);
  background: #a78bfa;
  border: 2px solid #0f172a;
  opacity: 1;
  z-index: 5;
}
.node-port-interior:hover { background: #c4b5fd; }

.node:hover .node-port { opacity: 1; }
.node-port:hover { background: #a78bfa; }

.node-resize {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 14px;
  height: 14px;
  cursor: nwse-resize;
  background:
    linear-gradient(135deg, transparent 0 50%, #94a3b8 50% 60%, transparent 60% 75%, #94a3b8 75% 85%, transparent 85%);
  opacity: 0;
  transition: opacity 120ms;
}

.node:hover .node-resize { opacity: 0.7; }
.node-resize:hover { opacity: 1; }
.node.resizing { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6); }

#connections-layer,
#connections-layer-top {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  /* SVG defaults to overflow:hidden, which clipped wires when a node was
     dragged above/left of the 20000x20000 world origin (trivial to hit:
     just drag a node up past y=0). Letting the SVG render outside its
     viewport removes the invisible wall. */
  overflow: visible;
}

/* Default wires sit BEHIND nodes (z:1). Interior-cleat (image-latch)
   wires sit ABOVE nodes (z:4) so they remain visible when emerging
   from inside the image. Interior port dots bump to z:5 so the
   anchor stays on top of its own wire. */
#connections-layer { z-index: 1; }
#connections-layer-top { z-index: 4; }

#connections-layer .wire-hit,
#connections-layer-top .wire-hit {
  pointer-events: stroke;
}

/* During a drag-connect, let the existing wires pass mouse events through
   so the cursor can land on a port that already has wires attached. Prod
   documented this same fix (see index.html search for "connecting svg"):
   without it, the new wire's drop hits an existing wire-hit path instead
   of the port beneath, and the connection silently fails. Symptom was
   "only two wires can tie to any cleat" — the 3rd onward was being
   swallowed. */
body.connecting #connections-layer .wire-hit,
body.connecting #connections-layer-top .wire-hit {
  pointer-events: none;
}

.node-header {
  padding: 0.4rem 1.8rem 0.4rem 0.6rem;
  background: #0f3460;
  border-bottom: 1px solid #334155;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: grab;
  outline: none;
  color: #a78bfa;
  border-top-left-radius: 6px;
  border-top-right-radius: 6px;
}

.node-header.editing {
  background: #1e293b;
  cursor: text;
  color: #e0e0e0;
  box-shadow: inset 0 0 0 2px #533483;
}

.node.dragging .node-header { cursor: grabbing; }
.node.dragging { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6); opacity: 0.95; }

.node.selected {
  border-color: #a78bfa;
  box-shadow: 0 0 0 2px rgba(167, 139, 250, 0.35), 0 1px 4px rgba(0, 0, 0, 0.4);
}
.node.selected.dragging {
  box-shadow: 0 0 0 2px rgba(167, 139, 250, 0.6), 0 4px 12px rgba(0, 0, 0, 0.6);
}

.perimeter {
  position: absolute;
  background: rgba(83, 52, 131, 0.12);
  border: 1px dashed #a78bfa;
  border-radius: 6px;
  z-index: 0;
  user-select: none;
}

.perimeter.dragging { opacity: 0.85; }

/* Preview rectangle shown while the user is drag-drawing a new perimeter
   (triggered by pressing P). Slightly more saturated so it's visually
   distinct as "not yet committed." */
.perimeter-preview {
  position: absolute;
  background: rgba(83, 52, 131, 0.22);
  border: 1.5px dashed #c4b5fd;
  border-radius: 6px;
  pointer-events: none;
  z-index: 2;
}

.perimeter-label {
  position: absolute;
  top: -10px;
  left: 12px;
  background: #533483;
  color: #fff;
  font-size: 0.75rem;
  padding: 2px 8px;
  border-radius: 10px;
  cursor: grab;
  outline: none;
}

.perimeter.dragging .perimeter-label { cursor: grabbing; }

.perimeter-delete {
  position: absolute;
  top: -10px;
  right: 4px;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  background: #533483;
  color: #fff;
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
  border-radius: 50%;
  opacity: 0;
  transition: opacity 120ms;
}

.perimeter:hover .perimeter-delete { opacity: 1; }

/* + button to add a node inside the perimeter. Matches .perimeter-delete
   sizing, sits immediately to the left of × on owned perimeters. Purple
   accent so it doesn't read as destructive. On shared-edit perimeters
   there's no × — .perimeter-add-shared occupies the same top-right slot
   the × normally takes. */
.perimeter-add {
  position: absolute;
  top: -10px;
  right: 28px;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  background: #a78bfa;
  color: #fff;
  font-size: 0.95rem;
  line-height: 1;
  cursor: pointer;
  border-radius: 50%;
  opacity: 0;
  transition: opacity 120ms;
}

.perimeter-add.perimeter-add-shared { right: 4px; }

.perimeter:hover .perimeter-add { opacity: 1; }

.perimeter-resize {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 14px;
  height: 14px;
  cursor: nwse-resize;
  background:
    linear-gradient(135deg, transparent 0 50%, #a78bfa 50% 60%, transparent 60% 75%, #a78bfa 75% 85%, transparent 85%);
  opacity: 0;
}

.perimeter:hover .perimeter-resize { opacity: 0.8; }

.node-delete {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  background: transparent;
  color: #94a3b8;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  border-radius: 3px;
  opacity: 0;
  transition: opacity 120ms, background 120ms, color 120ms;
}

.node:hover .node-delete { opacity: 1; }
.node-delete:hover { background: #7f1d1d; color: #fff; }

.node-body {
  flex: 1;
  padding: 0.6rem;
  font-size: 0.85rem;
  overflow: auto;
  color: #e0e0e0;
}

.node-body-generic {
  color: #64748b;
  font-style: italic;
  font-size: 0.8rem;
}

.node-body-text {
  color: #e0e0e0;
  font-style: normal;
  outline: none;
  white-space: pre-wrap;
  line-height: 1.35;
}

.node-body-text:focus {
  background: #0f172a;
}

/* Groove List — unified list spec with task/title/web_link/yes_no
   item types. Per-row cleats sit just INSIDE the node edge, so the
   left/right padding here makes room for them without crowding the
   row content. Row heights MUST match GL_ROW_H / GL_LINK_ROW_H /
   GL_YESNO_ROW_H in specs.js so cleat yAbs aligns with rows. */
.node-body-groove-list {
  flex: 1;
  display: flex;
  flex-direction: column;
  /* Top + bottom padding for the new outer cleats (top/bottom of
     the node edge). Magnitude must match GL_TOP_PAD / GL_BOTTOM_PAD
     in specs.js (12px each) so cleat yAbs alignment stays correct. */
  padding: 12px 0;
  font-size: 0.82rem;
  overflow: hidden;
}
/* .gl-items: flex-grows to fill node body, scrolls when content
   overflows the resized node height. min-height:0 is required for
   a flex child to actually shrink below its intrinsic content size
   (without it, the list pushes the footer out of view).
   overflow-y:auto chosen over overlay — overlay has poor cross-
   browser support (Firefox ignores it, WebKit-only). */
.gl-items {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
.gl-item {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0 36px;     /* room for inside-edge cleats (16px+2px border
                         at left:8px) plus 8px breathing space */
  height: 32px;
  border-bottom: 1px solid rgba(51, 65, 85, 0.4);
}
.gl-item-web_link {
  height: 56px;
}
/* File item — single line, same height as a task row. Layout is
   drag + icon + title input + delete; the icon swap (vs checkbox)
   keeps the row at 32px so cleat positions don't shift. */
.gl-item-file {
  height: 32px;
}
.gl-file-icon {
  cursor: pointer;
  padding: 0 4px;
  font-size: 0.95rem;
  user-select: none;
  flex-shrink: 0;
}
.gl-file-icon[data-empty="true"] {
  opacity: 0.4;
  cursor: not-allowed;
}
.gl-drag {
  color: #475569;
  cursor: grab;
  font-size: 0.85rem;
  user-select: none;
  width: 12px;
  text-align: center;
}
.gl-drag:active {
  cursor: grabbing;
}
.gl-item.gl-dragging {
  opacity: 0.55;
  background: rgba(167, 139, 250, 0.08);
}
/* Highlight a different list when the user drags an item OUT of its
   original list and over another groove_list node — visual hint that
   "drop here will move the item to this list." Matches the perimeter
   drop-target pattern (dashed indigo outline + soft fill). */
.gl-items.gl-cross-target {
  outline: 2px dashed rgba(99, 102, 241, 0.6);
  outline-offset: 4px;
  background: rgba(99, 102, 241, 0.06);
  border-radius: 4px;
  transition: outline-color 0.1s, background 0.1s;
}

/* Cursor-trailing ghost: a clone of the dragged row pinned to the
   page (position:fixed) and updated each mousemove. Standard kanban
   pattern — gives "you're holding this" feedback while the in-place
   row at 55% opacity still shows the drop insertion point. The
   `list-style: none` overrides the default <li> bullet that would
   otherwise appear when the clone is taken out of its parent <ul>
   context. */
.gl-drag-ghost {
  position: fixed;
  pointer-events: none;
  z-index: 10000;
  list-style: none;
  margin: 0;
  padding: 4px 8px;
  background: rgba(30, 41, 59, 0.96);
  border: 1px solid rgba(99, 102, 241, 0.55);
  border-radius: 4px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.78rem;
  color: #e2e8f0;
  cursor: grabbing;
  /* Slight tilt for a more "lifted" feeling. */
  transform: rotate(-1deg);
}
.gl-check {
  margin: 0;
  cursor: pointer;
}
.gl-text {
  flex: 1;
  background: transparent;
  border: none;
  color: #e2e8f0;
  font-size: 0.82rem;
  outline: none;
  padding: 0.15rem 0.25rem;
  min-width: 0;
}
.gl-text:focus {
  background: #0f172a;
  border-radius: 3px;
}
.gl-item-title .gl-text {
  font-weight: 700;
  color: #c4b5fd;
  font-size: 0.9rem;
}
.gl-item-task.gl-checked .gl-text {
  color: #64748b;
  text-decoration: line-through;
}
/* Note item — fixed-height row that matches GL_NOTE_ROW_MIN_H (60px)
   in specs.js. Long content scrolls INSIDE the textarea (.gl-note
   below) instead of growing the row, because cleats for items below
   are positioned at GL_NOTE_ROW_MIN_H — letting the row grow would
   render those cleats inside the note's visible area (Trevor saw
   3 phantom cleats stacked in one note). */
.gl-item-note {
  height: 60px;
  align-items: stretch;
  padding-top: 4px;
  padding-bottom: 4px;
}
.gl-note {
  flex: 1;
  background: transparent;
  border: none;
  color: #e2e8f0;
  font: inherit;
  font-size: 0.82rem;
  outline: none;
  padding: 0.15rem 0.25rem;
  resize: none;
  min-width: 0;
  line-height: 1.4;
  overflow-y: auto;
}
.gl-note:focus {
  background: #0f172a;
  border-radius: 3px;
}
.gl-note::placeholder {
  color: #64748b;
}
.gl-link-fields {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.gl-url {
  background: transparent;
  border: none;
  color: #94a3b8;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.72rem;
  outline: none;
  padding: 0 0.25rem;
  min-width: 0;
}
.gl-url:focus {
  background: #0f172a;
  color: #cbd5e1;
  border-radius: 3px;
}
.gl-open, .gl-del {
  background: transparent;
  border: none;
  color: #64748b;
  cursor: pointer;
  font-size: 0.85rem;
  padding: 0 0.25rem;
}
.gl-open:hover, .gl-del:hover { color: #e2e8f0; }
.gl-yn-radios {
  display: flex;
  gap: 0.5rem;
  flex-shrink: 0;
}
.gl-yn-opt {
  display: inline-flex;
  align-items: center;
  gap: 0.15rem;
  font-size: 0.78rem;
  color: #cbd5e1;
  cursor: pointer;
}
.gl-yn-opt input[type="radio"] {
  margin: 0;
  cursor: pointer;
}
.gl-footer {
  display: flex;
  gap: 0.4rem;
  padding: 0.4rem 0.5rem;
  border-top: 1px solid rgba(51, 65, 85, 0.5);
  flex-wrap: wrap;
  /* Footer must NEVER shrink or scroll out — it's the action bar that
     adds new rows, so it stays pinned to the bottom of the body even
     when the .gl-items list above is overflowing/scrolling. */
  flex-shrink: 0;
}
.gl-add {
  background: rgba(167, 139, 250, 0.1);
  border: 1px solid rgba(167, 139, 250, 0.3);
  color: #c4b5fd;
  font-size: 0.78rem;
  padding: 0.2rem 0.5rem;
  border-radius: 4px;
  cursor: pointer;
}
.gl-add:hover {
  background: rgba(167, 139, 250, 0.2);
  color: #e2e8f0;
}

/* Inside-edge cleats — siblings of default node-port- side classes,
   but positioned via inline JS (top:Xpx for per-row alignment). */
.node-port-inside-left,
.node-port-inside-right,
.node-port-inside-top,
.node-port-inside-bottom {
  /* Inline left/top/right set by JS. Inherit shape from .node-port. */
}

/* Groove List has header cleats too (left + right at yAbs=16) — they
   sit on top of the .node-header. The default header padding (0.4rem
   1.8rem 0.4rem 0.6rem) is too tight: title text starts ~9px in, but
   the inside-edge cleat occupies x=8..28. So the header text needs
   36px left padding (8 cleat + 20 cleat-width + 8 breathing) and the
   × delete button needs to move clear of the right cleat. */
.node.node-spec-groove_list .node-header {
  padding: 0.4rem 36px 0.4rem 36px;
}
.node.node-spec-groove_list .node-delete {
  right: 32px;
}

.node-body-image {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  padding: 0;
  min-height: 60px;
}

.image-upload-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: #94a3b8;
  font-size: 0.85rem;
  border: 1px dashed #334155;
  border-radius: 4px;
  margin: 0.35rem;
  min-height: 60px;
  text-align: center;
  padding: 0.5rem;
  transition: border-color 0.15s, color 0.15s;
}

.image-upload-empty .image-upload-hint {
  font-size: 0.7rem;
  color: #64748b;
  margin-top: 0.25rem;
}

.image-upload-empty:hover,
.image-upload-empty.image-upload-dragover {
  border-color: #a78bfa;
  color: #e2e8f0;
}
.image-upload-empty.image-upload-dragover {
  background: rgba(167, 139, 250, 0.08);
  border-style: solid;
}

.node-body-image:focus,
.node-body-image:focus-within {
  outline: 2px solid #a78bfa;
  outline-offset: -2px;
  border-radius: 4px;
}

/* === FILE SPEC === */
.node-body-file {
  flex: 1;
  display: flex;
  align-items: stretch;
  justify-content: stretch;
  padding: 0;
  min-height: 70px;
}
.node-body-file:focus,
.node-body-file:focus-within {
  outline: 2px solid #a78bfa;
  outline-offset: -2px;
  border-radius: 4px;
}
.file-upload-empty {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: #94a3b8;
  font-size: 0.85rem;
  border: 1px dashed #334155;
  border-radius: 4px;
  margin: 0.35rem;
  text-align: center;
  padding: 0.6rem;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.file-upload-empty:hover,
.file-upload-empty.file-upload-dragover {
  border-color: #a78bfa;
  color: #e2e8f0;
}
.file-upload-empty.file-upload-dragover {
  background: rgba(167, 139, 250, 0.08);
  border-style: solid;
}
.file-upload-empty .file-upload-hint {
  font-size: 0.7rem;
  color: #64748b;
  margin-top: 0.25rem;
}
.file-card {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.6rem 0.7rem;
  margin: 0.35rem;
  background: rgba(15, 23, 42, 0.4);
  border: 1px solid #334155;
  border-radius: 4px;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}
.file-card:hover {
  border-color: #a78bfa;
  background: rgba(15, 23, 42, 0.6);
}
.file-icon-wrap {
  position: relative;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
}
.file-icon {
  width: 44px;
  height: 44px;
}
.file-icon-tag {
  position: absolute;
  bottom: 2px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 0.55rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  padding: 0 3px;
  background: #0f172a;
  border: 1px solid;
  border-radius: 2px;
  line-height: 1.1;
}
.file-meta {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.file-name {
  font-size: 0.85rem;
  color: #e2e8f0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.file-size {
  font-size: 0.72rem;
  color: #94a3b8;
}

/* Inline preview for previewable file types (PDF + image). Thin bar
   with filename + size at top, click-to-open. Below: full-bleed
   preview that fills the rest of the node. */
.file-preview-wrap {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.file-preview-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.5rem;
  background: rgba(15, 23, 42, 0.6);
  border-bottom: 1px solid #334155;
  cursor: pointer;
  flex-shrink: 0;
}
.file-preview-bar:hover { background: rgba(15, 23, 42, 0.8); }
.file-preview-name {
  flex: 1;
  font-size: 0.78rem;
  color: #e2e8f0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.file-preview-size {
  font-size: 0.7rem;
  color: #94a3b8;
  flex-shrink: 0;
}
.file-preview-slot {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  background: #0f172a;
  overflow: hidden;
}
.file-preview-img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  display: block;
  cursor: pointer;
}

/* === COMMENT SPEC (chat-style log) === */
.node-body-comment {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 0;
  min-height: 100px;
}
.comment-list {
  flex: 1;
  overflow-y: auto;
  padding: 0.4rem 0.55rem 0.3rem;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  min-height: 30px;
}
.comment-empty {
  color: #64748b;
  font-style: italic;
  font-size: 0.78rem;
  text-align: center;
  padding: 0.5rem 0;
}
.comment-entry {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.35rem 0.45rem;
  background: rgba(15, 23, 42, 0.45);
  border-left: 2px solid rgba(167, 139, 250, 0.5);
  border-radius: 3px;
}
.comment-stamp {
  font-size: 0.68rem;
  letter-spacing: 0.02em;
}
.comment-author {
  color: #c4b5fd;
  font-weight: 600;
}
.comment-when {
  color: #94a3b8;
}
.comment-entry-text {
  font-size: 0.85rem;
  color: #e2e8f0;
  line-height: 1.4;
  white-space: pre-wrap;
  word-break: break-word;
}
.comment-bar {
  display: flex;
  align-items: flex-end;
  gap: 0.3rem;
  padding: 0.35rem 0.45rem;
  border-top: 1px solid rgba(51, 65, 85, 0.5);
  background: rgba(15, 23, 42, 0.3);
}
.comment-input {
  flex: 1;
  background: #0f172a;
  border: 1px solid #334155;
  border-radius: 4px;
  color: #e2e8f0;
  font-size: 0.82rem;
  font-family: inherit;
  line-height: 1.35;
  padding: 0.35rem 0.45rem;
  resize: none;
  outline: none;
  max-height: 100px;
  overflow-y: auto;
}
.comment-input:focus {
  border-color: #a78bfa;
}
.comment-input::placeholder {
  color: #64748b;
}
.comment-send {
  background: #a78bfa;
  color: #0f172a;
  border: none;
  border-radius: 4px;
  width: 30px;
  height: 30px;
  font-size: 1rem;
  font-weight: 700;
  cursor: pointer;
  flex-shrink: 0;
  transition: background 0.12s;
}
.comment-send:hover {
  background: #c4b5fd;
}
.comment-send:disabled {
  opacity: 0.5;
  cursor: wait;
}

.update-toast {
  position: fixed;
  right: 16px;
  bottom: 16px;
  background: #0f172a;
  border: 1px solid #334155;
  border-left: 3px solid #a78bfa;
  border-radius: 6px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  padding: 0.6rem 0.8rem 0.6rem 0.85rem;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: #e2e8f0;
  font-size: 0.85rem;
  z-index: 1050;
}
.update-toast-reload {
  padding: 0.25rem 0.7rem;
  background: #a78bfa;
  color: #0f172a;
  border: none;
  border-radius: 4px;
  font-weight: 600;
  cursor: pointer;
}
.update-toast-reload:hover { background: #c4b5fd; }
.update-toast-dismiss {
  background: transparent;
  border: none;
  color: #94a3b8;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.15rem;
}
.update-toast-dismiss:hover { color: #e2e8f0; }

.nav-panel {
  position: fixed;
  background: #1e293b;
  border: 1px solid #334155;
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  padding: 0.5rem 0.8rem 0.8rem;
  min-width: 340px;
  max-width: 400px;
  max-height: 80vh;
  overflow-y: auto;
  z-index: 1040;
  color: #e2e8f0;
  font-size: 0.82rem;
}
.nav-panel h3 {
  margin: 0.2rem 0 0.4rem;
  font-size: 0.95rem;
  color: #a78bfa;
}
.nav-panel h4 {
  margin: 0.6rem 0 0.25rem;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #94a3b8;
  font-weight: 600;
}
.nav-panel dl {
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: 0.75rem;
  row-gap: 0.2rem;
  margin: 0;
}
.nav-panel dt {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: #c4b5fd;
  white-space: nowrap;
}
.nav-panel dd {
  margin: 0;
  color: #cbd5e1;
}

.image-body-img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  user-select: none;
  -webkit-user-drag: none;
}

/* Legacy .checklist-* classes removed — the checklist spec is now
   aliased to grooveList; new rendering uses .gl-* selectors above. */

/* Wires (tethers). Prod uses white/light lines on the dark canvas. */
#connections-layer .wire {
  fill: none;
  stroke: #94a3b8;
  stroke-width: 2;
}

/* Lasso-select preview — a thin light-purple dashed rectangle while the
   user is shift-dragging across empty canvas. Lives inside nodesLayer so
   it pans/zooms with the world and doesn't need viewport tracking. */
.lasso-box {
  position: absolute;
  border: 1px dashed #a78bfa;
  background: rgba(167, 139, 250, 0.10);
  pointer-events: none;
  z-index: 4;
}

/* Floating menu that appears when a wire is clicked. Lets the user
   set flow direction (forward/backward/off) or delete the wire.
   Ported from prod's .conn-flow-menu. */
.conn-flow-menu {
  position: fixed;
  z-index: 1020;
  display: flex;
  align-items: center;
  gap: 4px;
  background: #0f172a;
  border: 1px solid #334155;
  border-radius: 8px;
  padding: 6px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5);
}
.conn-flow-menu .cfm-btn {
  background: #1e293b;
  border: 1px solid #334155;
  color: #cbd5e1;
  cursor: pointer;
  padding: 4px 10px;
  border-radius: 5px;
  font-size: 14px;
  line-height: 1;
  min-width: 30px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 120ms, border-color 120ms, color 120ms;
}
.conn-flow-menu .cfm-btn:hover { background: #334155; color: #e2e8f0; }
.conn-flow-menu .cfm-btn.active {
  background: #533483; border-color: #a78bfa; color: #fff;
}
.conn-flow-menu .cfm-delete:hover {
  background: #7f1d1d; border-color: #ef4444; color: #fff;
}
.conn-flow-menu .cfm-sep {
  width: 1px; height: 22px; background: #334155; margin: 0 2px;
}
/* Color swatches: small round color pills inside the flow menu.
   Active swatch gets a ring; the "default" swatch has a slash icon
   because grey-on-grey wouldn't read. */
.conn-flow-menu .cfm-swatch {
  width: 20px; height: 20px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
  transition: transform 120ms, border-color 120ms;
}
.conn-flow-menu .cfm-swatch:hover { transform: scale(1.15); border-color: #e2e8f0; }
.conn-flow-menu .cfm-swatch.active { border-color: #fff; box-shadow: 0 0 0 1px #0f172a, 0 0 0 3px #a78bfa; }
.conn-flow-menu .cfm-swatch-default {
  background:
    linear-gradient(135deg,
      transparent 0 45%,
      #ef4444 45% 55%,
      transparent 55% 100%),
    #475569;
  background-blend-mode: normal;
}

/* Right-click "Add node" context menu. Fixed-positioned at the cursor
   (screen coords, not world) so it doesn't move if the user pans behind it. */
.context-menu {
  position: fixed;
  background: #0f172a;
  border: 1px solid #533483;
  border-radius: 4px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);
  padding: 4px;
  z-index: 1000;
  min-width: 120px;
  display: flex;
  flex-direction: column;
}

.context-menu .context-menu-label {
  padding: 4px 10px;
  font-size: 11px;
  color: #94a3b8;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border-bottom: 1px solid #1e293b;
  margin-bottom: 2px;
}

.context-menu button {
  text-align: left;
  padding: 6px 10px;
  background: transparent;
  color: #e0e0e0;
  border: 0;
  border-radius: 3px;
  cursor: pointer;
  font-family: inherit;
  font-size: 0.9rem;
}

.context-menu button:hover { background: #533483; color: #fff; }

/* Admin button + popup. Header button visible only to role=admin/owner.
   Popup has the signup kill-switch toggle; future flags slot in here. */
#admin-btn {
  background: rgba(248, 113, 113, 0.12);
  border: 1px solid rgba(248, 113, 113, 0.4);
  color: #fca5a5;
  padding: 0.3rem 0.7rem;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.8rem;
}
#admin-btn:hover {
  background: rgba(248, 113, 113, 0.2);
  color: #fecaca;
}

.admin-popup {
  position: fixed;
  width: 240px;
  background: #0f172a;
  border: 1px solid #334155;
  border-left: 3px solid #f87171;
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
  padding: 0.8rem 0.9rem;
  color: #e2e8f0;
  z-index: 1040;
}
.admin-popup h3 {
  margin: 0 0 0.6rem 0;
  font-size: 0.85rem;
  color: #fca5a5;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.admin-popup .admin-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  cursor: pointer;
  font-size: 0.85rem;
  position: relative;
}
.admin-popup .admin-toggle input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 36px;
  height: 20px;
  background: #475569;
  border-radius: 10px;
  position: relative;
  cursor: pointer;
  transition: background 0.15s;
}
.admin-popup .admin-toggle input[type="checkbox"]:checked {
  background: #34d399;
}
.admin-popup .admin-toggle input[type="checkbox"]::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  background: #f1f5f9;
  border-radius: 50%;
  transition: transform 0.15s;
}
.admin-popup .admin-toggle input[type="checkbox"]:checked::after {
  transform: translateX(16px);
}
.admin-popup .admin-slider { display: none; } /* placeholder for future visual styling */
.admin-popup .admin-hint {
  margin: 0.5rem 0 0 0;
  font-size: 0.72rem;
  color: #94a3b8;
  line-height: 1.35;
}

/* documentFlow spec — node body with template dropdown + role slots
   + Generate button. Vertical stack inside a small node. */
.node-body-document {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  padding: 0.6rem 0.5rem;
  font-size: 0.78rem;
  color: #cbd5e1;
}
.node-body-document .doc-row {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.node-body-document .doc-label {
  font-size: 0.66rem;
  color: #94a3b8;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.node-body-document .doc-select {
  width: 100%;
  background: rgba(255, 255, 255, 0.04);
  color: #e2e8f0;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 6px;
  padding: 0.35rem 0.4rem;
  font-size: 0.78rem;
  font-family: inherit;
  cursor: pointer;
}
.node-body-document .doc-select:hover {
  background: rgba(255, 255, 255, 0.08);
}
/* Dropdown panel uses OS chrome by default — typically white
   background, which made our white text unreadable when the
   dropdown opened. Explicit background + color on `option`
   forces the panel to match the dark theme. */
.node-body-document .doc-select option {
  background: #1e293b;
  color: #e2e8f0;
}
.node-body-document .doc-slots {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.node-body-document .doc-hint {
  font-size: 0.7rem;
  color: #94a3b8;
  font-style: italic;
  padding: 0.2rem 0;
}
.node-body-document .doc-actions {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.node-body-document .doc-generate-btn {
  width: 100%;
  padding: 0.5rem 0.7rem;
  background: linear-gradient(180deg, #6366f1, #4f46e5);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 6px;
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.12s, opacity 0.12s;
}
.node-body-document .doc-generate-btn:hover:not(:disabled) {
  transform: translateY(-1px);
}
.node-body-document .doc-generate-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.node-body-document .doc-status {
  font-size: 0.65rem;
  color: #94a3b8;
  text-align: center;
  font-style: italic;
}
.node-body-document .doc-refresh-btn {
  background: rgba(99, 102, 241, 0.15);
  color: #a5b4fc;
  border: 1px solid rgba(99, 102, 241, 0.4);
  border-radius: 4px;
  padding: 1px 6px;
  font-size: 0.7rem;
  font-family: inherit;
  cursor: pointer;
  margin: 0 2px;
}
.node-body-document .doc-refresh-btn:hover:not(:disabled) {
  background: rgba(99, 102, 241, 0.3);
}
.node-body-document .doc-refresh-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
/* Always-visible refresh in the actions row — secondary action style
   (subtler than the indigo Generate button so it doesn't compete). */
.node-body-document .doc-refresh-btn-top {
  width: 100%;
  padding: 0.4rem 0.7rem;
  background: rgba(99, 102, 241, 0.12);
  color: #c7d2fe;
  border: 1px solid rgba(99, 102, 241, 0.35);
  border-radius: 6px;
  font-size: 0.78rem;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.12s;
}
.node-body-document .doc-refresh-btn-top:hover:not(:disabled) {
  background: rgba(99, 102, 241, 0.22);
}
.node-body-document .doc-refresh-btn-top:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* === PIPELINE SPEC ===
   Sales/prospect tracker. Header bar with title + Add button, then a
   compact table. Rows colored subtly by stage; won/lost rows fade. */
.node-body-pipeline {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 0.4rem 0.5rem 0.5rem;
  gap: 0.4rem;
  overflow: auto;
  min-height: 0;
}
.node-body-pipeline .pipeline-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
}
.node-body-pipeline .pipeline-title {
  flex: 1;
  font-size: 0.95rem;
  font-weight: 600;
  color: #e2e8f0;
  padding: 2px 6px;
  border-radius: 4px;
  outline: none;
  cursor: text;
}
.node-body-pipeline .pipeline-title:hover { background: rgba(255, 255, 255, 0.04); }
.node-body-pipeline .pipeline-title:focus { background: rgba(255, 255, 255, 0.06); }
.node-body-pipeline .pipeline-add-btn {
  background: linear-gradient(180deg, #6366f1, #4f46e5);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 5px;
  padding: 4px 10px;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  flex-shrink: 0;
}
.node-body-pipeline .pipeline-add-btn:hover {
  transform: translateY(-1px);
}
.node-body-pipeline .pipeline-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.74rem;
  color: #e2e8f0;
}
.node-body-pipeline .pipeline-table thead th {
  text-align: left;
  padding: 4px 6px;
  font-size: 0.66rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #94a3b8;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  font-weight: 600;
  user-select: none;
}
.node-body-pipeline .pipeline-table th.pp-sortable {
  cursor: pointer;
}
.node-body-pipeline .pipeline-table th.pp-sortable:hover {
  color: #c7d2fe;
  background: rgba(99, 102, 241, 0.1);
}
.node-body-pipeline .pipeline-table th.pp-sort-active {
  color: #c7d2fe;
  background: rgba(99, 102, 241, 0.18);
}
.node-body-pipeline .pipeline-table th.pp-days,
.node-body-pipeline .pipeline-table th.pp-conf {
  text-align: center;
}
.node-body-pipeline .pipeline-row td {
  padding: 3px 4px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  vertical-align: middle;
}
.node-body-pipeline .pipeline-row.done {
  opacity: 0.55;
}
.node-body-pipeline .pipeline-row.stage-won td:first-child {
  border-left: 2px solid #10b981;
}
.node-body-pipeline .pipeline-row.stage-lost td:first-child {
  border-left: 2px solid #475569;
}
.node-body-pipeline .pipeline-row.stage-hot td:first-child {
  border-left: 2px solid #f59e0b;
}
.node-body-pipeline .pipeline-row.stage-proposal td:first-child {
  border-left: 2px solid #6366f1;
}
.node-body-pipeline .pipeline-table input[type="text"],
.node-body-pipeline .pipeline-table input[type="number"],
.node-body-pipeline .pipeline-table input[type="date"] {
  width: 100%;
  background: rgba(15, 23, 42, 0.4);
  border: 1px solid transparent;
  color: #e2e8f0;
  padding: 2px 4px;
  border-radius: 3px;
  font-size: 0.74rem;
  font-family: inherit;
}
.node-body-pipeline .pipeline-table input:hover {
  background: rgba(15, 23, 42, 0.7);
}
.node-body-pipeline .pipeline-table input:focus {
  outline: none;
  border-color: rgba(99, 102, 241, 0.6);
  background: rgba(15, 23, 42, 0.85);
}
.node-body-pipeline .pipeline-table select {
  width: 100%;
  background: rgba(15, 23, 42, 0.6);
  color: #e2e8f0;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 3px;
  padding: 2px 4px;
  font-size: 0.72rem;
  font-family: inherit;
  cursor: pointer;
}
.node-body-pipeline .pipeline-table select option {
  background: #1e293b;
  color: #e2e8f0;
}
.node-body-pipeline .pp-name { width: 14%; }
.node-body-pipeline .pp-stage { width: 9%; }
.node-body-pipeline .pp-value { width: 10%; }
.node-body-pipeline .pp-conf {
  width: 6%;
  text-align: center;
  white-space: nowrap;
}
.node-body-pipeline .pp-conf input { display: inline-block; width: 70%; }
.node-body-pipeline .pp-pct {
  color: #94a3b8;
  font-size: 0.7rem;
  margin-left: 2px;
}
.node-body-pipeline .pp-next { width: 16%; }
.node-body-pipeline .pp-source { width: 11%; }
.node-body-pipeline .pp-last {
  width: 14%;
  white-space: nowrap;
}
.node-body-pipeline .pp-last input[type="date"] {
  display: inline-block;
  width: calc(100% - 50px);
  margin-right: 4px;
}
.node-body-pipeline .pp-today-btn {
  background: rgba(99, 102, 241, 0.2);
  color: #c7d2fe;
  border: 1px solid rgba(99, 102, 241, 0.4);
  border-radius: 3px;
  padding: 2px 6px;
  font-size: 0.66rem;
  cursor: pointer;
  font-family: inherit;
}
.node-body-pipeline .pp-today-btn:hover {
  background: rgba(99, 102, 241, 0.35);
}
.node-body-pipeline .pp-days {
  width: 4%;
  text-align: center;
  font-weight: 600;
  color: #cbd5e1;
}
.node-body-pipeline .pp-days-never { color: #64748b; }
.node-body-pipeline .pp-days-warning { color: #f59e0b; }
.node-body-pipeline .pp-days-stale { color: #ef4444; }
.node-body-pipeline .pp-actions {
  width: 8%;
  white-space: nowrap;
  text-align: right;
}
.node-body-pipeline .pp-act {
  background: transparent;
  color: #94a3b8;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 3px;
  padding: 2px 5px;
  font-size: 0.78rem;
  cursor: pointer;
  margin-left: 2px;
  line-height: 1;
}
.node-body-pipeline .pp-act:hover { color: #fff; }
.node-body-pipeline .pp-won:hover {
  background: rgba(16, 185, 129, 0.25);
  border-color: rgba(16, 185, 129, 0.55);
}
.node-body-pipeline .pp-lost:hover {
  background: rgba(71, 85, 105, 0.4);
}
.node-body-pipeline .pp-del:hover {
  background: rgba(239, 68, 68, 0.25);
  border-color: rgba(239, 68, 68, 0.55);
}
.node-body-pipeline .pp-empty {
  padding: 1rem;
  text-align: center;
  color: #64748b;
  font-style: italic;
  font-size: 0.78rem;
}
/* === Email-drop + expanded message log === */
/* Visual hint when a file's being dragged over a row. */
.node-body-pipeline .pipeline-row.pp-drop-target {
  background: rgba(99, 102, 241, 0.18);
  outline: 2px dashed rgba(99, 102, 241, 0.6);
  outline-offset: -2px;
}
.node-body-pipeline .pp-expand {
  position: relative;
}
.node-body-pipeline .pp-expand.has-messages {
  background: rgba(99, 102, 241, 0.18);
  border-color: rgba(99, 102, 241, 0.45);
  color: #c7d2fe;
}
.node-body-pipeline .pp-expand.open {
  background: rgba(99, 102, 241, 0.35);
  color: #fff;
}
.node-body-pipeline .pp-msg-count {
  font-size: 0.62rem;
  margin-left: 3px;
  font-weight: 600;
}
.node-body-pipeline .pp-chevron {
  font-size: 0.6rem;
  display: inline-block;
}
/* Sub-row holding the message log. Tall but bounded — Trevor asked
   "up to 4x the row height"; cap at 380px and let the inner div
   scroll if there are many messages. */
.node-body-pipeline .pp-messages-row td {
  background: rgba(15, 23, 42, 0.65);
  padding: 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.node-body-pipeline .pp-messages {
  /* No fixed max-height — let the message log grow with content; the
     outer .node-body-pipeline overflow:auto is the only scroll, so
     resizing the node smaller naturally reflows the body text and
     scrolls cleanly inside the node. */
  padding: 6px 12px 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.node-body-pipeline .pp-messages-empty {
  color: #64748b;
  font-style: italic;
  font-size: 0.74rem;
  padding: 8px 0;
}
.node-body-pipeline .pp-msg-item {
  background: rgba(30, 41, 59, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 4px;
  overflow: hidden;
}
.node-body-pipeline .pp-msg-header {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 5px 8px;
  cursor: pointer;
  font-size: 0.74rem;
  user-select: none;
}
.node-body-pipeline .pp-msg-header:hover {
  background: rgba(255, 255, 255, 0.04);
}
.node-body-pipeline .pp-msg-date {
  color: #94a3b8;
  font-size: 0.68rem;
  white-space: nowrap;
  flex-shrink: 0;
}
.node-body-pipeline .pp-msg-subj {
  color: #e2e8f0;
  font-weight: 600;
  flex-shrink: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.node-body-pipeline .pp-msg-del {
  margin-left: auto;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.1);
  color: #94a3b8;
  border-radius: 3px;
  padding: 0 4px;
  cursor: pointer;
  font-size: 0.78rem;
  line-height: 1;
  flex-shrink: 0;
}
.node-body-pipeline .pp-msg-del:hover {
  color: #fff;
  background: rgba(239, 68, 68, 0.25);
  border-color: rgba(239, 68, 68, 0.5);
}
.node-body-pipeline .pp-msg-body {
  margin: 0;
  padding: 6px 10px 8px;
  font-family: ui-monospace, 'Cascadia Code', Menlo, Consolas, monospace;
  font-size: 0.7rem;
  line-height: 1.45;
  color: #cbd5e1;
  background: rgba(15, 23, 42, 0.4);
  white-space: pre-wrap;
  word-wrap: break-word;
  border-top: 1px solid rgba(255, 255, 255, 0.04);
  /* No fixed height — let the body grow to fit its content. The
     node body's overflow:auto handles scroll if the user resizes
     the node smaller than the content. The .collapsed rule below
     still caps the preview to 2.6em for long messages until the
     user clicks the header to expand. */
}
.node-body-pipeline .pp-msg-body.collapsed {
  max-height: 2.6em;
  overflow: hidden;
  position: relative;
}
.node-body-pipeline .pp-msg-body.collapsed::after {
  content: '… (click header to expand)';
  position: absolute;
  right: 8px;
  bottom: 2px;
  font-size: 0.62rem;
  color: #64748b;
  font-family: system-ui, -apple-system, sans-serif;
  background: rgba(15, 23, 42, 0.85);
  padding: 0 4px;
  border-radius: 3px;
}
.node-body-pipeline .pp-msg-truncated {
  margin-top: 6px;
  color: #f59e0b;
  font-size: 0.66rem;
  font-style: italic;
}

/* Per-user feature-flag section in the admin popup */
.admin-popup { max-height: 600px; overflow-y: auto; min-width: 360px; }
.admin-sep {
  border: none;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  margin: 0.8rem 0 0.6rem;
}
.admin-subtitle {
  font-size: 0.78rem;
  color: #cbd5e1;
  margin: 0 0 0.4rem;
  font-weight: 600;
}
.admin-users {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-bottom: 0.4rem;
}
.admin-user-row {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.02);
}
.admin-user-name {
  font-size: 0.78rem;
  line-height: 1.3;
  color: #e2e8f0;
}
.admin-user-name strong {
  font-weight: 600;
  color: #fff;
}
.admin-user-role {
  font-size: 0.66rem;
  color: #94a3b8;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.admin-user-email {
  font-size: 0.7rem;
  color: #94a3b8;
}
.admin-user-toggles {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.8rem;
}
.admin-feature-toggle {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 0.7rem;
  color: #cbd5e1;
  cursor: pointer;
}
.admin-feature-toggle input[type="checkbox"] {
  width: 13px;
  height: 13px;
  margin: 0;
  accent-color: #6366f1;
}

/* Signup-closed note above auth form when admin has paused signups. */
#signup-closed-note {
  margin: 0.5rem 0;
  padding: 0.5rem 0.7rem;
  background: rgba(248, 113, 113, 0.1);
  border-left: 3px solid #f87171;
  border-radius: 4px;
  color: #fecaca;
  font-size: 0.85rem;
}
.auth-legal-links {
  margin-top: 1.2rem;
  font-size: 0.7rem;
  color: #64748b;
  text-align: center;
  line-height: 1.4;
}
.auth-legal-links a {
  color: #94a3b8;
}
.auth-legal-links a:hover {
  color: #c4b5fd;
}

/* Invite-from-link banner shown on auth screen when URL has #invite=…  */
#invite-banner {
  margin: 0.5rem 0;
  padding: 0.5rem 0.7rem;
  background: rgba(110, 231, 183, 0.12);
  border-left: 3px solid #6ee7b7;
  border-radius: 4px;
  color: #6ee7b7;
  font-size: 0.85rem;
}

/* Invite popup — header button → small panel with email + generate link. */
#invite-btn {
  background: rgba(110, 231, 183, 0.1);
  border: 1px solid rgba(110, 231, 183, 0.3);
  color: #6ee7b7;
  padding: 0.3rem 0.7rem;
  border-radius: 4px;
  cursor: pointer;
  font-size: 0.8rem;
}
#invite-btn:hover {
  background: rgba(110, 231, 183, 0.18);
  color: #a7f3d0;
}

.invite-popup {
  position: fixed;
  width: 320px;
  background: #0f172a;
  border: 1px solid #334155;
  border-left: 3px solid #6ee7b7;
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
  padding: 0.85rem 1rem;
  color: #e2e8f0;
  z-index: 1040;
}
.invite-popup h3 {
  margin: 0 0 0.4rem 0;
  font-size: 0.85rem;
  color: #6ee7b7;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.invite-quota {
  margin: 0 0 0.6rem 0;
  font-size: 0.78rem;
  color: #94a3b8;
}
.invite-row {
  display: flex;
  gap: 0.4rem;
  margin-bottom: 0.5rem;
}
.invite-row .invite-email {
  flex: 1;
  background: #1e293b;
  border: 1px solid #475569;
  color: #e2e8f0;
  padding: 0.3rem 0.5rem;
  border-radius: 3px;
  font-size: 0.82rem;
}
.invite-row .invite-generate {
  background: #6ee7b7;
  color: #0f172a;
  border: none;
  border-radius: 3px;
  padding: 0.3rem 0.7rem;
  font-size: 0.82rem;
  font-weight: 600;
  cursor: pointer;
}
.invite-row .invite-generate:hover { background: #a7f3d0; }
.invite-row .invite-generate:disabled { opacity: 0.5; cursor: wait; }

.invite-result {
  display: flex;
  gap: 0.4rem;
  margin: 0.5rem 0;
  padding: 0.4rem;
  background: rgba(110, 231, 183, 0.07);
  border-radius: 4px;
}
.invite-result .invite-link {
  flex: 1;
  background: #1e293b;
  border: 1px solid #475569;
  color: #e2e8f0;
  padding: 0.3rem 0.5rem;
  border-radius: 3px;
  font-size: 0.72rem;
  font-family: ui-monospace, "Cascadia Code", monospace;
}
.invite-result .invite-copy {
  background: transparent;
  border: 1px solid #6ee7b7;
  color: #6ee7b7;
  padding: 0.3rem 0.6rem;
  border-radius: 3px;
  font-size: 0.78rem;
  cursor: pointer;
}
.invite-result .invite-copy:hover { background: rgba(110, 231, 183, 0.15); }

.invite-msg {
  margin: 0.4rem 0 0 0;
  font-size: 0.78rem;
}
.invite-msg.info { color: #6ee7b7; }
.invite-msg.error { color: #fca5a5; }
.invite-hint {
  margin: 0.5rem 0 0 0;
  font-size: 0.72rem;
  color: #94a3b8;
  line-height: 1.4;
}

/* Feature-preset block on the invite popup. Stack: label+dropdown
   on one row, checkbox list below. All-off by default; admin
   ticks what to grant. */
.invite-features {
  margin: 0.6rem 0;
  padding: 0.6rem 0.7rem;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.02);
}
.invite-features-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  font-size: 0.72rem;
  color: #cbd5e1;
  font-weight: 600;
  margin-bottom: 0.5rem;
}
.invite-preset {
  background: rgba(255, 255, 255, 0.06);
  color: #e2e8f0;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 4px;
  padding: 0.25rem 0.4rem;
  font-size: 0.74rem;
  font-family: inherit;
  cursor: pointer;
}
/* Dropdown panel — same fix as .doc-select option (OS-chrome
   white panel + our white text = unreadable). */
.invite-preset option {
  background: #1e293b;
  color: #e2e8f0;
}
.invite-feature-list {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.invite-feature-toggle {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  font-size: 0.74rem;
  color: #cbd5e1;
  cursor: pointer;
  padding: 4px 0;
}
.invite-feature-toggle input[type="checkbox"] {
  width: 13px;
  height: 13px;
  margin: 2px 0 0 0;
  accent-color: #6366f1;
  flex-shrink: 0;
}
.invite-feature-row-text {
  display: flex;
  flex-direction: column;
  gap: 1px;
  line-height: 1.3;
}
.invite-feature-name {
  font-weight: 600;
  color: #e2e8f0;
}
.invite-feature-scope {
  color: #94a3b8;
  font-size: 0.7rem;
  font-style: italic;
}
.invite-features-info {
  margin: 0.4rem 0 0.5rem;
  padding: 0.4rem 0.5rem;
  background: rgba(99, 102, 241, 0.08);
  border-left: 2px solid rgba(99, 102, 241, 0.5);
  border-radius: 3px;
  font-size: 0.7rem;
  color: #cbd5e1;
  line-height: 1.4;
}
.invite-features-info strong {
  color: #e2e8f0;
}

/* === PERSON SPEC ===
   Chromeless visual: the node frame goes transparent so the
   silhouette PNG floats with the canvas behind it. The standard
   node-header collapses into a small pill-tag at the top-left
   (still functions as the drag handle). Cleats sit around the
   figure with breathing room above (bigger top padding). The
   contact chevron drops the panel below (kept code simple per
   Trevor's call). */
.node.node-spec-person {
  background: transparent;
  border: none;
  box-shadow: none;
  overflow: visible;
}
/* Header pill is hidden on person nodes — the editable .person-name
   field above the silhouette is the single name source; the title
   pill at top-left was redundant AND its container (.node-body)
   covered the × delete button. The silhouette image is the drag
   handle (img.cursor:grab + explicit startNodeDrag). */
.node.node-spec-person .node-header {
  display: none;
}
/* Lift the × above the body so its clicks aren't intercepted —
   .node-body-person comes after .node-delete in DOM order, so without
   a stacking nudge the body wins all pointer events in the top-right
   corner. */
.node.node-spec-person .node-delete {
  top: 4px;
  right: 4px;
  z-index: 3;
}
.node-body-person {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 1.5rem 0.4rem 0.4rem; /* top padding gives the top cleat breathing room above the figure */
  gap: 0.3rem;
  overflow-y: auto;
  position: relative; /* anchors .person-mode-pill */
}

/* Mode-switcher pill: TOP-LEFT — replaces the (now-hidden) title
   pill. Top-right is uncontested for the × delete button. Click
   cycles detailed ↔ simple. The data-mode attribute on
   .node-body-person drives the visibility rules below. */
.person-mode-pill {
  position: absolute;
  top: 4px;
  left: 4px;
  z-index: 2;
  font: inherit;
  font-size: 9px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  color: #aab;
  border: 1px solid rgba(255, 255, 255, 0.14);
  cursor: pointer;
  user-select: none;
  transition: background .15s, color .15s;
}
.person-mode-pill:hover {
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
}

/* Lock toggle: sliding switch sitting just below the mode pill in
   the body's top-left. Track 38×18, thumb 14×14 with the current
   icon (🔓 unlocked / 🔒 locked). Locked variant uses an amber tint
   to stand out. Click flips node.locked via window.updateNodeLocked. */
.person-lock-toggle {
  position: absolute;
  top: 30px;
  left: 4px;
  z-index: 2;
  width: 38px;
  height: 18px;
  padding: 0;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.14);
  cursor: pointer;
  display: flex;
  align-items: center;
  user-select: none;
  transition: background 0.15s, border-color 0.15s;
}
.person-lock-toggle:hover {
  background: rgba(255, 255, 255, 0.12);
}
.person-lock-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  margin: 1px;
  border-radius: 50%;
  background: #cbd5e1;
  font-size: 9px;
  line-height: 1;
  transform: translateX(0);
  transition: transform 0.18s, background 0.15s;
}
.person-lock-toggle.locked {
  background: rgba(212, 160, 23, 0.22);
  border-color: rgba(212, 160, 23, 0.45);
}
.person-lock-toggle.locked:hover {
  background: rgba(212, 160, 23, 0.36);
}
.person-lock-toggle.locked .person-lock-thumb {
  transform: translateX(20px);
  background: #fcd34d;
}
/* Subtle amber outline on the body so a locked person node is
   visually distinct from an unlocked one at a glance. */
.node-body-person.person-locked {
  box-shadow: inset 0 0 0 2px rgba(252, 211, 77, 0.20);
  border-radius: 6px;
}

/* Simple mode: hide vault, documents, executor-details, notes;
   hide secondary phones (home, work) and the secondary email (work);
   hide the star buttons since "preferred" is meaningless when only
   one of each shows. The data-mode attr is "simple" or "detailed"
   today; legacy data with the old "family-tree" / "executor" values
   is normalized to these in renderBody before the attr is set. */
.node-body-person[data-mode="simple"] .person-chevron--executor,
.node-body-person[data-mode="simple"] .person-chevron--vault,
.node-body-person[data-mode="simple"] .person-chevron--documents,
.node-body-person[data-mode="simple"] .person-chevron--income,
.node-body-person[data-mode="simple"] .person-assets-row,
.node-body-person[data-mode="simple"] .person-notes,
.node-body-person[data-mode="simple"] .person-star,
.node-body-person[data-mode="simple"] .person-field-row[data-group="phones"][data-key="home"],
.node-body-person[data-mode="simple"] .person-field-row[data-group="phones"][data-key="work"],
.node-body-person[data-mode="simple"] .person-field-row[data-group="emails"][data-key="work"] {
  display: none;
}

/* === T4 income chevron + per-year rows === */
.person-income-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 4px 0;
}
.person-income-empty {
  color: #64748b;
  font-style: italic;
  font-size: 0.72rem;
  padding: 4px 0;
}
.person-income-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
.person-income-year {
  flex: 0 0 auto;
  color: #cbd5e1;
  font-size: 0.74rem;
  font-weight: 500;
}
.person-income-value {
  flex: 1;
  background: rgba(15, 23, 42, 0.4);
  border: 1px solid transparent;
  color: #e2e8f0;
  padding: 3px 6px;
  border-radius: 3px;
  font-size: 0.74rem;
  font-family: inherit;
}
.person-income-value:hover { background: rgba(15, 23, 42, 0.7); }
.person-income-value:focus {
  outline: none;
  border-color: rgba(99, 102, 241, 0.6);
  background: rgba(15, 23, 42, 0.85);
}
.person-income-del {
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.1);
  color: #94a3b8;
  border-radius: 3px;
  padding: 0 6px;
  cursor: pointer;
  font-size: 0.85rem;
  line-height: 1;
}
.person-income-del:hover {
  color: #fff;
  background: rgba(239, 68, 68, 0.25);
  border-color: rgba(239, 68, 68, 0.5);
}
.person-income-add {
  background: rgba(99, 102, 241, 0.15);
  color: #c7d2fe;
  border: 1px solid rgba(99, 102, 241, 0.4);
  border-radius: 4px;
  padding: 4px 10px;
  font-size: 0.74rem;
  font-family: inherit;
  cursor: pointer;
  margin-top: 6px;
  width: 100%;
}
.person-income-add:hover {
  background: rgba(99, 102, 241, 0.3);
}

/* === Assets row === */
.person-assets-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 6px;
}
.person-assets-label {
  flex: 0 0 auto;
  color: #cbd5e1;
  font-size: 0.74rem;
  font-weight: 500;
}
.person-assets-value {
  flex: 1;
  background: rgba(15, 23, 42, 0.4);
  border: 1px solid transparent;
  color: #e2e8f0;
  padding: 3px 6px;
  border-radius: 3px;
  font-size: 0.74rem;
  font-family: inherit;
}
.person-assets-value:hover { background: rgba(15, 23, 42, 0.7); }
.person-assets-value:focus {
  outline: none;
  border-color: rgba(99, 102, 241, 0.6);
  background: rgba(15, 23, 42, 0.85);
}

/* Executor details panel: stack textarea/input rows with labels above
   inputs, give the textareas a touch more height than the single-line
   contact fields. */
.person-field-row--exec {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: 6px;
}
.person-field-row--exec .person-field-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: #889;
}
.person-field-row--exec .person-field-input {
  width: 100%;
  resize: vertical;
  min-height: 1.6em;
  font-family: inherit;
  font-size: 12px;
}

/* Hide the chunky default browser scrollbar across every scrollable
   surface inside the app — node bodies, popups, lists. Content
   still scrolls via wheel + touch + keyboard. Notion / Linear /
   Discord pattern. Standalone HTML pages (legal/) keep the default. */
.node-body,
.node-body-person,
.share-list,
.comment-list,
.person-notes-list,
.invite-popup,
.share-popup,
.context-menu,
.nav-panel,
.comment-input,
.person-notes-input {
  scrollbar-width: none;
}
.node-body::-webkit-scrollbar,
.node-body-person::-webkit-scrollbar,
.share-list::-webkit-scrollbar,
.comment-list::-webkit-scrollbar,
.person-notes-list::-webkit-scrollbar,
.invite-popup::-webkit-scrollbar,
.share-popup::-webkit-scrollbar,
.context-menu::-webkit-scrollbar,
.nav-panel::-webkit-scrollbar,
.comment-input::-webkit-scrollbar,
.person-notes-input::-webkit-scrollbar {
  display: none;
}

/* Groove List body scrollbar — thin, purple-tinted, barely visible
   at rest, more obvious on hover. Replaces the previous "hide
   completely" rule because shrinking a long list now needs the user
   to be able to scroll. Purple matches the cleat/wire palette
   (a78bfa) so it reads as "part of GrooveBoard" rather than the
   chunky native gray bar. Shared by the list body (.gl-items) AND
   the note textarea (.gl-note), which scrolls internally now that
   note rows are pinned to a fixed height. */
.gl-items,
.gl-note {
  scrollbar-width: thin;
  scrollbar-color: rgba(167, 139, 250, 0.45) transparent;
}
.gl-items::-webkit-scrollbar,
.gl-note::-webkit-scrollbar {
  width: 6px;
}
.gl-items::-webkit-scrollbar-track,
.gl-note::-webkit-scrollbar-track {
  background: transparent;
}
.gl-items::-webkit-scrollbar-thumb,
.gl-note::-webkit-scrollbar-thumb {
  background: rgba(167, 139, 250, 0.45);
  border-radius: 3px;
}
.gl-items::-webkit-scrollbar-thumb:hover,
.gl-note::-webkit-scrollbar-thumb:hover {
  background: rgba(167, 139, 250, 0.75);
}
.person-top {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
}

/* Name input: sits above the silhouette in the transparent space
   above the head. Always editable (contentEditable), no border/box.
   Reads/writes spec_data.title; same source of truth as the
   top-left .node-header pill (which acts as the drag handle but is
   visually obscured by the body on chromeless person nodes — this
   in-body input is the discoverable affordance). */
.person-name {
  text-align: center;
  font-size: 0.95rem;
  font-weight: 600;
  color: #e2e8f0;
  padding: 2px 6px;
  border-radius: 4px;
  outline: none;
  cursor: text;
  user-select: text;
  min-height: 1.1em;
  width: 100%;
  word-break: break-word;
}
.person-name:hover {
  background: rgba(255, 255, 255, 0.04);
}
.person-name:focus {
  background: rgba(255, 255, 255, 0.08);
  box-shadow: 0 0 0 1px rgba(167, 139, 250, 0.4);
}
/* Placeholder shown when empty — uses :empty + ::before so it
   disappears the instant the user types. */
.person-name:empty::before {
  content: attr(data-placeholder);
  color: rgba(226, 232, 240, 0.4);
  font-weight: 400;
  font-style: italic;
}
.person-img {
  max-width: 100%;
  max-height: 200px;
  object-fit: contain;
  display: block;
  user-select: none;
  pointer-events: none;
}
.person-age-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  width: 100%;
  font-size: 0.78rem;
  background: rgba(15, 23, 42, 0.85);
  border: 1px solid rgba(51, 65, 85, 0.5);
  border-radius: 4px;
  padding: 0.4rem 0.45rem;
}
.person-dob {
  background: #0f172a;
  border: 1px solid #334155;
  border-radius: 3px;
  color: #e2e8f0;
  padding: 0.2rem 0.35rem;
  font: inherit;
  font-size: 0.78rem;
}
.person-dob:focus {
  outline: 1px solid #a78bfa;
}
.person-dob::-webkit-calendar-picker-indicator {
  filter: invert(1) brightness(0.9);
  cursor: pointer;
  opacity: 0.85;
}
.person-dob::-webkit-calendar-picker-indicator:hover {
  opacity: 1;
}
.person-age {
  color: #c4b5fd;
  font-size: 0.78rem;
  text-align: center;
  font-weight: 600;
}
.person-age.empty {
  color: #64748b;
  font-style: italic;
  font-weight: normal;
}

/* === Present ↔ Departed slider ===
   Two text labels flank a CSS-styled checkbox; the active label
   brightens (green for Present, red for Departed). When toggled
   to Departed, the date-of-passing input becomes visible. */
.person-life-toggle {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin-top: 0.3rem;
  cursor: pointer;
  user-select: none;
}
.person-life-label {
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  color: #64748b;
  font-weight: 500;
  transition: color .15s;
}
.person-life-label.person-life-active.person-life-present { color: #4ade80; }
.person-life-label.person-life-active.person-life-departed { color: #f43f5e; }
.person-life-input {
  appearance: none; -webkit-appearance: none;
  position: relative;
  width: 32px; height: 16px;
  background: #334155;
  border-radius: 9px;
  cursor: pointer;
  outline: none;
  transition: background .15s;
  flex-shrink: 0;
  margin: 0;
}
.person-life-input:checked { background: #7f1d1d; }
.person-life-input::before {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: 12px; height: 12px;
  border-radius: 50%;
  background: #e2e8f0;
  transition: transform .15s, background .15s;
}
.person-life-input:checked::before {
  transform: translateX(16px);
  background: #fca5a5;
}

.person-death-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2rem;
  margin-top: 0.4rem;
  font-size: 0.75rem;
}
.person-death-row[hidden] { display: none; }
.person-death-label {
  color: #94a3b8;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.7rem;
}
.person-death-input {
  background: #0f172a;
  border: 1px solid #7f1d1d;
  border-radius: 3px;
  color: #e2e8f0;
  padding: 0.2rem 0.35rem;
  font: inherit;
  font-size: 0.78rem;
  color-scheme: dark;
}
.person-death-input:focus {
  outline: 1px solid #f43f5e;
}

.person-chevron {
  margin-top: 0.2rem;
  display: flex;
  flex-direction: column;
}
.node.node-spec-person .person-chevron-panel {
  /* When chevron is open, the panel gets its own card background
     so it stays readable against the now-transparent node frame. */
  background: rgba(15, 23, 42, 0.9);
  border: 1px solid rgba(51, 65, 85, 0.6);
  border-radius: 4px;
  padding: 0.5rem;
  margin-top: 0.3rem;
}
.node.node-spec-person .person-chevron-header {
  background: rgba(15, 23, 42, 0.7);
  border: 1px solid rgba(51, 65, 85, 0.5);
  border-radius: 4px;
  padding: 0.25rem 0.5rem;
  text-align: center;
}
.person-chevron-header {
  width: 100%;
  text-align: left;
  background: transparent;
  border: 0;
  color: #c4b5fd;
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  padding: 0.2rem 0;
}
.person-chevron-header:hover {
  color: #e2e8f0;
}
.person-chevron-panel {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding-top: 0.3rem;
}
.person-field-group {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.person-field-row {
  display: flex;
  align-items: center;
  gap: 0.3rem;
}
.person-star {
  background: transparent;
  border: 0;
  color: #475569;
  cursor: pointer;
  font-size: 0.95rem;
  line-height: 1;
  padding: 0 2px;
  width: 18px;
  flex-shrink: 0;
}
.person-star.starred {
  color: #fbbf24;
}
.person-star:hover {
  color: #fcd34d;
}
.person-field-label {
  font-size: 0.7rem;
  color: #94a3b8;
  width: 56px;
  flex-shrink: 0;
}
.person-field-input {
  flex: 1;
  background: #0f172a;
  border: 1px solid #334155;
  border-radius: 3px;
  color: #e2e8f0;
  padding: 0.2rem 0.4rem;
  font: inherit;
  font-size: 0.78rem;
  min-width: 0;
}
.person-field-input:focus {
  outline: 1px solid #a78bfa;
}

.person-notes {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  border-top: 1px solid rgba(51, 65, 85, 0.5);
  padding-top: 0.4rem;
}
.person-notes-title {
  font-size: 0.72rem;
  color: #c4b5fd;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.person-notes-empty {
  font-size: 0.75rem;
  color: #64748b;
  font-style: italic;
}
.person-notes-list {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  max-height: 180px;
  overflow-y: auto;
}
.person-note-entry {
  background: rgba(15, 23, 42, 0.4);
  border-left: 2px solid rgba(167, 139, 250, 0.5);
  padding: 0.3rem 0.45rem;
  border-radius: 3px;
}
.person-note-entry.has-full {
  cursor: pointer;
}
.person-note-entry.has-full:hover {
  background: rgba(15, 23, 42, 0.6);
}
.person-note-stamp {
  font-size: 0.65rem;
  color: #94a3b8;
}
.person-note-author {
  color: #c4b5fd;
  font-weight: 600;
}
.person-note-subject {
  font-size: 0.78rem;
  color: #e2e8f0;
  margin-top: 0.1rem;
}
.person-note-full {
  font-size: 0.78rem;
  color: #e2e8f0;
  margin-top: 0.1rem;
  white-space: pre-wrap;
}
.person-note-entry.expanded .person-note-subject {
  display: none;
}
.person-notes-bar {
  display: flex;
  align-items: flex-end;
  gap: 0.3rem;
  border-top: 1px solid rgba(51, 65, 85, 0.4);
  padding-top: 0.3rem;
}
.person-notes-input {
  flex: 1;
  background: #0f172a;
  border: 1px solid #334155;
  border-radius: 3px;
  color: #e2e8f0;
  font: inherit;
  font-size: 0.78rem;
  line-height: 1.35;
  padding: 0.25rem 0.4rem;
  resize: none;
  outline: none;
  max-height: 80px;
  overflow-y: auto;
}
.person-notes-input:focus {
  border-color: #a78bfa;
}
.person-notes-send {
  background: #a78bfa;
  color: #0f172a;
  border: none;
  border-radius: 3px;
  width: 26px;
  height: 26px;
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  flex-shrink: 0;
}
.person-notes-send:hover { background: #c4b5fd; }
.person-notes-send:disabled { opacity: 0.5; cursor: wait; }

/* Phase 2 — Vault + Documents chevrons. Reuses the chevron header /
   panel + field-row look. Adds a labeled upload list affordance. */
.person-vault-hint {
  font-size: 0.65rem;
  color: #fbbf24;
  font-style: italic;
  background: rgba(251, 191, 36, 0.08);
  border-left: 2px solid rgba(251, 191, 36, 0.55);
  padding: 0.2rem 0.4rem;
  border-radius: 2px;
  margin-bottom: 0.1rem;
}
.person-uploads-section {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  border-top: 1px solid rgba(51, 65, 85, 0.5);
  padding-top: 0.4rem;
}
.person-uploads-title {
  font-size: 0.7rem;
  color: #c4b5fd;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.person-uploads-list {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.person-uploads-empty {
  font-size: 0.72rem;
  color: #64748b;
  font-style: italic;
}
.person-upload-row {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  background: rgba(15, 23, 42, 0.4);
  border-left: 2px solid rgba(167, 139, 250, 0.5);
  padding: 0.2rem 0.4rem;
  border-radius: 3px;
  font-size: 0.72rem;
}
.person-upload-link {
  flex: 1;
  color: #c4b5fd;
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
}
.person-upload-link:hover {
  color: #e2e8f0;
  text-decoration: underline;
}
.person-upload-size {
  color: #64748b;
  font-size: 0.65rem;
  flex-shrink: 0;
}
.person-upload-del {
  background: transparent;
  border: 0;
  color: #64748b;
  cursor: pointer;
  padding: 0 4px;
  font-size: 0.85rem;
  line-height: 1;
  flex-shrink: 0;
}
.person-upload-del:hover {
  color: #f87171;
}
.person-upload-bar {
  display: flex;
  margin-top: 0.2rem;
}
.person-upload-btn {
  flex: 1;
  background: rgba(167, 139, 250, 0.15);
  border: 1px dashed rgba(167, 139, 250, 0.5);
  border-radius: 3px;
  color: #c4b5fd;
  cursor: pointer;
  font: inherit;
  font-size: 0.72rem;
  padding: 0.25rem 0.45rem;
}
.person-upload-btn:hover {
  background: rgba(167, 139, 250, 0.25);
  color: #e2e8f0;
}
.person-upload-btn:disabled {
  opacity: 0.5;
  cursor: wait;
}

/* === LINKER SPEC ===
   Chromeless: transparent node frame, no visible border. The chain
   image itself is the drag handle (see specs.js — uses
   window.startNodeDrag). The × delete button still appears in the
   corner so users can remove it. The standard .node-header collapses
   to zero height so it doesn't take any space; the image carries
   all the drag affordance. */
.node.node-spec-linker {
  background: transparent;
  border: none;
  box-shadow: none;
  overflow: visible;
}
.node.node-spec-linker .node-header {
  display: none;
}
.node.node-spec-linker .node-delete {
  /* Keep the × visible but discrete — small, top-right corner of
     the chain image area. */
  top: 2px;
  right: 2px;
  background: rgba(15, 23, 42, 0.7);
  border-radius: 50%;
  width: 18px;
  height: 18px;
  font-size: 0.85rem;
  line-height: 18px;
  text-align: center;
  padding: 0;
  opacity: 0.7;
}
.node.node-spec-linker .node-delete:hover {
  opacity: 1;
}
.node-body-linker {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 0.4rem 0.5rem;
  gap: 0.3rem;
  background: transparent;
}
.linker-img {
  max-width: 100%;
  max-height: 90px;
  object-fit: contain;
  user-select: none;
  pointer-events: none;
}
.linker-select {
  width: 100%;
  background: rgba(15, 23, 42, 0.92);
  border: 1px solid rgba(51, 65, 85, 0.7);
  border-radius: 4px;
  color: #e2e8f0;
  padding: 0.3rem 0.45rem;
  font: inherit;
  font-size: 0.8rem;
  cursor: pointer;
}
.linker-select:focus {
  outline: 1px solid #a78bfa;
}
.linker-date {
  width: 100%;
  background: rgba(15, 23, 42, 0.92);
  border: 1px solid rgba(51, 65, 85, 0.7);
  border-radius: 4px;
  color: #e2e8f0;
  padding: 0.3rem 0.45rem;
  font: inherit;
  font-size: 0.8rem;
  color-scheme: dark;
  box-sizing: border-box;
}
.linker-date:focus {
  outline: 1px solid #a78bfa;
}

/* === FLOURISH SPEC ===
   Decorative transparent-PNG overlay. Frame is transparent, no
   border, no shadow. Sits ABOVE the regular node layer so it can
   float on top of other nodes (z-index 8 — higher than the
   default node z-index of 2 and the cleat ports at z-index 5).
   The × delete is the only visible chrome; the inner img uses
   object-fit:contain so the artwork stays proportional even
   under non-uniform resize. */
.node.node-spec-flourish {
  background: transparent;
  border: none;
  box-shadow: none;
  overflow: visible;
  z-index: 8;
}
.node.node-spec-flourish .node-header {
  display: none;
}
.node.node-spec-flourish .node-delete {
  top: 2px;
  right: 2px;
  background: rgba(15, 23, 42, 0.7);
  border-radius: 50%;
  width: 18px;
  height: 18px;
  font-size: 0.85rem;
  line-height: 18px;
  text-align: center;
  padding: 0;
  opacity: 0.7;
  z-index: 9;
}
.node.node-spec-flourish .node-delete:hover {
  opacity: 1;
}
/* No corner-resize affordance on flourish — sizing belongs to the
   v2 Adobe-style transform UI (uniform-scale corner handles +
   rotate handle). Until that lands, flourish stays at its
   default size; the × delete is the only chrome. */
.node.node-spec-flourish .node-resize {
  display: none;
}
.node-body-flourish {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: transparent;
  /* The body IS the drag handle — no header to grab, just click
     anywhere on the artwork to move the node. */
  cursor: grab;
}
.node-body-flourish:active {
  cursor: grabbing;
}
.flourish-img {
  /* Fill the body box (not just cap at natural size) so corner-
     scale visibly grows the artwork. object-fit: contain keeps
     the aspect ratio so the PNG never distorts even if the body
     ends up non-square (which it shouldn't, since corner-scale
     is locked aspect — but defensive). */
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  user-select: none;
  /* pointer-events: auto so clicks on the image bubble to the
     node's mousedown handler (which startDrag listens for). */
  pointer-events: auto;
}

/* Adobe-style transform UI for flourish. Visible only when the node
   is .selected. The bounding box is an `outline` (zero layout impact);
   the 4 corner handles + 1 rotate handle live as children of the
   node element so they inherit the node's CSS rotate transform —
   no extra math needed to keep them at the visually-rotated corners. */
.node.node-spec-flourish.selected {
  outline: 1px dashed #a78bfa;
  outline-offset: 4px;
}
.flourish-handle {
  position: absolute;
  display: none;
  pointer-events: auto;
  z-index: 9;
}
.node.node-spec-flourish.selected .flourish-handle {
  display: block;
}
.flourish-handle-corner {
  width: 10px;
  height: 10px;
  background: #a78bfa;
  border: 1px solid #ffffff;
  border-radius: 1px;
  box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
}
.flourish-handle-nw { top: -9px;    left: -9px;    cursor: nwse-resize; }
.flourish-handle-ne { top: -9px;    right: -9px;   cursor: nesw-resize; }
.flourish-handle-sw { bottom: -9px; left: -9px;    cursor: nesw-resize; }
.flourish-handle-se { bottom: -9px; right: -9px;   cursor: nwse-resize; }
.flourish-handle-rotate {
  width: 14px;
  height: 14px;
  top: -32px;
  left: 50%;
  margin-left: -7px;
  background: #a78bfa;
  border: 2px solid #ffffff;
  border-radius: 50%;
  cursor: grab;
  box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
}
.flourish-handle-rotate:active {
  cursor: grabbing;
}
.flourish-handle-rotate::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 14px;
  width: 1px;
  height: 18px;
  background: #a78bfa;
  margin-left: -1px;
  pointer-events: none;
}
.flourish-empty {
  color: #64748b;
  font-style: italic;
  font-size: 0.78rem;
  padding: 0.5rem;
  text-align: center;
}

/* === ADVISOR SPEC ===
   Stripped-down chromeless node, modeled on person but minimal.
   Shares the person-* chevron / field / notes CSS (intentional —
   the body internals are visually identical, only the field set
   differs). Adds advisor-specific UI for the variant pill +
   image shuffler + flat figure layout. */
.node.node-spec-advisor,
.node.node-spec-healthSpecialist {
  background: transparent;
  border: none;
  box-shadow: none;
  overflow: visible;
}
.node.node-spec-advisor .node-header,
.node.node-spec-healthSpecialist .node-header {
  display: none;
}
.node.node-spec-advisor .node-delete,
.node.node-spec-healthSpecialist .node-delete {
  top: 4px;
  right: 4px;
  z-index: 3;
}
.node-body-advisor {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 1.5rem 0.4rem 0.4rem;
  gap: 0.3rem;
  overflow-y: auto;
  position: relative; /* anchors .advisor-variant-pill */
}

/* Variant toggle — top-LEFT (mirrors person's mode pill placement,
   so users have a consistent mental model: top-right is for ×
   delete, top-left is for spec-specific switches). */
.advisor-variant-pill {
  position: absolute;
  top: 4px;
  left: 4px;
  z-index: 2;
  font: inherit;
  font-size: 9px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  color: #aab;
  border: 1px solid rgba(255, 255, 255, 0.14);
  cursor: pointer;
  user-select: none;
  transition: background .15s, color .15s;
}
.advisor-variant-pill:hover {
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
}

.advisor-top {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
}
.advisor-name {
  width: 100%;
  text-align: center;
  font-weight: 600;
  font-size: 0.95rem;
  color: #f1f5f9;
  padding: 2px 4px;
  border-radius: 4px;
  outline: none;
  cursor: text;
  min-height: 1.2em;
}
.advisor-name:empty::before {
  content: attr(data-placeholder);
  color: #64748b;
  font-style: italic;
  font-weight: 400;
}
.advisor-name:focus {
  background: rgba(255, 255, 255, 0.06);
}

/* Image row: ◀ image ▶ — flex with arrows on each end. The image
   is the dominant visual + drag handle. Arrows are subtle so they
   don't compete; they brighten on hover so they're discoverable. */
.advisor-image-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  width: 100%;
}
.advisor-shuffle {
  background: rgba(255, 255, 255, 0.05);
  color: #94a3b8;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 6px;
  width: 22px;
  height: 32px;
  font-size: 12px;
  cursor: pointer;
  user-select: none;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  transition: background .15s, color .15s;
}
.advisor-shuffle:hover {
  background: rgba(167, 139, 250, 0.18);
  color: #fff;
  border-color: rgba(167, 139, 250, 0.4);
}
.advisor-img {
  flex: 1;
  max-width: 160px;
  max-height: 240px;
  object-fit: contain;
  user-select: none;
  -webkit-user-drag: none;
  display: block;
}

.advisor-stub {
  color: #64748b;
  font-style: italic;
  font-size: 0.78rem;
  padding: 0.5rem;
  text-align: center;
}

/* === OM PRODUCT SPEC ===
   Reference card for a wholesaler-supplied investment fund. Brand
   band header (wholesaler-coloured) + body with fund name + active
   class summary + FundServe code + a "Details" chevron that opens
   a tabbed panel for Classes / KYP / Fact Sheets / Manager Comp /
   Summary. Matches the visual language of person/file specs:
   monospace for codes, purple accents (#c4b5fd / #a78bfa), dashed
   call-to-action buttons. */
.node-body-om-product {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 0;
  gap: 0;
  overflow-y: auto;
  background: #f8fafc;
  color: #0f172a;
}
.om-band {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  min-height: 32px;
  color: #fff;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.78rem;
}
.om-band-text {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.om-band-logo {
  height: 28px;
  width: auto;
  max-width: 60%;
  object-fit: contain;
  object-position: left center;
  flex-shrink: 0;
  display: block;
  margin-right: auto;
}
.om-card-body {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.6rem 0.75rem;
  background: #fff;
}
.om-fund-name {
  font-size: 0.95rem;
  font-weight: 600;
  color: #0f172a;
  line-height: 1.25;
}
.om-class-line {
  background: rgba(167, 139, 250, 0.08);
  border: 1px solid rgba(167, 139, 250, 0.3);
  border-radius: 3px;
  color: #4c1d95;
  cursor: pointer;
  font: inherit;
  font-size: 0.82rem;
  font-weight: 500;
  padding: 0.25rem 0.45rem;
  text-align: left;
}
.om-class-line:hover {
  background: rgba(167, 139, 250, 0.18);
  border-color: rgba(167, 139, 250, 0.55);
  color: #2e1065;
}
.om-fundserve-line {
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.78rem;
  color: #475569;
  letter-spacing: 0.02em;
}
.om-details {
  display: flex;
  flex-direction: column;
  border-top: 1px solid #e2e8f0;
  background: #f1f5f9;
}
.om-details-header {
  width: 100%;
  text-align: left;
  background: transparent;
  border: 0;
  color: #6d28d9;
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  padding: 0.4rem 0.75rem;
}
.om-details-header:hover {
  color: #4c1d95;
  background: rgba(167, 139, 250, 0.1);
}
.om-details-panel[hidden] {
  display: none;
}
.om-details-panel {
  display: flex;
  flex-direction: column;
  border-top: 1px solid #e2e8f0;
  background: #fff;
}
.om-tab-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  border-bottom: 1px solid #e2e8f0;
  background: #f8fafc;
}
.om-tab {
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  color: #64748b;
  cursor: pointer;
  font: inherit;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.4rem 0.55rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.om-tab:hover {
  color: #4c1d95;
  background: rgba(167, 139, 250, 0.08);
}
.om-tab.active {
  color: #4c1d95;
  border-bottom-color: #a78bfa;
  background: #fff;
}
.om-tab-content {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.55rem 0.75rem;
  font-size: 0.78rem;
  color: #1e293b;
}
.om-empty {
  color: #94a3b8;
  font-style: italic;
  font-size: 0.78rem;
}
.om-class-row {
  background: #f8fafc;
  border: 1px solid #e2e8f0;
  border-left: 2px solid #cbd5e1;
  border-radius: 3px;
  padding: 0.3rem 0.45rem;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}
.om-class-row-active {
  border-left-color: #a78bfa;
  background: rgba(167, 139, 250, 0.08);
}
.om-class-row-top {
  font-size: 0.78rem;
  color: #0f172a;
  font-weight: 500;
}
.om-class-row-bot {
  font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
  font-size: 0.7rem;
  color: #64748b;
}
.om-link {
  color: #6d28d9;
  text-decoration: none;
  font-size: 0.82rem;
  padding: 0.2rem 0;
}
.om-link:hover {
  color: #4c1d95;
  text-decoration: underline;
}
.om-mc-row {
  display: flex;
  justify-content: space-between;
  font-size: 0.78rem;
  padding: 0.15rem 0;
  border-bottom: 1px solid #f1f5f9;
}
.om-mc-label {
  color: #64748b;
}
.om-mc-value {
  color: #0f172a;
  font-weight: 500;
}
.om-mc-notes {
  margin-top: 0.4rem;
  padding: 0.4rem 0.5rem;
  background: #f8fafc;
  border-left: 2px solid #cbd5e1;
  border-radius: 3px;
  font-size: 0.75rem;
  color: #334155;
  white-space: pre-wrap;
}
.om-summary {
  font-size: 0.82rem;
  color: #1e293b;
  line-height: 1.5;
  white-space: pre-wrap;
}

/* ── Wholesaler library panel ──────────────────────────────────────
   Slide-in left panel that browses sample wholesalers and spawns OM
   Product nodes. Overlay (NOT push) — matches the prod convention
   (full-access-panel, layers-panel, chat-panel are all overlays) and
   keeps canvas pan/zoom plumbing untouched. The canvas underneath
   remains fully interactive (no pointer-events:none on the panel
   container). */
#wholesaler-panel {
  position: fixed;
  top: 0; /* pinned under the header via padding-top below */
  left: 0;
  width: 280px;
  height: 100vh;
  background: #16213e;
  border-right: 1px solid #533483;
  box-shadow: 4px 0 18px rgba(0, 0, 0, 0.55);
  z-index: 1035;
  color: #e2e8f0;
  display: flex;
  flex-direction: column;
  /* Header height (~56px) — keeps the panel below the top bar so the
     "Wholesalers" toggle button stays clickable while the panel is
     open. The header z-index is implicit (default) but the panel is
     z-index 1035 so it floats over the canvas only. */
  padding-top: 56px;
  font-size: 0.88rem;
  /* Slide-in animation. Hidden state uses [hidden] which is overridden
     to keep the element rendered but translated off-screen, so the
     in/out animation runs both directions. */
  transition: transform 180ms ease-out;
  transform: translateX(0);
}
#wholesaler-panel[hidden] {
  display: flex;
  transform: translateX(-100%);
  pointer-events: none;
}
#wholesaler-btn {
  /* Active-state when the panel is open. */
}
#wholesaler-btn.active {
  background: #533483;
  border-color: #a78bfa;
}
.wp-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid #0f3460;
  background: #0f172a;
}
.wp-title {
  flex: 1;
  margin: 0;
  font-size: 0.95rem;
  font-weight: 600;
  color: #a78bfa;
  letter-spacing: 0.02em;
}
.wp-back, .wp-close {
  background: transparent;
  border: 0;
  color: #cbd5e1;
  cursor: pointer;
  font-size: 1.1rem;
  line-height: 1;
  padding: 4px 8px;
  border-radius: 4px;
  font-family: inherit;
}
.wp-back:hover, .wp-close:hover { background: #533483; color: #fff; }
.wp-body {
  flex: 1;
  overflow-y: auto;
  padding: 12px;
}
.wp-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}
/* Fund grid (View 2) stacks vertically — full panel width — so fund
   names render unabbreviated, sized like the rep cards above them. */
.wp-fund-grid {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.wp-fund-grid .wp-tile {
  aspect-ratio: auto;
  min-height: 48px;
  font-size: 0.82rem;
  padding: 10px 12px;
  text-align: center;
  word-break: normal;
}
.wp-tile {
  aspect-ratio: 1 / 1;
  border: 1px dashed #533483;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: #fff;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-align: center;
  padding: 4px;
  transition: transform 80ms ease-out, box-shadow 80ms ease-out;
  user-select: none;
}
.wp-tile:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
.wp-tile:focus-visible {
  outline: 2px solid #a78bfa;
  outline-offset: 2px;
}
/* Logo path — white-bg tile hosting the wordmark image. Inner padding
   (~6px) keeps the wordmark from kissing the dashed border. */
.wp-tile-logo-host {
  padding: 6px;
}
.wp-tile-logo {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  user-select: none;
  pointer-events: none;
}
.wp-classes {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.wp-fund-row {
  background: #0f3460;
  border: 1px solid #533483;
  border-radius: 4px;
  padding: 8px 10px;
  text-align: left;
  cursor: pointer;
  color: #e2e8f0;
  font: inherit;
  font-size: 0.82rem;
  line-height: 1.4;
}
.wp-fund-row:hover { background: #533483; border-color: #a78bfa; }
.wp-fund-row:focus-visible {
  outline: 2px solid #a78bfa;
  outline-offset: 1px;
}
.wp-fund-name {
  font-weight: 600;
  color: #e2e8f0;
  margin-bottom: 2px;
}
.wp-fund-meta {
  color: #94a3b8;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.72rem;
}

/* ── Contact block (View 2 — above the fund grid) ──────────────────
   Stacked, full-width cards (panel is 280px so two columns is too
   tight). Slate body + purple accents to match the rest of the
   wholesaler panel palette. */
.wp-contact-block {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 12px;
}
.wp-contact-header {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #a78bfa;
  margin-bottom: 2px;
}
.wp-rep-card {
  background: #0f3460;
  border: 1px solid #533483;
  border-radius: 4px;
  padding: 8px 10px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.wp-rep-name {
  font-weight: 600;
  color: #e2e8f0;
  font-size: 0.82rem;
}
.wp-rep-email {
  color: #a78bfa;
  font-size: 0.75rem;
  text-decoration: none;
  word-break: break-all;
}
.wp-rep-email:hover { text-decoration: underline; }
.wp-rep-phone {
  color: #cbd5e1;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.72rem;
}
.wp-rep-region {
  color: #94a3b8;
  font-size: 0.72rem;
}

/* Thin separator between rep entries on the OM Product Contact Info
   tab (light-card panel — uses the slate-100 hairline that matches
   the rest of the om-* details panel). */
.om-contact-sep {
  height: 1px;
  background: #e2e8f0;
  margin: 0.4rem 0;
}
