@keyframes link-origin-pulse {
  0%, 100% { box-shadow: 0 0 0 3px rgba(181, 133, 10, 0.2); }
  50% { box-shadow: 0 0 0 5px rgba(181, 133, 10, 0.32); }
}

/* ---------- Groups (nested/collapsible containers) ---------- */
.group-frame {
  position: absolute;
  border: 1.6px dashed #a4acbb;
  border-radius: 12px;
  background: rgba(59, 110, 246, 0.03);
  pointer-events: none;
}
.group-frame.selected { border-color: var(--accent); background: rgba(59, 110, 246, 0.06); }

.group-titlebar {
  position: absolute;
  top: -14px;
  left: 12px;
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--bg);
  padding: 2px 8px 2px 4px;
  border-radius: 20px;
  border: 1.6px solid #a4acbb;
  cursor: var(--cursor-black);
  pointer-events: auto;
  max-width: calc(100% - 24px);
}
.group-frame.selected .group-titlebar { border-color: var(--accent); }
.group-titlebar.link-origin {
  border-color: #b5850a;
  box-shadow: 0 0 0 3px rgba(181, 133, 10, 0.2);
  animation: link-origin-pulse 1.1s ease-in-out infinite;
}

.group-title {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink);
  outline: none;
  cursor: text;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 220px;
}

.group-collapse-btn {
  width: 18px;
  height: 18px;
  border: none;
  background: #eef0f3;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-soft);
  cursor: var(--cursor-black);
  flex-shrink: 0;
  padding: 0;
}
.group-collapse-btn:hover { background: var(--accent-soft); color: var(--accent); }
.group-collapse-btn svg { width: 12px; height: 12px; stroke: currentColor; fill: none; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }

