:root{
  --bg:#0b1220; --panel:#121a2b; --panel2:#0f1726; --border:#243047;
  --text:#e6edf7; --muted:#8ea0bd; --accent:#38bdf8; --accent2:#22d3ee; --danger:#f87171;
}
*{box-sizing:border-box;}
html,body{height:100%;margin:0;}
body{
  background:var(--bg); color:var(--text);
  font-family:"Geist",-apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif;
  user-select:none; overflow:hidden;
}
#app{display:flex; flex-direction:column; height:100vh;}

#topbar{
  height:54px; flex:0 0 auto; display:flex; align-items:center; justify-content:space-between;
  padding:0 16px; background:linear-gradient(180deg,#141d31,#101728); border-bottom:1px solid var(--border); gap:14px;
}
#topbar .title{font-weight:700; font-size:15px; display:flex; align-items:center; gap:8px; flex:0 0 auto;}
#diagramTitle{
  border-radius:4px; padding:2px 6px; margin:-2px -6px;
  outline:none; cursor:text;
}
#diagramTitle:hover{background:var(--panel);}
#diagramTitle:focus{background:var(--panel); outline:1px solid var(--accent);}
.edit-title{
  display:inline-flex; align-items:center; justify-content:center;
  width:24px; height:24px; padding:0; flex:0 0 auto;
  background:none; border:none; color:var(--muted); font-size:13px; line-height:1;
  border-radius:6px; cursor:pointer; transition:.15s;
}
.edit-title:hover{color:var(--accent); background:var(--panel);}
/* Title isn't editable when locked or on phones — hide the rename affordance. */
#app[data-readonly="true"] .edit-title{display:none;}
@media (max-width: 640px){ .edit-title{display:none;} }
.topbar-right{display:flex; align-items:center; gap:10px; flex:0 0 auto;}
/* Sets the destructive button apart from the ones next to it. */
.topbar-sep{width:1px; height:22px; background:var(--border); flex:0 0 auto;}
/* .topbar-right is flex:0 0 auto and never shrinks, so the row simply runs off
   the right edge once it outgrows the window — and what falls off is whatever
   is last, which on /try is the Sign in link, the only route back on a page
   with no header. That page carries more buttons than the editor, and at 1280
   (a very ordinary laptop) it was 17px over. Tighten the row instead of
   shaving another label, which only defers the next one. */
@media (max-width: 1400px){
  .topbar-right{gap:7px;}
  .topbar-right .btn{padding:8px 10px;}
}
/* The left half is the other half of the problem: on /try it carries the logo,
   an editable title, a Guest badge, the save status AND a note, and #topbar is
   space-between with neither side shrinking. Let the title give way first — a
   long diagram name should ellipsize rather than shove the toolbar off the
   screen — and drop the note early, since "saved in this browser only" repeats
   what the Guest badge and the "Local" save status already say. */
#topbar .title{min-width:0;}
#diagramTitle{overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:34ch;}
@media (max-width: 1500px){ .guest-note{display:none;} }
#saveStatus{font-size:12px; color:var(--muted); opacity:0; transition:opacity .4s;}
/* Guest-mode topbar notes. The standing "this is local" warning lives here
   rather than in a banner: it's always visible, costs no vertical space on a
   page whose whole job is the canvas, and sits beside the Save button that
   fixes it. Both drop out on phones, where the topbar is already tight. */
.guest-note{font-size:11px; color:var(--muted); white-space:nowrap;}
@media (max-width: 640px){ .guest-note{display:none;} }

/* Who last touched this, in a shared team library. Sits beside the save status
   and drops out on phones, where the topbar is already tight. */
