: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;}

/* The header row is frozen under the top of the viewport while the camera is
   further down the diagram (stickSequenceHeaders in canvas.js), so it floats
   over the messages and has to paint above them — above .edge-label-wrap's
   z-index:9 and its :hover/​.is-active lifts included.

   The float cue is a filter, not a box-shadow, deliberately: .node.processing
   paints its own box-shadow for the component the walkthrough is on, and a
   more specific box-shadow rule here would silently cancel that highlight on
   exactly the two headers you most want lit. drop-shadow composes with it. */
#app.seq-view .node{z-index:12;}
#app.seq-view .node.seq-stuck{filter:drop-shadow(0 6px 10px rgba(0,0,0,.5));}

@media (max-width: 640px){
  /* Bottom-left for thumb reach, but *above* the step bar rather than inside
     it. The stepper goes full-width at the bottom on a phone, so bottom:16px
     put this straight on top of it: measured at 393px the toggle (16,730
     146x35) sat inside the bar (12,720 369x48), covering ⟲ and ⏮ Back
     entirely — the two step controls were unreachable, which is most of what
     "the controls don't work on mobile" turns out to mean. .zoom-control had
     the same collision at the other end, over ▶ and the speed. All three now
     stack; --phone-stepper-clear is the one number they share. */
  .view-toggle{top:auto; bottom:var(--phone-stepper-clear); left:12px;}
  .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. */
/* ⚠️ Sized to match .view-toggle exactly (37px). The two are the same kind of
   object — floating canvas chrome in opposite top corners — and the bar stood
   17px taller than the toggle, which read as two unrelated widgets rather than
   one set of controls. The arithmetic is load-bearing and easy to undo by
   accident: 4px padding + 1px border on each side leaves **27px for the tallest
   child**, so every control below is sized to 27 or under. Add one that is
   taller and the whole bar grows again. The phone query keeps its own larger
   sizes for thumb reach, and --phone-stepper-clear is measured from those. */
.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:4px 10px; border-radius:14px;
}
/* 11.5px is .view-tab's size too, so the two corners read as one type scale
   rather than one being a shrunken copy of the other. 4+17+4+2 = 27. */
.stepper .btn{font-size:11.5px; padding:4px 12px;}
/* 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;}
/* One phrase, never broken across lines. Left to wrap, "Step 10 of 12" split
   after "of" the moment the bar got tight, which reads as a rendering fault
   rather than as a position. Narrow viewports clip it instead (see the phone
   query below), and below 340px it takes a row of its own. */
.step-label{font-size:11.5px; color:var(--muted); min-width:108px; text-align:center; white-space:nowrap;}
/* Two spans so the phone bar can drop the word and keep the glyph, exactly as
   .step-play-text does — canvas.js splitBackLabel() builds them. */
#backBtn{display:inline-flex; align-items:center; gap:5px;}
.step-btn-icon{line-height:1;}
.step-reset{display:inline-flex; align-items:center; justify-content:center; background:none; border:none; color:var(--muted); cursor:pointer; font-size:19px; width:27px; height:27px; padding:0; border-radius:8px; line-height:1;}
.step-reset:hover{color:var(--accent); background:var(--panel);}
/* Autoplay: the walkthrough running itself, injected by canvas.js next to Next
   so the manual transport and the hands-off one read as one group. The playing
   state is the accent outline rather than a filled button — #nextBtn is the
   only .primary in the bar and two competing fills made neither read as the
   main action. */
.step-play{display:inline-flex; align-items:center; gap:6px;}
.step-play-icon{font-size:11px; line-height:1;}
.step-play.is-playing{border-color:var(--accent); color:var(--accent); background:rgba(56,189,248,.12);}
/* Speed sits last: it modifies playback rather than driving it, and it is the
   one control here that reports its own state as its label. */