.group-chip {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: var(--node-bg);
  border: 1.6px dashed #a4acbb;
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  cursor: var(--cursor-black);
  user-select: none;
}
.group-chip:hover { border-color: #7c8698; box-shadow: var(--shadow-md); }
.group-chip.selected { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.group-chip.link-origin {
  border-color: #b5850a;
  box-shadow: 0 0 0 3px rgba(181, 133, 10, 0.2);
  animation: link-origin-pulse 1.1s ease-in-out infinite;
}
.group-chip.dragging { box-shadow: var(--shadow-md); opacity: 0.95; z-index: 30; }
.group-chip .group-title { font-size: 13.5px; max-width: 200px; }
.group-chip .group-collapse-btn { background: var(--accent-soft); color: var(--accent); }

/* ---------- Concept nodes ---------- */
.concept {
  position: absolute;
  width: max-content;
  min-width: 120px;
  max-width: 340px;
  min-height: 40px;
  padding: 9px 14px;
  background: var(--node-bg);
  border: 1.6px solid var(--node-border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
  font-size: 13.5px;
  line-height: 1.35;
  text-align: center;
  color: var(--ink);
  cursor: var(--cursor-black);
  user-select: none;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: box-shadow 0.12s ease, border-color 0.12s ease;
  /* Wherever a tile ends up visually overlapping a bubble (most likely
     right after a tight alignment), the tile would otherwise win hit-
     testing — it's appended later in the same DOM layer (renderLinks()
     runs after renderNodes()), so it paints on top by default. That let a
     click meant for the bubble underneath grab the tile instead, moving
     just its text while the bubble never budged. Bubbles now always win. */
  z-index: 1;
}
.concept:hover { border-color: #a4acbb; box-shadow: var(--shadow-md); }
.concept.selected { border-color: var(--node-selected); box-shadow: 0 0 0 3px var(--accent-soft); }
.concept.link-origin {
  border-color: #b5850a;
  box-shadow: 0 0 0 3px rgba(181, 133, 10, 0.2);
  animation: link-origin-pulse 1.1s ease-in-out infinite;
}
.concept.editing { cursor: text; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.concept.dragging { cursor: var(--cursor-black); box-shadow: var(--shadow-md); opacity: 0.95; z-index: 30; }
.concept .concept-text {
  outline: none;
  width: 100%;
  word-break: break-word;
  white-space: pre-wrap;
}
.concept-placeholder { color: #9aa2b1; }

/* Auto-linked URL (see linkify.js) — an explicit pointer cursor since
   .concept sets its own custom cursor, which as an inherited CSS property
   would otherwise carry straight through onto the link. */
.concept-text a.concept-link {
  color: var(--accent);
  text-decoration: underline;
  cursor: pointer;
}
.concept-text a.concept-link:hover { color: #2f5fe0; }

/* Small badge marking a bubble that has a copy-pasted image attached — the
   image itself is never shown inline. A real (always-present, shown only
   via the parent's has-image class) element rather than a ::after
   pseudo-element, since it needs its own hover/dblclick handling distinct
   from the bubble underneath it. */
.concept-image-badge {
  display: none;
  position: absolute;
  top: -6px;
  right: -6px;
  width: 18px;
  height: 18px;
  padding: 0;
  border-radius: 50%;
  background: var(--accent) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='5' width='18' height='14' rx='2'/><circle cx='8.5' cy='10' r='1.4' fill='white' stroke='none'/><path d='M21 15l-5.5-5.5L5 20'/></svg>") center / 11px no-repeat;
  border: 1.5px solid var(--bg);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.concept.has-image .concept-image-badge { display: block; }
.concept-image-badge:hover { box-shadow: 0 0 0 3px var(--accent-soft); }

/* ---------- Hover preview of a bubble's attached image(s) ---------- */
.image-preview-popup {
  position: fixed;
  z-index: 250;
  background: white;
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: var(--shadow-md);
  padding: 6px;
}
.image-preview-popup.hidden { display: none; }
/* Exactly one image: the original fixed-size single-preview look —
   non-interactive (a passive peek, nothing to click, no close button —
   hovering away always dismisses it, nothing can trap a click), same as
   before multiple images were possible. */
.image-preview-popup:not(.gallery) {
  width: 240px;
  height: 240px;
  pointer-events: none;
}
.image-preview-popup:not(.gallery) img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: 6px;
}
.image-preview-close { display: none; }
/* More than one: a clickable thumbnail grid instead — see image-preview.js
   for the hover-into-the-popup handling this needs, and for why it has an
   explicit close button (only shown here, not on the single-image case
   above — that one's already unambiguous to dismiss). */
.image-preview-popup.gallery {
  /* NOT position: relative here — the base rule's position: fixed already
     works fine as the close button's absolute-positioning containing
     block, and overriding it breaks positionPopup()'s viewport-relative
     top/left entirely: a fixed element interprets those as "this many px
     from the viewport edge", but a relative one interprets the exact same
     values as "this many px offset from its own normal-flow position" —
     which, appended at the end of <body>, is off in empty space well
     below everything else on the page. Cost real time to track down
     because the popup wasn't hidden by any definition the code checks —
     display:block, no .hidden class — it was just rendering hundreds of
     pixels outside the visible viewport.
  */
  padding-top: 26px;
}
.image-preview-popup.gallery .image-preview-close {
  display: flex;
  position: absolute;
  top: 4px;
  right: 4px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: none;
  background: #f2f4f7;
  color: var(--ink-soft);
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
}
.image-preview-popup.gallery .image-preview-close:hover { background: #e5e8ee; color: var(--ink); }
.image-preview-popup.gallery .image-preview-close svg { width: 12px; height: 12px; stroke: currentColor; fill: none; stroke-width: 2.5; }
.image-preview-popup.gallery .image-preview-grid {
  display: grid;
  grid-template-columns: repeat(3, 72px);
  gap: 6px;
  max-width: 246px;
}
.image-preview-popup.gallery .image-preview-grid img {
  width: 72px;
  height: 72px;
  object-fit: cover;
  border-radius: 6px;
  cursor: pointer;
  display: block;
}
.image-preview-popup.gallery .image-preview-grid img:hover { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Same idea as .concept-image-badge, for a bubble's attached table — sits
   just to its left (right:16px vs. right:-6px) so both badges can show at
   once, since a bubble can host an image and a table together. */
.concept-table-badge {
  display: none;
  position: absolute;
  top: -6px;
  right: 16px;
  width: 18px;
  height: 18px;
  padding: 0;
  border-radius: 50%;
  background: var(--accent) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><rect x='3' y='4' width='18' height='16' rx='1.5'/><path d='M3 9.3h18M3 14.7h18M9 4v16'/></svg>") center / 13px no-repeat;
  border: 1.5px solid var(--bg);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.concept.has-table .concept-table-badge { display: block; }
.concept-table-badge:hover { box-shadow: 0 0 0 3px var(--accent-soft); }

/* Same idea again, for a bubble's link to a local PDF — one step further
   left (right:38px) so image, table, and PDF badges can all show at once. */
.concept-pdf-badge {
  display: none;
  position: absolute;
  top: -6px;
  right: 38px;
  width: 18px;
  height: 18px;
  padding: 0;
  border-radius: 50%;
  background: var(--accent) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M6 3h9l4 4v14H6z'/><path d='M15 3v4h4'/><path d='M9 13h6M9 17h6'/></svg>") center / 13px no-repeat;
  border: 1.5px solid var(--bg);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.concept.has-pdf-link .concept-pdf-badge { display: block; }
.concept-pdf-badge:hover { box-shadow: 0 0 0 3px var(--accent-soft); }

/* Same idea again, for a bubble's link to a web page — one step further
   left still (right:60px) so image, table, PDF, and web-link badges can
   all show at once. */
.concept-weblink-badge {
  display: none;
  position: absolute;
  top: -6px;
  right: 60px;
  width: 18px;
  height: 18px;
  padding: 0;
  border-radius: 50%;
  background: var(--accent) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71'/><path d='M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'/></svg>") center / 13px no-repeat;
  border: 1.5px solid var(--bg);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.concept.has-web-link .concept-weblink-badge { display: block; }
.concept-weblink-badge:hover { box-shadow: 0 0 0 3px var(--accent-soft); }

/* Same idea again, for a bubble's link to another saved Cmap — one step
   further left still (right:82px) so image, table, PDF, web-link, and
   Cmap-link badges can all show at once. */
.concept-cmap-badge {
  display: none;
  position: absolute;
  top: -6px;
  right: 82px;
  width: 18px;
  height: 18px;
  padding: 0;
  border-radius: 50%;
  background: var(--accent) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M7 18a4 4 0 0 1-1-7.9 5 5 0 0 1 9.6-2A4.5 4.5 0 0 1 17 18H7z'/></svg>") center / 13px no-repeat;
  border: 1.5px solid var(--bg);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
}
.concept.has-cmap-link .concept-cmap-badge { display: block; }
.concept-cmap-badge:hover { box-shadow: 0 0 0 3px var(--accent-soft); }

/* ---------- Chooser popup (pdf-link.js / web-link.js) ----------
   Double-clicking a PDF/web-link badge with more than one link attached
   needs some way to say *which* one — this lists them, positioned via the
   same positionPopup() helper the image/table hover previews use. */
.link-chooser {
  position: fixed;
  z-index: 250;
  min-width: 180px;
  max-width: 320px;
  background: white;
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: var(--shadow-md);
  padding: 6px;
}
.link-chooser.hidden { display: none; }
.link-chooser-item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 7px 8px;
  border: none;
  background: transparent;
  border-radius: 6px;
  font-size: 12.5px;
  color: var(--ink);
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.link-chooser-item:hover { background: #f2f4f7; }

/* ---------- Hover preview of a bubble's attached table ---------- */
.table-preview-popup {
  position: fixed;
  z-index: 250;
  max-width: 320px;
  max-height: 260px;
  overflow: auto;
  background: white;
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: var(--shadow-md);
  padding: 8px;
  pointer-events: none;
}
.table-preview-popup.hidden { display: none; }
.table-preview-popup table,
.table-lightbox table {
  border-collapse: collapse;
  font-size: 12px;
}
.table-preview-popup td,
.table-lightbox td {
  border: 1px solid var(--line);
  padding: 4px 8px;
  color: var(--ink);
}

/* ---------- Linking phrases ---------- */
.linklabel {
  position: absolute;
  background: var(--label-bg);
  border: 1.4px solid transparent;
  border-radius: 6px;
  padding: 2px 8px;
  font-size: 12px;
  color: var(--ink-soft);
  text-align: center;
  white-space: pre-wrap;
  width: max-content;
  max-width: 260px;
  outline: none;
  cursor: var(--cursor-black);
  transform: translate(-50%, -50%);
  user-select: none;
  box-shadow: 0 0 0 4px var(--bg);
}
.linklabel:hover { color: var(--ink); }
.linklabel.selected { border-color: var(--node-selected); color: var(--ink); box-shadow: 0 0 0 3px var(--accent-soft); }
.linklabel.link-origin {
  border-color: #b5850a;
  color: var(--ink);
  box-shadow: 0 0 0 3px rgba(181, 133, 10, 0.2);
  animation: link-origin-pulse 1.1s ease-in-out infinite;
}
.linklabel.dragging { cursor: var(--cursor-black); box-shadow: var(--shadow-md); opacity: 0.95; z-index: 30; }
.linklabel.empty { color: #b7bec9; font-style: italic; }
.linklabel[contenteditable="true"] { cursor: text; }

.link-line { stroke: #9aa2b1; stroke-width: 1.8; fill: none; stroke-linejoin: round; }
.link-line.selected { stroke-width: 2.2; }
.link-hit { stroke: transparent; stroke-width: 14; fill: none; stroke-linejoin: round; pointer-events: stroke; cursor: var(--cursor-black); }
.link-preview { stroke: var(--accent); stroke-width: 1.8; stroke-dasharray: 5 4; fill: none; pointer-events: none; }
.link-preview.hidden { display: none; }

/* ---------- Find (Ctrl+F) match highlighting ---------- */
/* Every result gets a soft highlight so you can see at a glance how many
   there are and where; the one currently centered/selected gets a
   stronger ring on top, distinguishing "a result" from "the result". */
.concept.search-match,
.linklabel.search-match,
.group-chip.search-match,
.group-frame.search-match {
  box-shadow: 0 0 0 3px rgba(230, 184, 0, 0.45);
}
.concept.search-match-current,
.linklabel.search-match-current,
.group-chip.search-match-current,
.group-frame.search-match-current {
  box-shadow: 0 0 0 3px #e6b800;
}