/* No margin-left: #topbar .title is a flex row with gap:8px already. */
.attribution{font-size:11px; color:var(--muted); white-space:nowrap;}
@media (max-width: 640px){ .attribution{display:none;} }
.btn{
  background:var(--panel2); border:1px solid var(--border); color:var(--text);
  padding:8px 13px; border-radius:8px; font-size:12.5px; cursor:pointer; transition:.15s; white-space:nowrap;
}
.btn:hover{border-color:var(--accent); color:var(--accent);}
.btn.danger:hover{border-color:var(--danger); color:var(--danger);}
.btn.primary{background:linear-gradient(135deg,#0ea5e9,#22d3ee); border:none; color:#04141c; font-weight:700;}
.btn.primary:hover{filter:brightness(1.08); color:#04141c;}
.btn:disabled{opacity:.4; cursor:default; filter:none; border-color:var(--border); color:var(--muted);}

/* ---------- VIEW TOGGLE + SEQUENCE VIEW ----------
   Top-left of the canvas, mirroring .stepper's top-right and .zoom-control's
   bottom-right. Built by canvas.js and injected into #canvasWrap so all five
   contexts get it from one place. */
.view-toggle{
  position:absolute; top:16px; left:16px; z-index:60;
  display:flex; gap:2px; padding:3px; border-radius:12px;
  background:rgba(15,23,38,.92); border:1px solid var(--border);
  box-shadow:0 12px 40px rgba(0,0,0,.5); backdrop-filter:blur(8px);
}
.view-tab{
  background:none; border:none; color:var(--muted); cursor:pointer;
  font-family:inherit; font-size:11.5px; font-weight:600; white-space:nowrap;
  padding:6px 11px; border-radius:9px; transition:.15s;
}
.view-tab:hover{color:var(--text);}
.view-tab.is-active{background:var(--panel2); color:var(--accent); box-shadow:inset 0 0 0 1px var(--border);}
/* Present mode is the walkthrough — the transport is the only chrome there. */
#app.presenting .view-toggle{display:none !important;}
/* The palette is a drag source and there is nowhere to drop in this view. */
#app.seq-view #palette{display:none;}
/* Auto-layout and Clear both act on the flow canvas; Undo has nothing to undo
   in a read-only view. */
#app.seq-view #layoutBtn,
#app.seq-view #clearBtn,
#app.seq-view #undoBtn{display:none;}

/* The vertical dashed line under each component header. */
/* Lighter than the message arrows (#64748b) so it reads as structure, but
   var(--border) was almost invisible against the canvas. */
.seq-lifeline{stroke:#54637c; stroke-width:1.6; stroke-dasharray:4 6;}
#app.seq-view .node{cursor:default;}
/* Message labels sit on the arrow rather than floating beside it, so they get a
   solid backing — a dashed lifeline running behind the text is unreadable. */
#app.seq-view .edge-label-wrap{background:var(--bg); padding:0 2px; border-radius:6px;}

@media (max-width: 640px){
  .view-toggle{top:auto; bottom:16px; left:16px;}
  .view-tab{font-size:11px; padding:5px 9px;}
}

/* Step-through controls sit on the canvas rather than in the topbar corner. The
   affordance has to be on the thing it drives: parked in page chrome, viewers
   read the diagram as a static image and never press anything. canvas.js moves
   the element into #canvasWrap at init (it is positioned relative), which keeps
   it clear of the resizable notes panel automatically — the same arrangement
   .zoom-control uses in the opposite corner. */
.stepper{
  position:absolute; top:16px; right:16px; z-index:60;
  display:flex; align-items:center; gap:9px;
  background:rgba(15,23,38,.92); border:1px solid var(--border);
  box-shadow:0 12px 40px rgba(0,0,0,.5); backdrop-filter:blur(8px);
  padding:7px 10px; border-radius:14px;
}
/* It no longer lives inside #topbar, so present mode has to hide it explicitly
   — the present bar is the control there. */
#app.presenting .stepper{display:none;}
/* Navigation stays visible at every step. Collapsing it to a lone play button at
   overview hid the fact that this is a sequence you can move both ways through,
   and made the controls jump the moment you pressed it — Back and reset are
   already `disabled` at overview, so showing them costs nothing and advertises
   what's there. Only the labels change between states. */
/* Nothing to walk through — no floating chrome over an empty canvas. */
/* Shown but inert when there is nothing to walk yet. Hiding it meant a fresh
   or still-empty diagram gave no sign that walking is what this is for —
   the same reason the whole transport stays visible at every step instead of
   collapsing to a play button. The buttons are already `disabled` in
   updateStepUI, so this is only the visual cue that they aren't usable. */
.stepper[data-empty="true"]{opacity:.55;}
.step-label{font-size:11.5px; color:var(--muted); min-width:108px; text-align:center;}
.step-reset{display:inline-flex; align-items:center; justify-content:center; background:none; border:none; color:var(--muted); cursor:pointer; font-size:26px; width:38px; height:38px; padding:0; border-radius:8px; line-height:1;}
.step-reset:hover{color:var(--accent); background:var(--panel);}

#main{flex:1; display:flex; min-height:0;}

#palette{
  width:180px; flex:0 0 auto; background:var(--panel); border-right:1px solid var(--border);
  padding:14px 12px; overflow-y:auto;
}
.palette-back{display:flex; align-items:center; gap:6px; font-size:12.5px; font-weight:600; color:var(--accent); text-decoration:none; background:rgba(56,189,248,.08); border:1px solid rgba(56,189,248,.3); padding:8px 10px; border-radius:8px; margin-bottom:14px; transition:.15s;}
.palette-back:hover{background:rgba(56,189,248,.16); border-color:var(--accent);}
#palette h3{font-size:11px; text-transform:uppercase; letter-spacing:.08em; color:var(--muted); margin:4px 0 10px;}
/* Second and later groups need air above them, or the list reads as one run of
   items with a stray label in the middle. */
#palette h3.palette-group{margin-top:18px; padding-top:14px; border-top:1px solid var(--border);}

/* Guest starting-point chooser (/try only, injected by canvas.js). Qualified as
   .ai-modal.start-modal so it out-specifies the base .ai-modal{width:440px}
   declared further down — an unqualified .start-modal width here would lose on
   source order and be silently dead, which .howto-modal learned the hard way. */
.ai-modal.start-modal{width:520px; max-width:calc(100vw - 40px);}
.start-modal h3{margin:0 0 6px; font-size:17px;}
.start-modal-sub{margin:0 0 16px; font-size:13px; color:var(--muted); line-height:1.5;}
.start-modal-choice{
  display:flex; flex-direction:column; gap:3px; width:100%; text-align:left;
  font-family:inherit; cursor:pointer; margin-bottom:10px;
  padding:12px 14px; border-radius:10px;
  color:var(--text); background:var(--panel2); border:1px solid var(--border);
  transition:.15s;
}
.start-modal-choice:hover{border-color:var(--accent); background:var(--panel);}
.start-modal-choice-title{font-size:13.5px; font-weight:700;}
.start-modal-choice-desc{font-size:12px; color:var(--muted); line-height:1.45;}
.palette-item{
  display:flex; align-items:center; gap:8px; padding:9px 10px; margin-bottom:8px;
  background:var(--panel2); border:1px solid var(--border); border-radius:9px; cursor:grab; font-size:13px; transition:.15s;
}
.palette-item:hover{border-color:var(--accent); transform:translateX(2px);}
/* Custom used to carry `border-style:dashed`, which dashed all four sides, not
   just the separator above it — so the one item that was meant to read as
   "anything you like" instead read as a rendering fault next to eleven solid
   ones. It sits under its own group heading now, like the other two families,
   and the heading does the separating. */
.palette-item .sw{width:10px;height:10px;border-radius:3px; flex:0 0 auto;}
.tip{font-size:11.5px; color:var(--muted); line-height:1.6; margin-top:6px;}
.tip b{color:var(--text);}
.palette-help-btn{display:flex; align-items:center; justify-content:flex-start; gap:7px; width:100%; margin-top:16px; padding:8px 10px; font-size:12.5px; font-weight:600; color:var(--muted); background:var(--panel2); border:1px solid var(--border); border-radius:8px; cursor:pointer; text-align:left; transition:.15s; font-family:inherit;}
.palette-help-btn:hover{color:var(--text); border-color:var(--accent);}
.help-badge{display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; border-radius:50%; background:var(--accent); color:#04121f; font-size:11px; font-weight:800; line-height:1; flex:0 0 auto;}
/* Grouped by intent (build / refine / walk through / shortcuts) rather than one
   flat list — the modal is the only place the interaction model is explained,
   so it earns the width. canvas.js builds the content. */
/* Wider than the other dialogs: this one holds four grouped sections plus the
   shortcut list, and at the base 440px the two-column shortcut rows wrapped.

   Qualified with .ai-modal on purpose. buildHowTo() adds .howto-modal to an
   element that already has .ai-modal, and the base `.ai-modal{width:440px}`
   sits *later* in this file — at equal specificity that wins on source order,
   so a bare `.howto-modal{width:…}` here is silently dead. It was: this dialog
   rendered at 440px the whole time, including when the rule said 620px.
   `.comment-modal` gets away with a bare class only because it happens to be
   declared after the base rule. */
/* The dialog itself no longer scrolls — .howto-body does, so .ai-modal-actions
   stays pinned and "Got it" is always reachable. */
.ai-modal.howto-modal{width:min(820px, 94vw); max-width:94vw; max-height:86vh;
  overflow:hidden; display:flex; flex-direction:column;}
.howto-body{overflow-y:auto; flex:1 1 auto; min-height:0; padding-right:4px;}
.ai-modal.howto-modal .ai-modal-actions{flex:0 0 auto; margin-top:14px; padding-top:14px;
  border-top:1px solid var(--border);}
.howto-title{display:flex; align-items:center; gap:8px; margin:0 0 4px; padding-bottom:14px; border-bottom:1px solid var(--border);}
.howto-group{margin-top:16px;}
.howto-group h4{
  margin:0 0 8px; font-size:10.5px; font-weight:700; letter-spacing:.09em;
  text-transform:uppercase; color:var(--accent);
}
.howto-list, .howto-steps{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:2px;}
.howto-list li, .howto-steps li{
  position:relative; padding:7px 0 7px 26px;
  font-size:12.5px; color:var(--muted); line-height:1.55;
}
.howto-list li::before{
  content:""; position:absolute; left:8px; top:14px;
  width:5px; height:5px; border-radius:50%; background:var(--accent);
}
/* The first three are a sequence, so they're numbered rather than bulleted. */
.howto-steps{counter-reset:howto;}
.howto-steps li{counter-increment:howto;}
.howto-steps li::before{
  content:counter(howto); position:absolute; left:0; top:6px;
  width:18px; height:18px; border-radius:50%;
  background:rgba(56,189,248,.15); border:1px solid rgba(56,189,248,.45); color:var(--accent);
  font-size:10.5px; font-weight:800; line-height:16px; text-align:center;
}
.howto-list b, .howto-steps b{color:var(--text); font-weight:600;}

.howto-keys{
  margin:0; display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:6px 14px;
}
.howto-keys > div{display:flex; align-items:center; gap:9px; min-width:0;}
.howto-keys dt{
  flex:0 0 auto; padding:3px 7px; border-radius:5px;
  background:var(--panel2); border:1px solid var(--border); color:var(--text);
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:10.5px; white-space:nowrap;
}
.howto-keys dd{margin:0; font-size:11.5px; color:var(--muted); min-width:0;}
@media (max-width: 560px){ .howto-keys{grid-template-columns:1fr;} }
#howToModalOverlay .ai-modal-actions{border-top:1px solid var(--border); padding-top:14px; margin-top:18px;}

#canvasWrap{
  flex:1; position:relative; overflow:hidden; touch-action:none; background:
    radial-gradient(circle at 1px 1px, #1b2742 1.4px, transparent 1.4px) 0 0/22px 22px, var(--bg);
}
#canvasWrap.pan-ready{cursor:grab;}
#canvasWrap.panning{cursor:grabbing;}
#canvas{position:relative; width:1700px; height:950px; transform-origin:0 0;}

.zoom-control{
  position:absolute; right:16px; bottom:16px; z-index:20;
  display:flex; align-items:center; gap:2px; padding:3px;
  background:var(--panel2); border:1px solid var(--border); border-radius:10px;
  box-shadow:0 4px 14px rgba(0,0,0,.4);
}
.zoom-control .zoom-btn,
.zoom-control .zoom-level{
  background:transparent; border:none; color:var(--text); cursor:pointer;
  font-size:14px; line-height:1; border-radius:7px; transition:background .12s;
}
.zoom-control .zoom-btn{width:28px; height:28px; font-size:18px;}
.zoom-control .zoom-level{min-width:48px; height:28px; font-size:12px; font-variant-numeric:tabular-nums;}
.zoom-control .zoom-btn:hover,
.zoom-control .zoom-level:hover{background:rgba(148,163,184,.16);}

/* ---------- PRESENTATION MODE ---------- */
.present-btn{display:inline-flex; align-items:center; gap:5px;}
.present-lock{font-size:11px; opacity:.85;}

#app.presenting #topbar,
#app.presenting #palette,
#app.presenting #notesPanel,
#app.presenting .locked-banner{display:none !important;}
#app.presenting{position:relative; background:var(--bg);}
#app.presenting .zoom-control{bottom:90px;}

.present-bar{
  display:none; position:fixed; left:50%; bottom:26px; transform:translateX(-50%);
  z-index:1000; width:min(880px, calc(100vw - 48px));
  flex-direction:column; align-items:center; gap:12px;
  padding:16px 20px; border-radius:16px;
  background:rgba(15,23,38,.92); border:1px solid var(--border);
  box-shadow:0 12px 40px rgba(0,0,0,.55); backdrop-filter:blur(8px);
}
#app.presenting .present-bar{display:flex;}
.present-caption{
  font-size:20px; line-height:1.4; font-weight:600; color:var(--text);
  text-align:center; max-width:100%; user-select:text;
}
.present-controls{display:flex; align-items:center; gap:12px;}
.present-nav{
  background:var(--accent); border:none; color:#04141c; font-weight:700;
  font-size:14px; padding:9px 18px; border-radius:9px; cursor:pointer; transition:.15s;
}
.present-nav:hover{filter:brightness(1.08);}
.present-nav:disabled{opacity:.35; cursor:default; filter:none;}
.present-step{
  font-size:13px; color:var(--muted); min-width:96px; text-align:center;
  font-variant-numeric:tabular-nums;
}
.present-exit{
  display:none; position:fixed; top:16px; right:16px; z-index:1001;
  width:42px; height:42px; align-items:center; justify-content:center;
  background:rgba(15,23,38,.92); border:1.5px solid var(--border); color:var(--text);
  font-size:18px; font-weight:700; line-height:1; border-radius:50%; cursor:pointer; transition:.15s;
  backdrop-filter:blur(8px); box-shadow:0 8px 24px rgba(0,0,0,.45);
}
#app.presenting .present-exit{display:flex;}
.present-exit:hover{background:var(--danger); border-color:var(--danger); color:#0b1220;}

/* ---------- EMBED: chromeless viewer for docs (present-style controls) ----------
   The iframe drops the top bar entirely so the canvas fills the frame; the
   step-through controls float in a rounded, blurred bar like presentation mode,
   with a small Flostep attribution mark in the corner. */
#app[data-embed="true"] #topbar{
  position:absolute; top:0; left:0; right:0; height:0; min-height:0;
  padding:0; margin:0; background:none; border:none; overflow:visible; z-index:1000;
}
#app[data-embed="true"] #topbar .title{display:none;}
/* Embeds keep the controls bottom-centre. There's no topbar to distinguish
   them from, the badge already sits bottom-left, and an iframe is read like a
   video player — so the bar belongs under the diagram rather than over it. */
#app[data-embed="true"] .stepper{
  top:auto; right:auto; bottom:18px; left:50%; transform:translateX(-50%); z-index:1000;
}
/* A diagram with no steps stays fully chromeless — no floating bar. */
#app[data-embed="true"][data-has-steps="false"] .stepper{display:none;}

.embed-badge{
  position:fixed; left:12px; bottom:14px; z-index:999;
  display:inline-flex; align-items:center; padding:5px 9px; border-radius:9px;
  background:rgba(15,23,38,.8); border:1px solid var(--border);
  backdrop-filter:blur(6px); opacity:.72; transition:.15s; text-decoration:none;
}
.embed-badge:hover{opacity:1; border-color:var(--accent);}

/* overflow:visible because an <svg> root clips to its own width/height by
   default, and edge curves legitimately leave the canvas box: parallelPlan()
   fans a bundle perpendicular to the run, so the upper members of a bundle
   near y=0 arc above it. The chips are HTML divs in #canvas, which does NOT
   clip — so the two layers disagreed about the same boundary and an edge came
   out as two disconnected stubs with its "+ add description" chip floating in
   the gap. #canvasWrap still clips at the viewport, which is what pan is for. */
#edgesLayer{position:absolute; top:0; left:0; pointer-events:none; overflow:visible;}
.edge-hit{pointer-events:stroke; cursor:grab;}
.edge-hit:active{cursor:grabbing;}
.edge-visible{
  stroke-dasharray:6 5; animation:dash 1.1s linear infinite;
  transition:stroke .2s, stroke-width .2s, opacity .25s;
}
.edge-visible.hovered{stroke:#94a3b8 !important;}
.edge-visible.active{stroke:var(--accent) !important; stroke-width:3.4; filter:drop-shadow(0 0 5px rgba(56,189,248,.65));}
.edge-visible.dimmed{opacity:.18;}
@keyframes dash{to{stroke-dashoffset:-22;}}

/* canvas.js moves this into #canvasWrap at init, so these percentages resolve
   against the visible area rather than the 1700px canvas surface. Centred, not
   nudged: the old 54%/46% only looked right on one window size. */
#emptyHint{
  position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  color:var(--muted); font-size:13px; text-align:center; max-width:min(420px, 80%);
  line-height:1.7; pointer-events:none; z-index:1;
}
#emptyHint b{color:var(--text);}
#emptyHint .empty-hint-alt{display:block; margin-top:10px; font-size:12px; opacity:.85;}

.node{
  position:absolute; width:172px; height:56px; background:var(--panel2);
  border:1.5px solid var(--border); border-radius:11px; padding:0 12px;
  cursor:grab; box-shadow:0 2px 8px rgba(0,0,0,.35);
  transition:border-color .15s, box-shadow .2s, opacity .25s;
  display:flex; align-items:center;
}
.node:active{cursor:grabbing;}
.node.selected{box-shadow:0 0 0 3px rgba(56,189,248,.18);}
.node.processing{box-shadow:0 0 0 3px rgba(56,189,248,.4), 0 0 20px rgba(56,189,248,.35); border-color:var(--accent);}
.node.dimmed{opacity:.3;}
.node-header{display:flex; align-items:center; gap:7px; width:100%; min-width:0;}
.node-icon{font-size:16px; flex:0 0 auto;}
.node-icon.editable{cursor:pointer; border-radius:6px; padding:1px 3px; margin:-1px -3px; transition:background .12s, box-shadow .12s;}
.node-icon.editable:hover{background:rgba(56,189,248,.2); box-shadow:0 0 0 1px rgba(56,189,248,.4);}

.emoji-picker{
  position:fixed; z-index:200; display:none; width:312px; max-height:340px; overflow-y:auto;
  background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:10px;
  box-shadow:0 20px 60px rgba(0,0,0,.5);
}
.emoji-group-title{
  font-size:10px; text-transform:uppercase; letter-spacing:.07em; color:var(--muted);
  font-weight:700; margin:10px 2px 6px;
}
.emoji-group-title:first-child{margin-top:0;}
.emoji-grid{display:grid; grid-template-columns:repeat(9, 1fr); gap:2px;}
.emoji-btn{
  background:none; border:none; cursor:pointer; font-size:18px; line-height:1;
  padding:5px 0; border-radius:6px; transition:background .1s;
}
.emoji-btn:hover{background:rgba(56,189,248,.2);}
/* Long names wrap to two lines inside the fixed 172x56 node instead of being
   cut to one ellipsized line; anything past that still ellipsizes, and the
   full text is on the span's title. Keep the 2-line cap and line-height in
   sync with NAME_WRAP_CHARS/NAME_MAX_LINES in canvas.js so the SVG/PNG
   export breaks names at the same place. */
.node-label{
  font-size:12.5px; font-weight:700; line-height:1.25;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden; overflow-wrap:anywhere;
  flex:1; min-width:0; border-radius:4px; padding:2px 4px; margin:-2px -4px;
}
.node-label[contenteditable="true"]{
  outline:1px solid var(--accent); background:var(--panel); cursor:text;
  display:block; overflow:visible; -webkit-line-clamp:none;
}
.node-del{
  position:absolute; top:-8px; right:-8px; width:19px; height:19px; border-radius:50%;
  background:#1e293b; border:1px solid var(--border); color:var(--danger); font-size:12px; line-height:1;
  cursor:pointer; opacity:0; transition:opacity .15s; padding:0; z-index:6;
}
.node:hover .node-del{opacity:1;}
.dot{
  position:absolute; width:13px; height:13px; border-radius:50%; top:50%; transform:translateY(-50%);
  background:#1e293b; border:2px solid var(--accent); cursor:crosshair; z-index:5;
}
.dot:hover{background:var(--accent);}
.dot-in{left:-7px;}
.dot-out{right:-7px;}

.canvas-text{
  position:absolute; min-width:40px; max-width:300px; padding:4px 8px;
  border:1px solid transparent; border-radius:8px; cursor:grab; z-index:4;
  transition:border-color .15s, background .15s;
}
.canvas-text:hover{border-color:rgba(148,163,184,.35); background:rgba(148,163,184,.06);}
.canvas-text:active{cursor:grabbing;}
.text-content{
  font-size:14px; font-weight:600; color:#cbd5e1; line-height:1.5;
  white-space:pre-wrap; word-break:break-word; min-width:24px;
  border-radius:4px; padding:1px 3px; margin:-1px -3px;
}
.text-content:empty:before{content:attr(data-placeholder); color:#5b6b85;}
.text-content[contenteditable="true"]{outline:1px solid var(--accent); background:var(--panel); cursor:text;}
.text-del{
  position:absolute; top:-8px; right:-8px; width:19px; height:19px; border-radius:50%;
  background:#1e293b; border:1px solid var(--border); color:var(--danger); font-size:12px; line-height:1;
  cursor:pointer; opacity:0; transition:opacity .15s; padding:0; z-index:6;
}
.canvas-text:hover .text-del{opacity:1;}

/* width:max-content is load-bearing. Absolutely-positioned boxes shrink to fit
   the space left inside the containing block, so a chip sitting near the
   canvas's right edge would be squeezed into a narrow, tall column — both an
   ugly render and a lie to layoutFlow, which measures these to size row gaps. */
.edge-label-wrap{position:absolute; display:flex; align-items:center; gap:4px; width:max-content; transform:translate(-50%,-50%); z-index:7;}
.edge-label-wrap:hover{z-index:9;}
.edge-num{
  background:#334155; color:#cbd5e1; font-size:10px; font-weight:800; width:17px; height:17px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; flex:0 0 auto;
  transition:opacity .25s, background .2s, color .2s, transform .2s;
}
.edge-num.dimmed{opacity:.3;}
.edge-num.active{background:var(--accent); color:#04141c; transform:scale(1.15);}
.edge-label{
  background:#1e293b; border:1px solid var(--border); border-radius:6px; padding:4px 9px;
  font-size:11px; color:#cbd5e1; max-width:220px; text-align:center; cursor:text;
  white-space:pre-wrap; word-break:break-word; line-height:1.5;
  transition:opacity .25s, border-color .2s, background .2s, transform .2s;
}
.edge-label:empty:before{content:attr(data-placeholder); color:#5b6b85;}
.edge-label[contenteditable="true"]{outline:1px solid var(--accent); background:#0f1726; max-width:260px; white-space:pre-wrap;}
.edge-label.active{border-color:var(--accent); color:#fff; background:#0f2236; transform:scale(1.07);}
.edge-label.dimmed{opacity:.25;}
.edge-del{
  opacity:0; width:16px;height:16px;border-radius:50%; background:#1e293b;border:1px solid var(--border);
  color:var(--danger); font-size:11px; line-height:1; cursor:pointer; transition:opacity .15s; padding:0; flex:0 0 auto;
}
.edge-label-wrap:hover .edge-del{opacity:1;}

.packet{
  position:absolute; width:14px; height:14px; border-radius:50%;
  background:radial-gradient(circle at 35% 30%, #7dd3fc, #0284c7);
  box-shadow:0 0 9px rgba(56,189,248,.85); z-index:10; pointer-events:none;
  transition:opacity .35s, transform .35s;
}
.packet.done{opacity:0; transform:scale(0.3);}

.toast{
  position:absolute; bottom:18px; left:50%; transform:translateX(-50%);
  background:#1e293b; border:1px solid var(--border); padding:9px 16px; border-radius:9px;
  font-size:12.5px; opacity:0; transition:opacity .3s; pointer-events:none; z-index:20;
}
.toast.show{opacity:1;}

.ai-modal-overlay{position:fixed; inset:0; background:rgba(5,10,20,.6); display:none; align-items:center; justify-content:center; z-index:100;}
.ai-modal-overlay.show{display:flex;}
.ai-modal{width:440px; max-width:90vw; background:var(--panel); border:1px solid var(--border); border-radius:14px; padding:20px; box-shadow:0 20px 60px rgba(0,0,0,.5);}
.ai-modal h3{margin:0 0 8px; font-size:15px;}
.ai-modal-desc{font-size:12px; color:var(--muted); margin:0 0 12px; line-height:1.5;}
.ai-modal-warning{font-size:11px; color:#fbbf24; margin:8px 0 0;}
.ai-modal-actions{display:flex; justify-content:flex-end; gap:8px; margin-top:14px;}
.ai-modal-desc code{background:#0f1726; border:1px solid var(--border); border-radius:4px; padding:1px 5px; font-size:11px; color:var(--accent2);}

/* Share modal */
#shareModalOverlay .ai-modal{width:520px;}
.share-section{margin-bottom:16px;}
.share-section:last-of-type{margin-bottom:4px;}
.share-label{display:block; font-size:11px; font-weight:700; letter-spacing:.03em; text-transform:uppercase; color:var(--muted); margin-bottom:6px;}
.share-label-row{display:flex; align-items:center; justify-content:space-between; margin-bottom:6px;}
.share-label-row .share-label{margin-bottom:0;}
.share-input-row{display:flex; gap:8px; align-items:stretch;}
.share-input-row #shareUrlInput{flex:1 1 auto; min-width:0;}
.share-input-row .btn{flex:0 0 auto;}
#shareModalOverlay input[type=text], #shareModalOverlay textarea{width:100%; background:var(--panel2); border:1px solid var(--border); border-radius:8px; color:var(--text); padding:9px 11px; font-size:12px; font-family:inherit;}
#shareModalOverlay input[type=text]:focus, #shareModalOverlay textarea:focus{outline:none; border-color:var(--accent);}
#embedCodeInput{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:11px; line-height:1.5; resize:none;}
.share-mini-btn{padding:4px 10px; font-size:11px;}
.share-guide-link{display:inline-block; margin-top:8px; font-size:11px; color:var(--accent); text-decoration:none;}
.share-guide-link:hover{text-decoration:underline;}
#commentsToggleWrap{border-top:1px solid var(--border); padding-top:14px;}
/* Topbar button, not a per-edge chip: comments are on the diagram. */
#commentsBtn.has-comments{background:rgba(56,189,248,.18); border-color:var(--accent); color:var(--accent2);}

/* Roomy enough to read a discussion in, not just peek at it — a diagram-level
   thread is the whole conversation, where the old per-step popover held two or
   three lines. Both dimensions stay viewport-relative so it can't outgrow a
   laptop screen or a phone. */
.comment-modal{width:min(680px, 92vw);}
.comment-list{max-height:min(460px, 55vh); overflow-y:auto; margin:4px 0 14px; display:flex; flex-direction:column; gap:9px;}

.comment-reactions{display:flex; flex-wrap:wrap; gap:5px; margin-top:8px;}
.reaction-pill{
  background:#1e293b; border:1px solid var(--border); color:var(--muted); font-size:12px; line-height:1;
  padding:4px 8px; border-radius:999px; cursor:pointer; transition:border-color .15s, color .15s, background .15s;
}
.reaction-pill:hover:not(:disabled){border-color:var(--accent); color:var(--accent);}
.reaction-pill.mine{background:rgba(56,189,248,.18); border-color:var(--accent); color:var(--accent2);}
/* Not greyed out: for a non-member these are a read-out of what the team
   thought, so they should stay legible rather than look broken. */
.reaction-pill:disabled{cursor:default;}
.comment-empty{font-size:12px; color:var(--muted); text-align:center; padding:16px 0;}
.comment-item{background:var(--panel2); border:1px solid var(--border); border-radius:8px; padding:9px 11px;}
/* Two children only: the who-and-when group, and Remove. Anything else in here
   and space-between starts distributing the gaps between them instead. */
.comment-item .comment-meta{display:flex; justify-content:space-between; align-items:baseline; gap:10px; font-size:10.5px; color:var(--muted); margin-bottom:4px;}
/* baseline, not center: the name is 700-weight 10.5px against a 10px time, and
   centering two different sizes sits them half a pixel out of line. */
.comment-item .comment-who{display:flex; align-items:baseline; gap:6px; min-width:0; flex:1 1 auto;}
/* min-width:0 above lets this actually shrink — a flex item won't go below its
   content width otherwise, so a long name pushed the time out instead. */
.comment-item .comment-author{color:var(--text); font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
/* Never wraps or shrinks: "just now" and "12 Mar" are different widths, and a
   timestamp folding onto a second line is what made the row jump. */
.comment-item .comment-time{font-size:10px; color:var(--muted); white-space:nowrap; flex:0 0 auto;}
/* Shown in place of the name box for a signed-in member. The comment is going
   on a public page under their real name, so who it's attributed to shouldn't
   be invisible just because it isn't editable. */
.comment-as{margin:0 0 8px; font-size:11.5px; color:var(--muted);}

/* Marks a comment the server tied to a signed-in member, as opposed to a name
   someone typed into the box on a public page. */
/* No margin-left: it's a flex sibling of the name now, and .comment-who's gap
   already spaces it — the two together read as a double space. */
.comment-verified{flex:0 0 auto; font-size:9px; font-weight:700; text-transform:uppercase; letter-spacing:.04em;
  color:#34d399; background:rgba(52,211,153,.12); border:1px solid rgba(52,211,153,.35);
  border-radius:4px; padding:1px 4px; vertical-align:middle;}
.comment-item .comment-text{font-size:12.5px; line-height:1.5; white-space:pre-wrap; word-break:break-word;}
.comment-del{background:none; border:none; color:var(--muted); cursor:pointer; font-size:10.5px; padding:0; flex:0 0 auto;}
.comment-del:hover{color:var(--danger);}
#commentNameInput{
  width:100%; background:var(--panel2); border:1px solid var(--border); border-radius:8px; color:var(--text);
  font-size:12px; padding:8px 10px; margin-bottom:8px; font-family:inherit;
}
#commentNameInput:focus{outline:none; border-color:var(--accent);}
#commentTextInput{
  width:100%; min-height:64px; background:var(--panel2); border:1px solid var(--border); border-radius:8px;
  color:var(--text); font-size:12.5px; padding:9px 10px; font-family:inherit; line-height:1.5; resize:vertical;
}
#commentTextInput:focus{outline:none; border-color:var(--accent);}

/* Validation and post results for the comment form. .toast sits inside
   #canvasWrap under the overlay's z-index, so anything routed there from a
   modal reads as dimmed text behind the dialog — this stays in the dialog,
   beside the fields it's talking about. */
.comment-msg{display:none; margin:8px 0 0; font-size:11.5px; line-height:1.4; color:var(--danger);}
.comment-msg.show{display:block;}
.comment-msg.is-ok{color:#34d399;}
#commentNameInput.is-invalid, #commentTextInput.is-invalid{border-color:var(--danger);}

/* Right panel holds Notes and Code as tabs. Both are always-present editing
   surfaces for the same diagram, so they share one column rather than the code
   view covering the canvas as a modal — applying a change is visible straight
   away. The panel is a flex child of #main, so the canvas keeps its true
   width and fit-to-view needs no adjustment. */
#notesPanel{
  width:300px; flex:0 0 auto; background:var(--panel); border-left:1px solid var(--border);
  padding:12px 14px 14px; display:flex; flex-direction:column; min-height:0;
  position:relative;
}
/* Drag handle straddling the panel's left border. Sits above the panel content
   but the canvas owns everything to its left, so it never steals canvas drags. */
.side-resizer{
  position:absolute; left:-3px; top:0; bottom:0; width:7px; z-index:8;
  cursor:col-resize; background:transparent; transition:background .15s;
}
.side-resizer:hover, .side-resizer.is-dragging{background:rgba(56,189,248,.22);}
/* A permanently visible grip: a transparent strip that only lights up on hover
   is undiscoverable, so nobody learns the panel resizes at all. */
.side-resizer::after{
  content:""; position:absolute; left:2px; top:50%; transform:translateY(-50%);
  width:3px; height:26px; border-radius:2px;
  background-image:radial-gradient(circle at center, var(--muted) 1.1px, transparent 1.3px);
  background-size:3px 5px; opacity:.75; transition:.15s;
}
.side-resizer:hover::after, .side-resizer.is-dragging::after{
  opacity:1; height:38px;
  background-image:radial-gradient(circle at center, var(--accent) 1.1px, transparent 1.3px);
}
.side-tabs{display:flex; gap:4px; flex:0 0 auto; margin-bottom:10px;}
.side-tab{
  flex:1 1 0; background:none; border:1px solid transparent; border-radius:8px;
  padding:7px 8px; font-family:inherit; font-size:11px; font-weight:700;
  text-transform:uppercase; letter-spacing:.07em; color:var(--muted);
  cursor:pointer; transition:.15s; white-space:nowrap;
}
.side-tab:hover{color:var(--text);}
.side-tab.is-active{background:var(--panel2); border-color:var(--border); color:var(--accent);}

/* Panes stack in the same space; only the active one takes it. */
.side-pane{display:none; flex:1 1 auto; min-height:0; flex-direction:column;}
.side-pane.is-active{display:flex;}
.side-pane-desc{font-size:11px; color:var(--muted); margin:0 0 8px; line-height:1.5;}
.side-pane-desc code{
  background:var(--panel2); border:1px solid var(--border); border-radius:4px;
  padding:1px 4px; font-size:10.5px; color:var(--accent2);
}
.side-pane-actions{display:flex; justify-content:flex-end; gap:8px; margin-top:10px; flex:0 0 auto;}
.side-pane-actions .btn{padding:7px 11px; font-size:12px;}

#notes{
  width:100%; flex:1 1 auto; min-height:120px; resize:none; background:var(--panel2);
  border:1px solid var(--border); border-radius:8px; color:var(--text);
  padding:9px; line-height:1.6; font-size:12.5px; font-family:inherit;
}
#notes:focus{outline:none; border-color:var(--accent);}

/* The code view is a gutter + textarea sharing one bordered box, so the step
   numbers line up with the rows. canvas.js builds the wrapper and the gutter. */
.code-editor{
  display:flex; flex:1 1 auto; min-height:120px; overflow:hidden;
  background:var(--panel2); border:1px solid var(--border); border-radius:8px;
}
.code-editor:focus-within{border-color:var(--accent);}
.code-gutter{
  flex:0 0 auto; width:30px; padding:9px 6px 9px 0; overflow:hidden;
  text-align:right; color:var(--muted); background:rgba(148,163,184,.05);
  border-right:1px solid var(--border); user-select:none;
}
/* Gutter rows and textarea rows must share a line box or the numbers drift. */
/* .code-mirror belongs here too: it measures the wrapped height of each line,
   so it has to render in exactly the textarea's metrics. Leaving it out meant
   measuring in the inherited system font and the numbers drifted off the text. */
.code-gutter, #codeInput, .code-mirror{
  font-size:12px; line-height:1.7;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
/* Height is set per row by renderCodeGutter() from the measured mirror, since a
   wrapped line is taller than one row. This is only the unmeasured fallback. */
/* No min-height: renderCodeGutter() sets an exact sub-pixel height per row, and
   a min-height would silently floor short rows and break the alignment. */
.code-gutter-line{overflow:hidden;}
/* Every row shares one left edge. A hanging indent via text-indent looks right
   in a div but not here: a textarea's content is a single block, so hard
   newlines don't start new blocks and only the very first row gets shifted.
   Continuation rows are instead marked by their blank gutter cell.
   .code-mirror must keep every one of these properties identical — it measures
   the wrapped height of each line so the gutter rows can match. */
#codeInput, .code-mirror{
  padding:9px;
  white-space:pre-wrap; overflow-wrap:break-word;
}
#codeInput{
  flex:1 1 auto; min-width:0; resize:none;
  background:none; border:none; color:var(--text);
  overflow-x:hidden; overflow-y:auto;
}
#codeInput:focus{outline:none;}
/* Clearly a sample, not content: dimmer than real text and never applied. */
#codeInput::placeholder{color:var(--muted); opacity:.5;}
.code-sample-btn{margin-right:auto;}
.code-mirror{
  position:absolute; top:0; left:-9999px; visibility:hidden;
  box-sizing:border-box; pointer-events:none;
}

/* Syntax help, folded away until asked for — canvas.js builds the toggle. */
.side-pane-head{display:flex; align-items:flex-start; gap:8px; margin-bottom:8px;}
.side-pane-head .side-pane-desc{margin:0; flex:1 1 auto;}
/* Accent-tinted at rest — panel2 on panel is dark-on-dark and reads as invisible.
   Matches .help-badge in the palette so "?" means the same thing everywhere. */
.side-help-btn{
  flex:0 0 auto; width:20px; height:20px; padding:0; border-radius:50%;
  display:inline-flex; align-items:center; justify-content:center;
  background:rgba(56,189,248,.14); border:1px solid rgba(56,189,248,.5); color:var(--accent);
  font-family:inherit; font-size:12px; font-weight:800; line-height:1; cursor:pointer;
  transition:.15s;
}
.side-help-btn:hover, .side-help-btn.is-open{background:var(--accent); border-color:var(--accent); color:#04121f;}
.code-help{
  flex:0 0 auto; margin-bottom:8px; padding:10px;
  background:var(--panel2); border:1px solid var(--border); border-radius:8px;
  font-size:11px; color:var(--muted); line-height:1.6;
}
.code-help[hidden]{display:none;}
.code-help b{color:var(--text); font-weight:700;}
.code-help code{
  display:block; margin:5px 0; padding:6px 8px; border-radius:5px;
  background:var(--bg); border:1px solid var(--border); color:var(--accent2);
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:10.5px;
  white-space:pre; overflow-x:auto;
}
.code-help ul{margin:6px 0 0; padding-left:16px;}
.code-help li{margin-bottom:3px;}

/* ---------- RESPONSIVE (phones / narrow viewports) ---------- */
/* Keeps the read-only viewer (share, embed, step-through) usable on a phone:
   the single-row topbar would otherwise overflow off-screen. The topbar wraps
   to stacked rows and the stepper stretches to full width for thumb reach. */
@media (max-width: 640px){
  #topbar{ height:auto; min-height:54px; flex-wrap:wrap; padding:8px 12px; gap:8px 10px; }
  #topbar .title{ font-size:13px; flex:1 1 100%; min-width:0; }
  .topbar-right{ flex-wrap:wrap; width:100%; gap:8px; justify-content:flex-start; }
  .btn{ padding:7px 11px; font-size:12px; }
  /* Phones: full-width bar at the bottom, within thumb reach. */
  .stepper{ top:auto; left:12px; right:12px; bottom:12px; justify-content:space-between; gap:6px; padding:6px 8px; }
  .step-label{ min-width:0; flex:1 1 auto; }
  /* View-only on phones: hide the side panels (free the full-width canvas) and
     the edit-only actions. Editing is locked in JS (canvas.js MOBILE). */
  #palette, #notesPanel{ display:none; }
  #undoBtn, #layoutBtn, #clearBtn, #exportBtn{ display:none; }
  .zoom-control{ right:10px; bottom:10px; }

  /* Embed on a narrow iframe: keep the floating step bar + badge from overflowing. */
  /* Higher specificity than the base .stepper rule above, so the embed bar has
     to restate its own phone sizing or it would stay pinned at 50%. */
  #app[data-embed="true"] .stepper{ left:12px; right:12px; bottom:12px; transform:none; max-width:calc(100vw - 24px); padding:6px 8px; gap:6px; }
  #app[data-embed="true"] .step-label{ min-width:0; }
  .embed-badge{ transform:scale(.9); transform-origin:left bottom; }

  /* Present mode: shrink the control bar so Prev/Step/Next/Exit fit one row. */
  .present-bar{ bottom:14px; width:calc(100vw - 20px); padding:12px 14px; gap:10px; }
  .present-caption{ font-size:16px; }
  .present-controls{ width:100%; gap:8px; flex-wrap:wrap; justify-content:center; }
  .present-nav{ padding:8px 13px; font-size:13px; }
  .present-step{ min-width:0; font-size:12px; }
  .present-exit{ padding:8px 12px; font-size:12px; margin-left:0; }
  #app.presenting .zoom-control{ display:none; }   /* pinch-zoom instead; avoids bar overlap */
}

/* Persistent "View only" chip in the topbar, injected on phones for editable
   diagrams. Always visible so the mode stays discoverable; tap re-opens the pill. */
.view-only-chip{
  display:inline-flex; align-items:center; gap:4px; flex:0 0 auto; margin-left:8px;
  background:var(--panel2); border:1px solid var(--border); color:var(--muted);
  font-size:11px; font-weight:600; padding:3px 9px; border-radius:999px;
  cursor:pointer; white-space:nowrap; font-family:inherit;
}
.view-only-chip:hover{ border-color:var(--accent); color:var(--accent); }

/* One-time "edit on desktop" nudge, injected on phones for editable diagrams. */
.mobile-edit-note{
  position:fixed; left:50%; bottom:14px; transform:translateX(-50%); z-index:60;
  display:flex; align-items:center; gap:8px; max-width:calc(100vw - 24px);
  background:var(--panel); border:1px solid var(--border); border-radius:999px;
  padding:8px 8px 8px 14px; color:var(--text); font-size:12.5px;
  box-shadow:0 8px 24px rgba(0,0,0,.5);
}
.mobile-edit-note span{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.mobile-edit-note button{
  background:none; border:none; color:var(--muted); font-size:14px; line-height:1;
  cursor:pointer; padding:2px 6px; border-radius:6px; flex:0 0 auto;
}
.mobile-edit-note button:hover{ color:var(--text); }

/* First-visit pointer at the step controls.
   The walkthrough is the product, but a diagram doesn't look interactive — the
   same reason .stepper sits on the canvas instead of in page chrome. Anchored
   directly under it (matching its top:16px/right:16px) with a caret pointing
   up, so it reads as being about those buttons and nothing else. */
.stepper-hint{
  /* Above .stepper's z-index:60, not below it: the caret sits at the hint's
     top edge, so any overlap at all draws it behind the bar and the tooltip
     looks unanchored. The `top` here is only a fallback — canvas.js sets the
     real value from the stepper's measured height, which changes with the
     step count's label width and wraps on narrow screens. */
  position:absolute; top:82px; right:16px; z-index:61;
  display:flex; align-items:flex-start; gap:8px; max-width:238px;
  background:var(--panel); border:1px solid var(--accent);
  box-shadow:0 12px 40px rgba(0,0,0,.5);
  padding:10px 8px 10px 12px; border-radius:12px;
  color:var(--text); font-size:12px; line-height:1.45;
  animation:stepper-hint-in .28s ease-out both;
}
.stepper-hint::before{
  content:""; position:absolute; top:-6px; right:26px;
  width:10px; height:10px; background:var(--panel);
  border-left:1px solid var(--accent); border-top:1px solid var(--accent);
  transform:rotate(45deg);
}
.stepper-hint b{color:var(--accent); font-weight:600;}
.stepper-hint button{
  flex:0 0 auto; background:none; border:none; color:var(--muted);
  cursor:pointer; font-size:13px; line-height:1; padding:2px 4px; border-radius:6px;
}
.stepper-hint button:hover{color:var(--text); background:rgba(255,255,255,.06);}
@keyframes stepper-hint-in{from{opacity:0; transform:translateY(-6px);} to{opacity:1; transform:none;}}
/* Present mode owns the screen, and its own bar is the control there. */
#app.presenting .stepper-hint{display:none;}
@media (max-width: 640px){
  .stepper-hint{max-width:calc(100vw - 32px); right:12px;}
}