.step-speed{
  background:none; border:1px solid var(--border); color:var(--muted); cursor:pointer;
  font-size:11.5px; font-weight:700; line-height:1; padding:6px 6px; border-radius:8px;
  min-width:40px; text-align:center; font-variant-numeric:tabular-nums; transition:.15s;
}
.step-speed:hover:not(:disabled){color:var(--accent); border-color:var(--accent);}
.step-speed:disabled{opacity:.45; cursor:default;}
.present-nav.is-playing{background:transparent; color:var(--accent); box-shadow:inset 0 0 0 1px var(--accent);}
/* Secondary against the filled Prev/Next/Play, like .step-speed is in the
   canvas bar: it sets the pace rather than driving the walkthrough. Present
   mode hides .stepper entirely, so this is the only way to reach the speed
   once you are presenting. */
.present-speed{
  background:none; border:1px solid var(--border); color:var(--muted); cursor:pointer;
  font-size:13px; font-weight:700; line-height:1; padding:9px 10px; border-radius:9px;
  min-width:48px; text-align:center; font-variant-numeric:tabular-nums; transition:.15s;
}
.present-speed:hover:not(:disabled){color:var(--accent); border-color:var(--accent);}
.present-speed:disabled{opacity:.35; cursor:default;}
/* Quiet like .step-reset in the canvas bar: returning to the overview is a
   navigation, not one of the two directions the walkthrough moves in. */
.present-reset{
  display:inline-flex; align-items:center; justify-content:center;
  background:none; border:none; color:var(--muted); cursor:pointer;
  font-size:26px; line-height:1; width:42px; height:42px; padding:0;
  border-radius:9px; transition:.15s;
}
.present-reset:hover:not(:disabled){color:var(--accent); background:rgba(255,255,255,.06);}
.present-reset:disabled{opacity:.35; cursor:default;}

#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;}
/* Embed-only (see buildEmbedFullscreenBtn). A hairline sets it apart from the
   zoom trio, since it changes the frame rather than the scale. */
.zoom-control .zoom-fullscreen{display:inline-flex; align-items:center; justify-content:center; margin-left:2px; position:relative;}
.zoom-control .zoom-fullscreen svg{display:block;}
.zoom-control .zoom-fullscreen::before{
  content:""; position:absolute; left:-2px; top:6px; bottom:6px; width:1px; background:var(--border);
}
.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;
}
/* Zoom goes top-right in embeds, the one free corner. At bottom-right it shared
   a strip with the centred step bar, and in a Confluence-width frame (~760px)
   the two collided — made worse once it gained the fullscreen button. Up here
   it can't meet the bar at any width, so no breakpoint has to guess where the
   collision starts. Specific enough to beat the phone query's .zoom-control. */
#app[data-embed="true"] .zoom-control{top:16px; right:16px; bottom:auto;}
/* 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;}
/* The marching ants run on every edge, always, and that is a deliberate call
   rather than an oversight: it is the diagram's signature, and a canvas of
   still dashed lines reads as a picture — which is the one thing this editor
   spends its whole design budget arguing against.

   It is not free. stroke-dashoffset can't be composited, so every animated
   path repaints, and #edgesLayer is overflow:visible — its paint area is the
   whole diagram, not the viewport. On a dense diagram this is the largest
   standing cost on the page, running whether or not anyone is stepping and
   including the edges dimmed to .18. If a big walkthrough ever needs more
   frame budget than the rest of the step path can give back, this is the one
   remaining lever, and narrowing it to .active/.hovered past an edge count is
   the cheap version. Measure before reaching for anything else. */
.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;}

/* The step being read paints above its neighbours. Every wrap otherwise shares
   one z-index, so a dimmed sibling later in DOM order covered the one
   description the walkthrough had just advanced to. Mirrors the :hover lift. */
.edge-label-wrap.is-active{z-index:9;}
/* Chips cleared out of the way of the step being read — either siblings in the
   same dense bundle, or anything from elsewhere that overlaps it. visibility
   rather than display so the boxes stay measurable; see muteCrowdedSiblings()
   and muteChipsOverlapping() in canvas.js. */
.edge-label-wrap.chip-muted{visibility:hidden;}
/* Only a dense bundle's own curves fade. An unrelated edge whose chip was in
   the way keeps its line: that line is the diagram's structure. */
.edge-visible.bundle-muted{opacity:.08;}
.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 — the "?" beside the Code pane's description opens it as a dialog
   (canvas.js builds both). It used to unfold inline and push the editor down by
   most of a screen. */
.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{background:var(--accent); border-color:var(--accent); color:#04121f;}
/* ⚠️ Qualified with .ai-modal, for the reason .howto-modal documents: the base
   .ai-modal{width:440px} sits further down this file and wins on source order
   against a bare class. Laid out like the how-to dialog — a flex column where
   only .howto-body scrolls — so "Got it" stays on screen however short the
   window. */
.ai-modal.code-help-modal{width:min(580px, 94vw); max-width:94vw; max-height:86vh;
  display:flex; flex-direction:column;}
.ai-modal.code-help-modal .ai-modal-actions{flex:0 0 auto; margin-top:14px; padding-top:14px;
  border-top:1px solid var(--border);}
.code-help{font-size:12px; color:var(--muted); line-height:1.6;}
.code-help p{margin:0 0 4px;}
.code-help h4{margin:14px 0 6px; font-size:11px; font-weight:700; letter-spacing:.05em;
  text-transform:uppercase; color:var(--text);}
.code-help-group:first-child h4{margin-top:10px;}
.code-help b{color:var(--text); font-weight:700;}
.code-help code{
  display:block; margin:6px 0; padding:8px 10px; border-radius:6px;
  background:var(--bg); border:1px solid var(--border); color:var(--accent2);
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:11px;
  white-space:pre; overflow-x:auto;
}
.code-help ul, .code-help ol{margin:6px 0 8px; padding-left:18px;}
.code-help li{margin-bottom:3px;}
.code-help ol li{margin-bottom:5px;}

/* Why Apply refused, built by canvas.js. Sits between the editor and the
   "applying rebuilds the canvas" warning, and stays until the line is retyped —
   a toast would auto-dismiss six seconds into the fix. It quotes the offending
   text rather than numbering it: the gutter counts only lines that parsed as
   steps, so a line number would disagree with what's on screen beside it. */
.code-error{
  flex:0 0 auto; margin:8px 0 0; padding:10px;
  background:rgba(248,113,113,.08); border:1px solid rgba(248,113,113,.45);
  border-radius:8px; font-size:11px; color:var(--muted); line-height:1.6;
}
.code-error[hidden]{display:none;}
.code-error b{display:block; color:#fca5a5; font-weight:700; margin-bottom:6px;}
.code-error ul{margin:0; padding-left:16px;}
.code-error li{margin-bottom:5px;}
/* The echoed line is user text of any length, so it wraps rather than
   overflowing the panel — unlike .code-help code, which holds a fixed sample. */
.code-error code{
  display:block; margin-bottom:2px; padding:4px 6px; border-radius:4px;
  background:var(--bg); border:1px solid var(--border); color:var(--text);
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:10.5px;
  white-space:pre-wrap; overflow-wrap:anywhere;
}
.code-error li span{color:var(--muted); font-style:italic;}
.code-error-more{margin:4px 0 0;}
.code-error-hint{margin:8px 0 0; padding-top:8px; border-top:1px solid var(--border);}
.code-error-hint code{
  display:inline; padding:1px 4px; border:none; background:var(--bg);
  color:var(--accent2); white-space:nowrap;
}

/* ---------- 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){
  /* How far the bottom-corner controls have to clear the full-width step bar:
     its 12px offset plus its measured one-row height (48px) plus a 10px gap.
     The ultra-narrow query below restates it for the two-row bar. Kept as one
     variable because .view-toggle and .zoom-control sit in different sections
     of this file and drifting apart would put one of them back under the bar. */
  #app{ --phone-stepper-clear:70px; }
  #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; flex-wrap:wrap; }
  /* Clip rather than wrap: the position text is one phrase (see .step-label
     above). This is only the backstop for viewports narrower than the 340px
     query below can help — at 360px and up the trims here leave it whole. */
  .step-label{ min-width:0; flex:1 1 auto; overflow:hidden; text-overflow:ellipsis; }
  /* Six controls across a 375px bar: the play and back glyphs survive, their
     words do not. Keep this in step with buildAutoplayControls and
     splitBackLabel, which is why both labels are their own spans rather than
     part of the buttons' text. #nextBtn is deliberately not on this list. */
  .step-play-text, .step-btn-text{ display:none; }
  .step-play{ gap:0; }
  #backBtn{ gap:0; }
  .stepper .btn{ padding:7px 9px; font-size:12px; }
  /* Both must refuse to shrink. They are the only children whose min-content
     width is a single glyph, so flexbox charged them the entire overflow: ⟲
     measured 18px at 320px — clipped, and far under any thumb target — while
     the two text buttons beside it kept their full width. */
  .step-reset{ flex:0 0 auto; width:34px; height:34px; font-size:22px; }
  .step-speed{ min-width:34px; padding:7px 4px; flex:0 0 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:12px; bottom:var(--phone-stepper-clear); }

  /* 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; }
  #app[data-embed="true"] .zoom-control{ top:12px; right:12px; }
  .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-speed{ padding:8px 8px; font-size:12px; min-width:42px; }
  .present-reset{ width:36px; height:36px; font-size:22px; }
  .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 */
}

/* Below this the five controls and the position text genuinely cannot share a
   row — measured at 320px the buttons alone want 225px of the 280px available,
   leaving the text ~45px for a phrase that needs ~78. So it takes a row of its
   own, above the transport, where it stays whole and centred. 340 rather than
   360 because the trims in the query above still leave 85px at 360px, and a
   bar that splits in two while one line still fits reads as broken. */
@media (max-width: 340px){
  .step-label{ flex:1 0 100%; order:-1; overflow:visible; }
  /* The bar is two rows here (71px), so the corner controls have further to go. */
  #app{ --phone-stepper-clear:93px; }
}

/* 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;}
}

/* The sequence-view nudge: the stepper hint's mirror image, anchored under the
   .view-toggle at top-left instead of the .stepper at top-right. canvas.js sets
   the real `top` from the toggle's measured box and adds .points-down when the
   toggle has moved to the bottom of the canvas (the phone layout below), which
   turns the caret over so it still points at the control.

   --caret-left is set by canvas.js from the *Sequence tab's* measured centre,
   not the toggle's. The toggle's left edge is the Flow tab — the view you are
   already on — so a fixed caret there aimed the hint at the wrong button. The
   fallback below only applies before that measurement lands. */
.seq-nudge{
  position:absolute; top:74px; left:16px; z-index:61;
  display:flex; align-items:flex-start; gap:8px; max-width:290px;
  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;
}
.seq-nudge::before{
  content:""; position:absolute; top:-6px; left:var(--caret-left, 26px);
  width:10px; height:10px; background:var(--panel);
  border-left:1px solid var(--accent); border-top:1px solid var(--accent);
  transform:rotate(45deg);
}
.seq-nudge.points-down::before{
  top:auto; bottom:-6px;
  border-left:none; border-top:none;
  border-right:1px solid var(--accent); border-bottom:1px solid var(--accent);
}
.seq-nudge b{color:var(--accent); font-weight:600;}
/* A button rather than more prose: the whole point is that switching is one
   click, and describing the view is a worse argument than showing it. */
.seq-nudge-go{
  display:block; margin-top:8px; padding:5px 11px; border-radius:8px;
  background:var(--panel2); border:1px solid var(--border); color:var(--accent);
  font-family:inherit; font-size:11.5px; font-weight:600; cursor:pointer; transition:.15s;
}
.seq-nudge-go:hover{border-color:var(--accent); background:rgba(56,189,248,.1);}
.seq-nudge-close{
  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;
}
.seq-nudge-close:hover{color:var(--text); background:rgba(255,255,255,.06);}
/* Present mode owns the screen, and the toggle it points at is hidden there. */
#app.presenting .seq-nudge{display:none;}
@media (max-width: 640px){
  .seq-nudge{max-width:calc(100vw - 32px); left:12px;}
}
