
/* GENERATED by SCUBA.py's _write_if_changed on every server start, from
   this f-string — edit _style_css here, not assets/style.css directly; a
   direct edit there is overwritten on the next restart. */
/* ======================================================
   Portrait-mode overlay (mobile only)
   ====================================================== */
#rotate-overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 999999;
  background: var(--scuba-mobile-overlay-bg);
  color: var(--scuba-white);
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  font-size: 1.1rem;
  text-align: center;
  padding: 2rem;
}
#rotate-overlay .rotate-icon {
  font-size: 3rem;
  animation: rotate-hint 1.5s ease-in-out infinite alternate;
}
@keyframes rotate-hint {
  from { transform: rotate(0deg); }
  to   { transform: rotate(90deg); }
}
/* Touch device held upright — never a narrow desktop window. A mouse always
   reports hover:hover/pointer:fine however narrow the window is dragged, so
   width alone is not evidence of a phone and must not gate this overlay. */
@media (hover: none) and (pointer: coarse) and (orientation: portrait) {
  #rotate-overlay { display: flex; }
}

/* ======================================================
   Demo catalog picker — dataset-loading spinner
   ====================================================== */
.ud-catalog-spinner {
  display: inline-block;
  width: 9px;
  height: 9px;
  flex-shrink: 0;
  margin-left: 8px;
  border: 1.5px solid rgba(117,203,245,0.25);
  border-top-color: var(--scuba-accent);
  border-radius: 50%;
  animation: ud-catalog-spin 0.7s linear infinite;
}
@keyframes ud-catalog-spin {
  to { transform: rotate(360deg); }
}

/* Sunburst/contrasts/statistics panels: dim (don't clear) while a newly
   picked catalog dataset is still loading, so the previous dataset's
   content doesn't flicker to blank for the ~3-4s a cold restore can take.
   Transition lives on the element itself (not the toggled class) since
   other callbacks periodically replace these elements' inline "style"
   prop wholesale — putting it on the permanent id selector keeps the
   fade working regardless of what else touches "style". */
#ud-chart-card, #ud-contrasts-scroll, #ud-log, #ud-stats-summary {
  transition: opacity 0.15s ease-in-out;
}
.ud-stale-dim {
  opacity: 0.35;
  pointer-events: none;
}

/* #ud-chart-card is the one dimmed element that also contains a control: the
   Dive-in launch button sits inside it, absolutely positioned over the chart.
   Opacity on a parent composites its whole subtree, so the button faded with
   the chart AND again under Bootstrap's own :disabled rule — two fades
   multiplied, leaving it barely visible. A child cannot opt out of a parent's
   opacity, so the fade goes on the card's chart content instead of the card,
   and whether the button looks available is left entirely to its disabled
   state (see _analysis_pending in user_data/callbacks.py — the single
   condition that decides it). #ud-btn-open-wrap is excluded alongside the
   button itself now too — it's the button's tooltip-carrying wrapper
   (user_data/layout_upload.py), an unstyled span with no visible box of its
   own, but still a direct child that this selector would otherwise catch. */
#ud-chart-card.ud-stale-dim {
  opacity: 1;
  pointer-events: auto;
}
/* (.ud-design-dim removed. It faded the card on a design change while exempting
   the Dive-in button, which was consistent only for as long as that button was
   also disabled by the same condition. Once the button stopped following the
   design, the exemption left a lit button sitting on a greyed-out sunburst. The
   dim is gone with it — unapplied design changes are signalled by Cancel
   becoming available, not by greying results that are still valid.) */
#ud-chart-card.ud-stale-dim > *:not(.dive-in-launch-button):not(#ud-btn-open-wrap) {
  /* !important because plotly.js writes an inline opacity:1 onto the graph div
     it owns, and an inline declaration outranks any normal rule — the card's
     title span faded correctly while the chart itself stayed lit. Scoped to
     the dim state only, so nothing is overridden the rest of the time. */
  opacity: 0.35 !important;
  pointer-events: none;
  transition: opacity 0.15s ease-in-out;
}

/* The analysis bar advances on a 300ms poll, so without a transition every
   update is a jump cut. 320ms — just over the tick — means one step is still
   gliding as the next arrives, which reads as continuous motion rather than a
   staircase. Pairs with the server-side easing in poll_progress: that decides
   WHAT each tick reports, this decides how it gets there. */
#ud-progress-bar .progress-bar,
#ud-dl-bar .progress-bar {
  transition: width 0.32s linear;
}

/* Significance legend swatch — the silhouette the graph draws for a significant
   node. One class per candidate config.SIG_SHAPE, picked in layout.py, so the
   legend follows the setting instead of hardcoding whichever shape was current
   when it was written. clip-path rather than a border trick: the swatch has to
   read as a silhouette, which is the whole thing that marks the node. CSS
   cannot round a clip-path's corners, so these are the sharp-cornered
   equivalents of Cytoscape's round-* shapes — indistinguishable at 12px.
   All override the inline border-radius:50%; the not-significant swatch keeps
   that circle, since the circle is the contrast being drawn. */
.sig-swatch {
  border-radius: 0 !important;
  border: none !important;
}
/* Rounded square is the odd one out: a real border-radius, not a clip-path,
   which is also the only one of these that matches its cytoscape shape exactly
   (round-rectangle's radius is min(w/4, h/4, 8) — 25% at swatch size). */
.sig-swatch-rectangle { border-radius: 25% !important; }
.sig-swatch-diamond  { clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); }
.sig-swatch-pentagon { clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%); }
.sig-swatch-triangle { clip-path: polygon(50% 0%, 100% 100%, 0% 100%); }
.sig-swatch-hexagon  { clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%); }

/* Collapsible Filters / Contrasts sub-panels (Design accordion item).
   The class is toggled on the pane wrapper (#ud-filters-pane /
   #ud-contrasts-pane); collapsed shrinks the pane to its header and hides
   the scroll body, freeing height for the still-open sibling. Scroll bodies
   are hidden by id — #ud-contrasts-scroll's className is owned by the
   stale-dim callback, so a shared class there wouldn't survive. */
.ud-pane-collapsed {
  flex: 0 0 auto !important;
}
.ud-pane-collapsed #ud-filters-scroll,
.ud-pane-collapsed #ud-contrasts-scroll {
  display: none;
}

/* ======================================================
   Baseline
   ====================================================== */
:root {
  /* Type. Every surface in the app reaches the stack through this token —
     CSS rules and inline Dash styles with var(--scuba-font), canvas and
     plotly through window.scubaFont() (assets/ui_font.js), which resolves
     this same property off the DOM rather than keeping a second copy of the
     string. The one exception is the cytoscape stylesheet, which is built in
     Python and pushed as JSON: it carries UI_FONT_STACK itself, because a
     canvas cannot resolve a CSS variable. */
  --scuba-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif;
  --scuba-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --topbar-h: 52px;
  --panel-header-h: 40px;
  /* --ctrl-top is no longer where the control stacks sit — they hang off the
     bottom edge now — but it is still the ceiling they must not grow past,
     so their max-height is measured from it. */
  --ctrl-top: calc(var(--topbar-h) + var(--panel-header-h) + 12px);
  --ctrl-bottom: 12px;
  /* Width of the panel BODY, excluding the rail — so this figure is literally
     what the panel occupies, and the default is a clean third of the screen.
     It has to stay a bare vw or px value: split_divider.js reads and writes it
     directly and parses it by hand, so a calc() here comes back NaN and
     collapses the panel. The rules that span the whole right-hand side add
     var(--rail-w) themselves rather than folding it in here. */
  --right-panel-w: 33.333vw;
  /* Fits UMAP — the longest rail label — at .rail-label's size, with the icons
     left at 28px. Everything right-hand measures from this, so it is the one
     number to change if the label set grows. */
  --rail-w: 62px;
}

body {
  margin: 0;
  padding: 0;
  overflow: hidden;
  user-select: none;
  -webkit-user-select: none;
  /* Same colour as #app-root below, and the reason is timing, not decoration.
     Three stylesheets land in sequence on a cold load: the inline rule in
     SCUBA.py's index_string (immediate), the dbc Bootstrap theme (which sets
     body to its own #272b30), then this file. Without this declaration the
     theme wins for the whole gap between its arrival and the layout painting
     #app-root over it — measured at ~4s on a throttled connection, and visible
     as a grey screen on load. !important because the theme's own body rule wins
     on source order regardless of where this sheet sits — measured, not assumed.
     Keep in step with #app-root. */
  background: var(--scuba-canvas-bg) !important;
}

#app-root {
  width: 100vw;
  height: 100vh;
  background: var(--scuba-canvas-bg);
}

/* ======================================================
   Split-screen layout
   ====================================================== */
#top-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--topbar-h);
  background: var(--scuba-topbar-bg);
  border-bottom: 1px solid var(--scuba-wash-12);
  /* display/align-items live in the "Top bar layout" block below, which is
     later and therefore wins; declaring them here too did nothing. */
  z-index: 4200;
  box-sizing: border-box;
}

#panel-right {
  position: fixed;
  top: var(--topbar-h);
  right: var(--rail-w);
  bottom: 0;
  width: var(--right-panel-w);
  background: var(--scuba-canvas-bg);
  border-left: 1px solid var(--scuba-wash-10);
  z-index: 1800;
  overflow: hidden;
  /* No longer a container-type: size query container — #panel-pane-info's
     --info-font (below) used to read cqw/cqh off this box, but container
     queries did not visibly resize the text in practice (2026-08-30) even
     though this box's fixed top/right/bottom/width satisfies containment's
     size requirement on paper. assets/info_panel_font.js now measures this
     element directly with a ResizeObserver and writes --info-font itself,
     the same fallback already used for the CT-page icon row
     (assets/landing_svg_size.js) and others — so this is just #panel-right
     again, with no CSS-side role in the info card's type scale. */
}

#panel-header-left {
  position: fixed;
  top: var(--topbar-h);
  left: 0;
  /* Panel plus rail. Same on #split-divider and #graph-wrapper — the three
     rules that span the whole right-hand side rather than just the panel. */
  right: calc(var(--right-panel-w) + var(--rail-w));
  height: var(--panel-header-h);
  display: flex;
  align-items: flex-start;
  justify-content: flex-start;
  gap: 0;
  z-index: 4000;
  box-sizing: border-box;
  padding: 8px 8px 0;
  pointer-events: none;
  background: transparent;
  border-bottom: none;
}

#panel-header-right {
  position: fixed;
  top: var(--topbar-h);
  right: var(--rail-w);
  width: var(--right-panel-w);
  height: var(--panel-header-h);
  display: flex;
  align-items: flex-start;
  gap: 6px;
  box-sizing: border-box;
  padding: 8px 8px 0;
  z-index: 4000;
  pointer-events: none;
  background: transparent;
  border-left: none;
  border-bottom: none;
}

#panel-right-body {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

/* Right panel closed outright — by #btn-expand-left or by dragging the divider
   fully right. The header's width goes to 0 but it has no overflow:hidden, and
   must not get one, since it was lifted out of #panel-right precisely so its
   dropdowns escape that stacking context. Its children would spill leftward over
   the graph, so the strip is hidden. The rail stays, and with #btn-expand-left
   is one of the two ways back. */
body.right-closed #panel-header-right {
  display: none;
}

/* The left panel's twin of #btn-expand-panel, at this header's right edge so
   the pair sits either side of the divider. Mirrors its geometry exactly —
   same 15px, same 4px from the top — with left/right swapped. */
#panel-header-left #btn-expand-left {
  position: absolute;
  right: 8px;
  top: 4px;
  pointer-events: auto;
  flex-shrink: 0;
  width: 15px;
  height: 15px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 3px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0.75;
}
#panel-header-left #btn-expand-left:hover {
  opacity: 1;
}
/* Points left, into the pane it closes. arrow.svg is drawn pointing right and
   mirrored here rather than shipped twice, so the two directions cannot drift.
   Static: this button only ever moves the divider left, so nothing flips it. */
#panel-header-left #btn-expand-left img {
  width: 100%;
  height: 100%;
  transform: scaleX(-1);
}

/* Expand / restore, pinned to the panel's top-left corner. Absolute rather
   than a flex child: the header's 8px padding would hold it off the corner,
   and taking it out of the flow also means it cannot disturb the chips in
   either mode. Safe to overlap them because the network controls' own chips
   sit flush right (see #panel-right-network-controls below), leaving this
   corner empty in GCN mode and the title chip's margin-left:auto clearing it
   in UMAP mode.
   15px is the 21px nav-btn size less 30%. */
#panel-header-right #btn-expand-panel {
  position: absolute;
  /* Matches the header's own 8px gutter, so it lines up with everything else
     the strip holds rather than hugging the corner. */
  left: 8px;
  top: 4px;
  pointer-events: auto;
  flex-shrink: 0;
  width: 15px;
  height: 15px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: 3px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0.75;
}
#panel-header-right #btn-expand-panel:hover {
  opacity: 1;
}
#panel-header-right #btn-expand-panel img {
  width: 100%;
  height: 100%;
}

#split-divider {
  position: fixed;
  top: var(--topbar-h);
  bottom: 0;
  right: calc(var(--right-panel-w) + var(--rail-w));
  width: 8px;
  transform: translateX(50%);
  cursor: col-resize;
  z-index: 3100;
  background: var(--scuba-wash-04);
  display: flex;
  align-items: center;
  justify-content: center;
  user-select: none;
}

#split-divider:hover {
  background: var(--scuba-wash-18);
}

/* When left area is snapped closed, hide left-side elements */
body.left-snapped #control-stack,
body.left-snapped #btn-collapse-controls,
body.left-snapped #panel-header-left,
body.left-snapped #graph-wrapper,
body.left-snapped #cell-selector-page {
  display: none !important;
}

body.left-snapped #panel-right {
  z-index: 4000;
}

/* ...which ties with #panel-header-right's own 4000, and #panel-right comes
   later in the DOM, so it won the tie and painted over the header — taking
   #btn-expand-panel with it. Expanded, that button is the only way back, so it
   has to stay on top. The removed left-hand reopen tab carried a 4100 for this
   exact reason; the button inherited its job and needed its z-index too. */
body.left-snapped #panel-header-right {
  z-index: 4100;
}

/* Hide split panel and controls on landing page */
body.on-landing #panel-right,
body.on-landing #panel-header-right,
body.on-landing #panel-rail,
body.on-landing #split-divider,
body.on-landing #panel-header-left,
body.on-landing #control-stack,
body.on-landing #btn-collapse-controls,
body.on-landing #ctrl-stack-right,
body.on-landing #btn-collapse-controls-right {
  display: none !important;
}
/* On landing: show chip row (lives in #top-bar); hide the normal center */
body.on-landing #landing-chips-row {
  display: flex;
}
body.on-landing #top-bar-center {
  display: none !important;
}
/* On landing: search-wrap must clear chip dropdown menus (z-200000) within #top-bar's stacking context */
body.on-landing #search-wrap {
  z-index: 210000;
}
/* On landing: swap the shared nav buttons for the page's own pair. Fit view
   and Preferences join the hidden side with no swap of their own — neither
   is a landing-page action (there is no graph to fit, and Preferences is
   permanently disabled outside the upload page, see the .in-graph rule
   below) — a permanently dead button is worse than no button, same reasoning
   as that rule's. */
#cs-btn-back, #cs-btn-forward {
  display: none !important;
}
body.on-landing #btn-back,
body.on-landing #btn-forward,
body.on-landing #btn-reset,
body.on-landing #btn-settings {
  display: none !important;
}
body.on-landing #cs-btn-back,
body.on-landing #cs-btn-forward {
  display: inline-flex !important;
}

/* Forward is the back glyph turned around — one icon file, so the two arrows
   cannot drift apart the way two hand-drawn ones would. */
.nav-icon-flip {
  transform: rotate(180deg);
}
/* In the M and G views the rail carries Table and Help, so the top bar stops
   offering a second door to the same two destinations. Preferences goes with
   them: it is an upload-page action, disabled everywhere else, and a permanently
   dead button is worse than no button.

   Keyed on .in-graph rather than :not(.on-landing) — on-landing is derived from
   cell-selector-page alone, so its negation is also true on the upload page,
   which is the one page where preferences is live. */
body.in-graph #btn-show-table,
body.in-graph #btn-help,
body.in-graph #btn-settings {
  display: none !important;
}

#graph {
  width: 100%;
  height: 100%;
}

/* CORRECTED 2026-09-05 — the auto override below was wrong; see the
   2026-09-04 fix's own commit message for the original (incorrect) theory.
   Re-checked the bundled dash_cytoscape.dev.js directly: it does NOT set
   touch-action:none in a real browser — that assignment is gated behind
   ms(), which is `navigator.userAgent.match(/msie|trident|edge/i)`, true
   only for IE/legacy Edge. Confirmed live via Playwright too: the graph
   canvases carry no inline touch-action at all, computed value auto, with
   or without this rule — so forcing !important auto was a no-op the whole
   time, not a fix.
   What cytoscape DOES do unconditionally (its generic DOM-event binder,
   used for every listener it attaches — mousedown, mousemove, wheel, …):
   register with {passive:false} and preventDefault() inside the handler,
   to stop native browser behavior during a drag. Left at the default auto,
   that is exactly the condition under which Chromium can still hand a
   trackpad-synthesized pan gesture to Windows' native DirectManipulation
   path for smooth scrolling before JS's preventDefault gets a say — and
   that native path is what can surface the OS touch-feedback circle. none
   tells the browser "don't attempt native panning here, hand JS the raw
   events" — the opposite of what this rule said before. Only touch/
   pointer-type gestures are touch-action-gated (plain mouse events never
   are), so ordinary mouse-driven panning should be unaffected either way;
   !important is still required to beat the (harmless where it does apply)
   inline style. */
#graph canvas, #graph-right canvas {
  touch-action: none !important;
}

#graph-wrapper {
  position: fixed !important;
  top: var(--topbar-h) !important;
  left: 0 !important;
  right: calc(var(--right-panel-w) + var(--rail-w)) !important;
  bottom: 0 !important;
  width: auto !important;
  height: auto !important;
}

/* ======================================================
   Panel skin
   ====================================================== */
/* !important on all three: Bootstrap's own .popover rule (SLATE and FLATLY
   both compile one) is the exact same selector at the exact same
   specificity (0,1,0), so which one wins was purely a function of
   stylesheet load order — fragile, and the reported bug (the welcome
   modal, className="popover", rendering white while the rest of a
   dark-mode page was correctly dark) is consistent with Bootstrap's rule
   winning that tie. Confirmed by fetching the compiled CSS directly: SLATE
   sets --bs-popover-bg to a literal #32383e (which wouldn't explain white
   on its own) but FLATLY's is var(--bs-body-bg) — so this wins outright
   rather than leaving it to depend on exactly which stylesheet Dash
   happens to serve last. */
.popover {
  background-color: var(--scuba-panel-bg) !important;
  border: 1px solid var(--scuba-wash-15) !important;
  color: var(--scuba-muted) !important;
}


/* ======================================================
   Controls stack
   ====================================================== */
/* Bottom-anchored: the stack grows upward from the bottom edge, and stops
   short of the panel header rather than sliding under the top bar. */
#control-stack {
  top: auto;
  bottom: var(--ctrl-bottom);
  max-height: calc(100vh - var(--ctrl-top) - var(--ctrl-bottom));
  display: flex;
  flex-direction: column;
}

#btn-collapse-controls {
  top: auto;
  bottom: var(--ctrl-bottom);
}

#search-wrap,
#search-wrap *,
#my-multi-dropdown,
#my-multi-dropdown * {
  pointer-events: auto !important;
}

/* ── Input / dropdown icons ─────────────────────────────────────── */
.input-icon {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  width: 16px;
  height: 16px;
  opacity: 1;
  pointer-events: none;
  z-index: 2;
}

#search-input-container #search-input {
  padding-left: 32px !important;
}

#palette-icon {
  z-index: 4001;
}

#my-multi-dropdown .dash-dropdown-trigger {
  padding-left: 32px !important;
  padding-right: 4px !important;
}

/* ======================================================
   Custom search input
   ====================================================== */

/* dbc.Input renders <input id="search-input" class="form-control"> directly */
#search-input,
#search-input:focus,
#search-input:active,
#search-input:focus-visible {
  width: calc(10ch + 56px);
  height: 100%;
  box-sizing: border-box;
  color: var(--scuba-muted) !important;
  background: transparent !important;
  border: none !important;
  border-radius: 0 !important;
  padding: 5px 24px 5px 10px;
  font-size: 13px;
  font-weight: 400;
  outline: none !important;
  box-shadow: none !important;
  caret-color: var(--scuba-text);
  transition: width 0.18s ease, opacity 0.12s ease, padding 0.18s ease;
}

/* M-view: box is wide enough for the "Search gene or module" placeholder */
#search-wrap.search-wide #search-input,
#search-wrap.search-wide #search-input:focus,
#search-wrap.search-wide #search-input:active,
#search-wrap.search-wide #search-input:focus-visible {
  width: calc(20ch + 56px);
}

#btn-clear-search {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  color: var(--bs-secondary-color, rgba(255,255,255,0.5));
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  padding: 0 2px;
  z-index: 1;
}

#btn-clear-search:hover {
  color: var(--bs-body-color, rgba(255,255,255,0.9));
}

#search-input::placeholder {
  color: var(--scuba-muted);
  font-weight: 400;
  opacity: 1;
}

/* Search results dropdown — left/right: 0 (not the old unitless "7", which
   is invalid CSS and drops the whole declaration) so it always tracks
   #search-wrap's actual width, including the wider M-view box. */
#search-results {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  max-height: 450px;
  overflow-y: auto;
  margin-top: 4px;
  background: var(--scuba-panel-bg);
  border: 1px solid var(--scuba-wash-15);
  border-radius: 4px;
  z-index: 5000;
}

#search-results:empty {
  display: none;
}

.search-result-item {
  color: var(--scuba-muted-alt);
  padding: 1px 10px;
  font-size: 14px;
  font-weight: 400;
  cursor: pointer;
  border-radius: 4px;
  margin: 1px 4px;
}

.search-result-item:hover {
  background: var(--scuba-wash-15);
  color: var(--scuba-muted-alt);
}

/* Arrow-key highlighted result (assets/keyboard_shortcuts.js) — same look as
   :hover so keyboard and mouse selection read identically. */
.search-result-item.kb-active {
  background: var(--scuba-wash-15);
  color: var(--scuba-muted-alt);
}

/* ======================================================
   Category dropdown (my-multi-dropdown)
   ====================================================== */
#my-multi-dropdown {
  width: 100%;
  max-width: 100%;
  background-color: var(--scuba-panel-bg) !important;
  border: 1px solid var(--scuba-wash-15) !important;
  border-radius: 4px !important;
  box-sizing: border-box !important;
  min-height: 3.4vw !important;
  z-index: 4000 !important;
  color: var(--scuba-white) !important;
}

#my-multi-dropdown,
#my-multi-dropdown * {
  outline: none !important;
  box-shadow: none !important;
}

#my-multi-dropdown:focus-within {
  border-color: var(--scuba-wash-40) !important;
}

/* Trigger text */
#my-multi-dropdown .dash-dropdown-trigger,
#my-multi-dropdown [class*="singleValue"],
#my-multi-dropdown [class*="placeholder"],
#my-multi-dropdown span {
  color: var(--scuba-light-text) !important;
  font-size: 13px !important;
  font-weight: 600 !important;
}

#my-multi-dropdown input {
  color: var(--scuba-white) !important;
}

/* Arrow + clear X */
#my-multi-dropdown svg {
  fill: var(--scuba-light-text) !important;
  color: var(--scuba-light-text) !important;
}

#my-multi-dropdown [class*="indicatorContainer"] svg {
  fill: var(--scuba-light-text) !important;
}

#my-multi-dropdown [class*="clearIndicator"],
#my-multi-dropdown [class*="dropdownIndicator"] {
  color: var(--scuba-light-text) !important;
  padding: 4px !important;
}

#my-multi-dropdown [class*="indicatorContainer"] {
  padding: 0 6px !important;
}

/* Popover menu */
.dash-dropdown-content {
  background-color: var(--scuba-panel-bg) !important;
  border: 1px solid var(--scuba-wash-15) !important;
  border-radius: 4px !important;
  padding: 4px !important;
  z-index: 9999 !important;
  color: var(--scuba-light-text) !important;
  width: 280px !important;
  min-width: 280px !important;
  max-width: 280px !important;
  max-height: 450px !important;
  box-sizing: border-box !important;
}

/* Options */
.dash-dropdown-options {
  padding: 2px !important;
}

.dash-dropdown-option {
  color: var(--scuba-light-text) !important;
  border-radius: 4px;
  cursor: pointer;
  font-size: 11px;
  font-weight: 400;
  padding: 4px 10px;
}


.dash-dropdown-option:hover,
.dash-dropdown-option[data-highlighted] {
  background-color: var(--scuba-wash-15) !important;
  color: var(--scuba-white) !important;
}

.dash-dropdown-option[aria-selected="true"] {
  background-color: var(--scuba-wash-12) !important;
  color: var(--scuba-white) !important;
}

/* ======================================================
   Inline edit panel (in info box)
   ====================================================== */
#edit-panel label {
  font-size: 11px;
  color: var(--scuba-wash-50);
  display: block;
  margin-bottom: 2px;
  margin-top: 6px;
}

#edit-panel label:first-child {
  margin-top: 0;
}

#edit-panel .dash-input-container,
#edit-panel .dash-input {
  border: none !important;
  outline: none !important;
  box-shadow: none !important;
  background: transparent !important;
  padding: 0 !important;
  margin: 0 !important;
}

#edit-panel .dash-input-element {
  width: 100%;
  box-sizing: border-box;
  color: var(--scuba-text);
  background: var(--scuba-wash-08);
  border: 1px solid var(--scuba-wash-20) !important;
  border-radius: 4px;
  padding: 4px 6px;
  font-size: 12px;
  outline: none !important;
  box-shadow: none !important;
}

#edit-panel .dash-input-element:focus {
  border-color: var(--scuba-wash-40) !important;
  background: var(--scuba-wash-12);
}

#btn-save-edit {
  flex: 1;
  background: rgba(50, 140, 70, 0.35);
  border: 1px solid rgba(80, 180, 100, 0.5);
  border-radius: 4px;
  color: rgba(150, 230, 150, 0.9);
  padding: 4px 10px;
  font-size: 12px;
  cursor: pointer;
}

#btn-save-edit:hover {
  background: rgba(50, 140, 70, 0.55);
}

#btn-delete-node {
  flex: 1;
  background: rgba(140, 40, 40, 0.35);
  border: 1px solid rgba(180, 80, 80, 0.5);
  border-radius: 4px;
  color: rgba(230, 130, 130, 0.9);
  padding: 4px 10px;
  font-size: 12px;
  cursor: pointer;
}

#btn-delete-node:hover {
  background: rgba(140, 40, 40, 0.55);
}

#delete-confirm-panel {
  margin-top: 6px;
}

#delete-confirm-text {
  font-size: 11px;
  color: rgba(230, 150, 150, 0.9);
  margin-bottom: 6px;
  text-align: center;
}

#btn-delete-cancel {
  flex: 1;
  background: var(--scuba-wash-10);
  border: 1px solid var(--scuba-wash-25);
  border-radius: 4px;
  color: var(--scuba-text);
  padding: 4px 10px;
  font-size: 12px;
  cursor: pointer;
}

#btn-delete-confirm {
  flex: 1;
  background: rgba(140, 40, 40, 0.5);
  border: 1px solid rgba(180, 80, 80, 0.6);
  border-radius: 4px;
  color: rgba(230, 130, 130, 1.0);
  padding: 4px 10px;
  font-size: 12px;
  cursor: pointer;
}

#edit-status {
  font-size: 11px;
  color: rgba(100, 200, 100, 0.8);
  margin-top: 4px;
  min-height: 14px;
}

/* ======================================================
   Popovers
   ====================================================== */
.popover {
  position: fixed;
  z-index: 100000;
  padding: 10px 12px;
  border-radius: 4px;
  font-size: 12px;
  white-space: normal;
  overflow-wrap: anywhere;
  display: none;
  max-width: none;
}

/* --info-font itself is declared on #panel-pane-info (below), the common
   ancestor of this card and #info-cell-card — see the note there for why and
   for the proportional formula. Restated here only because .popover sets its
   own fixed 12px font-size (a class, 0,1,0) that would otherwise outrank the
   inherited value on specificity grounds if #info-box ever picks up that
   class again; the id selector here (1,0,0) keeps var(--info-font) winning
   regardless. */
#info-box {
  font-size: var(--info-font);
}

/* Docked, the info pane is no longer inside a .popover, so it stops inheriting
   the things that class contributed to its content. Type scale is safe —
   #info-box declares --info-font itself, above — but colour and wrapping were
   inherited and nothing else restates them. overflow-wrap in particular is what
   keeps a long GO term or an unbroken accession from pushing the card wide.

   Geometry lives here rather than in the callback that shows this pane, so
   there is one place to change it: that callback now sets display and nothing
   else. padding-top clears #btn-expand-panel, which floats over the panel's
   top-left corner from #panel-header-right — without it the category chip sits
   underneath the expand icon. */
/* Table pane. A header strip that stays put, over a scroller that takes both
   axes: fixed font with horizontal scroll, not a font that shrinks to fit —
   there is a legibility floor a narrow panel would breach, and text that
   resized as you dragged the divider would make the table's appearance depend
   on unrelated layout state. #btn-expand-panel is the answer to a wide table. */
#panel-pane-table {
  position: absolute;
  inset: 0;
  flex-direction: column;
  overflow: hidden;
  padding: 26px 0 0 0;
}

.panel-table-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-shrink: 0;
  padding: 0 12px 6px 18px;
}
#panel-table-title {
  font-size: 12px;
  font-weight: 700;
  color: var(--scuba-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#panel-table-container {
  flex: 1 1 0;
  min-height: 0;
  overflow: auto;
}

/* Horizontal scrolling costs you row identity and column meaning unless both
   are pinned. The header row sticks to the top of the scroller; the first
   column sticks to its left, so scrolling right keeps the gene or module you
   are reading about on screen. Opaque backgrounds because sticky cells are
   painted over by what scrolls beneath them otherwise. */
#panel-table-container thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background-color: var(--scuba-panel-bg-alt);
}
#panel-table-container tbody td:first-child,
#panel-table-container thead th:first-child {
  position: sticky;
  left: 0;
  background-color: var(--scuba-panel-bg-alt);
}
#panel-table-container thead th:first-child {
  /* Beats both neighbours: it is sticky on both axes at once. */
  z-index: 3;
}
#panel-table-container tbody td:first-child {
  z-index: 1;
}

/* Help pane. Scrolls as one column — its three topics are plain prose, unlike
   the info card whose two cards manage their own overflow. Same top inset as
   the info pane so both clear #btn-expand-panel. */
#panel-pane-help {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 28px 18px 12px 18px;
  scrollbar-gutter: stable;
  color: var(--scuba-muted);
  font-size: 12px;
  line-height: 1.35;
  white-space: normal;
  overflow-wrap: anywhere;
}

/* Structure pane. Holds one embedded viewer that is told to fill it, so this is
   a plain box with no scrolling of its own — the iframe scrolls its own content.
   Same top inset as Info and Help so all three clear #btn-expand-panel. */
#panel-pane-structure {
  position: absolute;
  inset: 0;
  padding: 28px 12px 12px 12px;
  overflow: hidden;
  color: var(--scuba-muted);
  font-size: 12px;
  line-height: 1.35;
  /* A column of caption + viewer. The pane's `display` is written by
     render_right_panel_type as flex (not block) so this direction applies —
     an inline display:block there would win and the caption would be laid over
     the frame instead of above it. */
  flex-direction: column;
}

/* Provenance. Small, muted, above the viewer and out of its way — the reason
   iCn3D's own title is switched off is that it covers the structure, so this
   must not repeat the mistake. Shown only with the structure it describes: a
   caption that outlived its picture would be describing the wrong thing. */
#structure-caption {
  flex: 0 0 auto;
  margin-bottom: 6px;
  font-size: 11px;
  letter-spacing: 0.02em;
  color: var(--scuba-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  opacity: 0;
  transition: opacity 400ms ease-in;
}
#panel-pane-structure[data-state="ready"] #structure-caption {
  opacity: 1;
}

/* The embed sizes to the pane rather than to the 600x450 NCBI suggests: this
   panel is user-resizable by the divider, and a fixed width would be clipped in
   a narrow panel and stranded in a wide one.

   Hidden with opacity rather than display:none, and deliberately: a display:none
   iframe is not guaranteed to fetch, and this reveals on the load event — no
   fetch would mean no event and a spinner that never stops. Invisible but
   loading is exactly the state wanted. pointer-events keeps it from swallowing
   clicks meant for the spinner area while it is still invisible. */
#structure-frame {
  display: block;
  width: 100%;
  /* Takes what the caption leaves rather than a flat 100%, which would overflow
     the pane by exactly the caption's height. min-height:0 because a flex item
     will not shrink below its content otherwise. */
  flex: 1 1 auto;
  min-height: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
  /* Slow enough to read as the picture settling rather than a cut. The reveal is
     on a timer, not a real ready signal (see SETTLE_MS in structure_frame.js), so
     it will sometimes be a little early — a fade makes being wrong look like
     part of the load instead of a flash. */
  transition: opacity 400ms ease-in;
}

/* The three states, written by assets/structure_frame.js. Default (attribute
   absent) shows the prompt, which is what the pane looks like before anything
   has happened. */
#panel-pane-structure[data-state="ready"] #structure-frame {
  opacity: 1;
  pointer-events: auto;
}

#structure-empty {
  display: none;
  color: var(--scuba-muted);
  font-size: 12px;
}
#panel-pane-structure:not([data-state]) #structure-empty,
#panel-pane-structure[data-state="idle"] #structure-empty {
  display: block;
}

/* Spinner. A ring rather than a bar because it sits over an area of unknown
   height and a ring reads at any size. The muted border is the track and the
   accent one segment of it; rotating the whole thing spins that segment.

   Hidden with opacity and visibility rather than display, so it can fade: the
   spinner going out while the frame comes in is one cross-fade instead of two
   cuts. visibility is transitioned with a delay equal to the fade, which is the
   standard trick for "fade out, then stop taking pointer events" — display
   cannot be transitioned at all. */
#structure-spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px;
  border: 2px solid var(--scuba-wash-14);
  border-top-color: var(--scuba-accent);
  border-radius: 50%;
  animation: structure-spin 700ms linear infinite;
  opacity: 0;
  visibility: hidden;
  transition: opacity 250ms ease-out, visibility 0s linear 250ms;
}
#panel-pane-structure[data-state="loading"] #structure-spinner {
  opacity: 1;
  visibility: visible;
  transition: opacity 250ms ease-in, visibility 0s;
}

@keyframes structure-spin {
  to { transform: rotate(360deg); }
}

/* Respect a system-level request for less motion: the spinner still says
   "working" by being present, it just stops turning. */
@media (prefers-reduced-motion: reduce) {
  #structure-spinner { animation: none; }
}

#panel-pane-info {
  position: absolute;
  inset: 0;
  flex-direction: column;
  overflow: hidden;
  /* Right stays 0 so the cards' own scrollbars render flush at the panel edge;
     their paddingRight is what holds text off the scrollbar. Left 18 + each
     card's own 6 gives 24, matching that 24 on the right — so the gutters read
     equal even though they are built from different pieces. */
  padding: 28px 0 12px 18px;
  color: var(--scuba-muted);
  white-space: normal;
  overflow-wrap: anywhere;
  /* ── Proportional type scale ──────────────────────────
     --info-font is the single base every size in all three info cards (cell,
     gene, module — titles and body alike) is derived from, either as an em
     multiple (.info-cell-name, #info-gene-symbol, .info-module-title, ...) or
     directly (the id-pinned fields further down). A field added later with no
     font-size of its own inherits this base automatically, instead of falling
     back to whatever a <p> or a UA default happens to be.

     Declared here rather than on #info-box so #info-cell-card — a sibling of
     #info-box, not a descendant — inherits it too. Its own font-size already
     read calc(var(--info-font) - 1px), but from a scope that never defined
     the property: a custom property used where it is undefined resolves as
     invalid at computed-value time, which for an inherited property like
     font-size means "act as though unset" — so the card was silently
     inheriting the page's own font-size instead, fixed, moved by no divider
     drag or window resize. Declaring --info-font on the common ancestor of
     all three cards is what makes that reference — and the new proportional
     one below — actually resolve.

     Used to be a clamp() reading cqw/cqh off #panel-right (container-type:
     size, declared there) directly in CSS. Container queries did not visibly
     resize the text in practice (2026-08-30), so assets/info_panel_font.js
     now measures #panel-right with a ResizeObserver and writes --info-font
     itself. Its comment has the live formula and calibration — kept out of
     here so there is one copy, not two that can drift — but the shape
     carries over unchanged: min(width term, height term), width weighted to
     carry the visible range on its own (a divider drag), height only
     binding in a genuinely short window. The coefficients themselves did
     not: the CSS version's evaluated to LESS than its own MIN bound at every
     width up to ~583px — including #panel-right's own ~500px resting width
     — so the clamp floor pinned the text flat for the entire narrow half of
     a drag; nothing ever visibly shrank there either, CSS or JS, until that
     was corrected in the JS rewrite. What's below is the pre-JS fallback
     (SSR paint, and if JS is ever disabled) — a fixed INFO_FONT_SIZE with no
     size-responsiveness of its own. */
  --info-font: 16px;
  font-size: var(--info-font);
}

/* Nothing inside the info cards or the drawer body keeps a size of its own:
   every descendant inherits, so the chain always resolves to --info-font. This
   is what makes "a new field inherits the adjustable size" true by construction
   rather than by convention — and it is deliberately blunt, because the earlier
   attempt (leaving .info-md unstyled and trusting inheritance) still left
   Molecular function rendering at a different size from Location and Process,
   and I could not find the rule responsible from the source alone. Whatever it
   was, it cannot win against this.

   Consequence to know about: the handful of elements that SHOULD differ are
   re-stated below qualified with #info-box, because a bare class selector
   (0,1,0) loses to the ID in "#info-card-gene *" (1,0,0). Add a new deliberate
   exception the same way, or it will silently be flattened to the base size. */
#info-card-gene, #info-card-gene *,
#info-card-module, #info-card-module * {
  font-size: inherit;
}

#info-box {
  top: var(--topbar-h);
  right: 15px;
  left: auto;
  width: 300px;
  max-height: 38vw;
  overflow-y: auto;
}

#help-popover {
  padding: 0;
  /* Kept a step above the base .popover z-index. It once had to out-paint
     #multi-panel, which came later in layout.py and so won on DOM order;
     that card is gone, but the bump costs nothing and there is only one
     of each, shared across views, not per-panel/per-view duplicates. */
  z-index: 100001;
}

/* ── Cell card (M view, nothing selected) ──────────────
   Sits above #info-placeholder in the info pane. Its display is written by a
   clientside callback (flex / none), so nothing here may set display — the rules
   below only describe the card once that callback has decided to show it.

   Gutters are built the same way the gene and module cards build theirs: the
   pane contributes 18px on the left, this adds 6 for 24, and carries the full 24
   on the right itself. Equal numbers here would read as left-heavy. */
#info-cell-card {
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding-left: 6px;
  padding-right: 24px;
  /* A step DOWN from --info-font, and the only one in the app. This card is the
     pane's resting state rather than an answer to a click, so it reads as
     background to the graph, not as content competing with it. Stated in px off
     the base (not a multiplier) so it stays exactly one pixel below whatever
     --info-font currently resolves to — the panel's live proportional value,
     not a fixed INFO_FONT_SIZE; everything in the card inherits from here, so the
     one exception below is expressed in em to follow it down. */
  font-size: calc(var(--info-font) - 1px);
  /* The pane clips its overflow, and this card shares it with the prompt below.
     Letting it shrink keeps a long blurb from pushing that prompt out of sight
     on a short panel — and the shrink is what gives the scroll below something
     to scroll, since a flex child at its content height never overflows. */
  min-height: 0;
  flex-shrink: 1;
  /* Scrolls itself, the way the gene and module cards do — the pane's own
     overflow is hidden, so without this a tall icon plus a long blurb is simply
     clipped with no way to reach the figures. paddingRight above is already the
     24px that holds text off the scrollbar (a browser puts an overflow
     scrollbar at the border edge, ignoring the element's padding), so it needs
     no adjustment here. The scrollbar's own appearance is not set here: it comes
     from the four ::-webkit-scrollbar blocks in assets/z_overrides.css, which
     this id had to be added to — a scrolling surface missing from that list gets
     the browser's default scrollbar, not a plainer SCUBA one. */
  overflow-y: auto;
  overflow-x: hidden;
}

.info-cell-icon {
  object-fit: contain;
  flex-shrink: 0;
  margin-bottom: 10px;
}

.info-cell-name {
  color: var(--scuba-text);
  font-weight: 600;
  /* The one deliberate step up in this card. Everything else in the info pane
     inherits --info-font; see the note above #info-card-gene about why an
     exception has to be stated rather than left to inheritance.

     em, not calc() on --info-font: the card sets its own base one pixel below
     that variable, and an absolute expression here would ignore the reduction
     and leave the name sitting a pixel and a half proud of the text under it. */
  font-size: 1.15em;
  line-height: 1.3;
  margin-bottom: 8px;
}

.info-cell-blurb {
  color: var(--scuba-muted);
  line-height: 1.55;
  /* A measure, not a width: 25 words centred across a wide panel is hard to
     track back to the next line. Narrower than the panel, centred within it.
     34ch -> 42.5ch (+25%, 2026-08-19): reported too narrow at the wider
     --info-font sizes landed the same day (config.py) — short lines from a
     bigger base font read as more, not fewer, wraps. */
  max-width: 42.5ch;
  margin: 0 auto 14px;
}

/* Label/value pairs, one per line, sharing a column so the values align down the
   card however wide the labels are. Left-aligned inside a centred block — the
   numbers are a small table, and a centred table has no edge to read down. */
/* Ruled above AND below. The lower rule is what separates the figures from the
   "Select a node…" prompt underneath, and it lives here rather than on the
   prompt itself for two reasons: #info-placeholder does double duty as a caption
   above the module card in the focused view, where a rule over it would be
   wrong; and hanging it off the last element of the card means it appears and
   disappears with the card, with no second visibility rule to keep in step.

   Both rules are on this same element so they span the same width — a
   border-bottom on #info-cell-card would run edge to edge across its padding box
   and visibly overhang the top one. */
.info-cell-stats {
  display: grid;
  grid-template-columns: auto auto;
  gap: 3px 14px;
  justify-content: center;
  text-align: left;
  width: 100%;
  padding-top: 12px;
  padding-bottom: 14px;
  border-top: 1px solid var(--sep-color);
  border-bottom: 1px solid var(--sep-color);
  opacity: 0.85;
}

/* The prompt under the card. Its colour, size, text-align and margin-top are
   written inline by the info-card callback and cannot be set from here — this
   supplies only the horizontal padding, which is what actually centres it.
   #panel-pane-info pads 18px on the left and 0 on the right (the cards' own
   scrollbars render flush at the panel edge), so text-align: center alone
   centres the prompt in a box that runs past the card's right edge and lands it
   visibly right of centre. These are the card's own 6/24, so the two share a
   centre line. */
#info-placeholder {
  padding-left: 6px;
  padding-right: 24px;
}

.info-cell-stat {
  display: contents;
}

.info-cell-stat-label {
  color: var(--scuba-muted);
}

.info-cell-stat-value {
  color: var(--scuba-text);
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* ── Gene card layout ──────────────────────────────── */
.info-symbol-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 6px;
  margin-bottom: 2px;
}

#info-box #info-gene-symbol {
  font-size: 1.334em;   /* 16/12 of --info-font */
  white-space: nowrap;
}

#info-box .info-synonym {
  font-size: 1em;   /* 12/12 of --info-font */
  opacity: 0.65;
}

#info-box .info-name {
  font-size: 1.084em;   /* 13/12 of --info-font */
  opacity: 0.85;
  margin-bottom: 6px;
}

/* ── Module card layout ────────────────────────────── */
#info-box .info-module-title {
  font-size: 1.5em;   /* 18/12 of --info-font */
  font-weight: 700;
  margin-bottom: 10px;
  overflow-wrap: break-word;
  word-break: break-word;
}

/* Whichever card is showing, its title stays put while the body scrolls — so a
   long annotation never leaves you reading about a module you can no longer
   name. This is what the header chip was doing by accident; done here it says
   the card's own title rather than the UMAP's readout, and it works the same in
   both G and M views.

   Direct children of the scrollers on purpose: position:sticky resolves against
   the nearest scrolling ancestor, so these have to sit at the top level of
   #info-card-gene / #info-card-module, which is where the layout already puts
   them. Opaque background because the text scrolling underneath would otherwise
   show straight through — var(--scuba-canvas-bg) is #panel-right's own colour
   (the same one #app-root uses), the info pane over it being transparent. The
   side gutters need no covering: the cards' own left/right padding insets
   every sibling by the same amount, so nothing is ever painted there. */
#info-card-gene > .info-symbol-row,
#info-card-module > .info-module-title {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--scuba-canvas-bg);
  padding-top: 2px;
  padding-bottom: 4px;
}

/* ── Shared: action buttons row — pinned to card bottom ── */
.info-actions {
  display: flex;
  gap: 8px;
  margin-top: auto;
  padding-top: 8px;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-start;
  flex-shrink: 0;
}

.info-actions button:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

/* Summary and Module annotation remarks need no rule of their own — they are
   built by the same fmt() helper as every other labelled field, so they are
   <strong>Label</strong>: <span>value</span> and inherit that styling. A class
   would not have worked anyway: dcc.Markdown's raw-HTML path drops class
   attributes while keeping style. */
/* Field rows and the show-more titles, pinned to the base by id and !important.
   The inherit rule above should already achieve this; it demonstrably did not for
   Molecular function and the Protein Summary title, and I could not find the
   competing rule by reading the source. So this stops asking and states the
   outcome: a label and its value are the same size as every other field's, and
   the section titles match the body they head.
   var(--info-font) rather than 1em on purpose — an absolute value cannot compound
   through nested markdown wrappers the way em does. Still one knob: change
   INFO_FONT_SIZE and these move with everything else.
   If a field STILL renders larger after this, the stylesheet is not reaching the
   browser (cache) — no CSS rule can outrank an id + !important. */
#info-essential, #info-location, #info-mol-function,
#info-biological-process, #info-top-cell, #info-gene-function,
#info-module-category, #info-module-certainty, #info-module-support,
#info-module-reasoning,
#info-essential *, #info-location *, #info-mol-function *,
#info-biological-process *, #info-top-cell *, #info-gene-function *,
#info-module-category *, #info-module-certainty *, #info-module-support *,
#info-module-reasoning * {
  font-size: var(--info-font) !important;
}

/* The section labels need no size rule of their own: they are emitted inside
   #info-gene-function / #info-module-reasoning, so the descendant pins above
   already hold them at var(--info-font). Bold is what marks them as titles. */

/* Markdown fields keep their paragraph spacing sane; sizing is handled by
   the id pins above. */
.info-md > p:last-child { margin-bottom: 0; }

/* One line between one titled field and the next — Category to Certainty,
   Location to Process, and so on. Each .info-md is one such field, so the gap
   belongs between them rather than inside them; the rule above has already
   flattened the <p> margin dcc.Markdown would otherwise contribute, so this is
   the only thing setting that distance.

   em, not a unitless line-height: em resolves against --info-font, so the air
   between fields scales with INFO_FONT_SIZE exactly as the text does, and a
   line-height here would have opened up the wrapped lines within a field too —
   which is spacing inside a section, not between sections. */
#info-box .info-md {
  margin-bottom: 1em;
}
#info-box .info-md:last-child {
  margin-bottom: 0;
}

/* ── Preferences modal inputs ────────────────────────── */
#ud-settings-modal input:focus,
#ud-settings-modal input:focus-visible {
  border-color: var(--scuba-wash-60) !important;
  outline: none !important;
  box-shadow: none !important;
}
/* Placeholders here read at the same weight as a real value, not the muted grey
   the rest of the app uses. These three fields (Cell type / Sample ID / Donor
   ID) sit in a column with the numeric prefs, and a cached dataset commonly
   fills two of them and leaves the third on its placeholder — at --scuba-muted
   that one row visibly faded out of the stack. `inherit` rather than a repeated
   literal, so it tracks _INP_BASE's own colour. opacity for Firefox, which
   applies one to placeholders by default and would reintroduce the difference.
   The cost is that a placeholder no longer looks distinct from a value; the
   labels beside each field ("Leave blank to auto-detect") carry that. */
#ud-settings-modal input::placeholder {
  color: inherit !important;
  opacity: 1;
}

#hpa-link,
#ot-link {
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
}

/* ======================================================
   Panel header left — chip / dropdown styling
   ====================================================== */
.panel-type-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--scuba-muted);
  text-transform: uppercase;
  padding: 0 10px 0 6px;
  flex-shrink: 0;
  white-space: nowrap;
  opacity: 0.6;
}
.graph-title-chip {
  display: inline-flex;
  align-items: center;
  font-size: 14px;
  font-weight: 400;
  color: var(--scuba-muted);
  background: var(--scuba-panel-bg);
  border: 1px solid var(--scuba-wash-15);
  border-radius: 4px;
  padding: 4px 13px;
  white-space: nowrap;
}
.graph-title-chip--ct {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 11px 2px 5px;
}
.graph-title-chip-icon {
  height: 24px;
  width: 24px;
  object-fit: contain;
  flex-shrink: 0;
}
/* Color-by dropdown in left panel header — compact chip appearance */
#panel-header-left #my-multi-dropdown {
  min-width: 155px;
  font-size: 13px;
}
#panel-header-left #my-multi-dropdown .Select-control {
  border-radius: 4px 0 0 4px;
  border-right: none;
}

/* Only the placement. The border, corners, cursor and alignment that used to be
   restated here are what .title-chip-tag now gives a standalone tag anyway. */
#panel-header-right #umap-title-chip {
  margin-left: auto;
}

/* ── Right panel destination rail ────────────────────────────────────────────
   Replaces the UMAP/GCN switch. Pinned to the screen edge rather than the
   panel's inner edge: the inner edge is #split-divider, and a clickable strip
   flush against a drag target produces constant mis-grabs. Being outside
   #panel-right also means it survives the panel collapsing to zero width, so
   it doubles as the way back.
   z-index clears #panel-header-right (4000) and body.left-snapped #panel-right
   (also 4000), but stays far below the dropdown menus at 200000. */
#panel-rail {
  position: fixed;
  top: var(--topbar-h);
  right: 0;
  bottom: 0;
  width: var(--rail-w);
  z-index: 4200;
  background: var(--scuba-panel-bg);
  border-left: 1px solid var(--scuba-wash-10);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  /* Stacked at the top, not spread over the rail's full height: the buttons
     read as one group of destinations, and the empty rail below them is where
     the eye stops looking. gap, not the old space-evenly distribution — with
     the items packed, a gap is the only thing setting the spacing, and it
     stays the same whatever the rail's height. */
  justify-content: flex-start;
  gap: 4px;
  padding: 5px 0;
  box-sizing: border-box;
  overflow: hidden;
}

.rail-btn {
  /* Not a .nav-btn: those are 21px icon-only action buttons in the top bar,
     and the difference is the point — bare icons fire actions, labelled ones
     select a destination and one of them is always lit. */
  background: transparent;
  border: none;
  padding: 5px 2px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  color: var(--scuba-muted);
  position: relative;
}

.rail-icon {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  /* The mask supplies the shape, this supplies the colour — so the icon
     inherits .rail-btn's colour and every state below is one property. */
  background-color: currentColor;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

.rail-label {
  /* Same treatment as .chip-cat in z_overrides.css, so the rail reads as part
     of the app rather than something newly invented. Uppercase also settles
     what would otherwise be a casing mess: HELP/TABLE/INFO are words and
     GCN/UMAP are acronyms, and Title Case has no good answer for that. */
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  line-height: 1.2;
  white-space: nowrap;
  user-select: none;
}

.rail-btn:hover {
  color: var(--scuba-rail-hover-color);
}

/* A destination that has nothing to show from where you are. Structure is the
   only one so far: it draws the selected GENE's protein, and the module view has
   no genes to select. Same 0.25 as the top-bar's disabled buttons, so "not
   available right now" reads the same everywhere. :hover is overridden rather
   than left out — .rail-btn:hover above would otherwise light it back up on the
   way past. The button element is disabled too, so this is appearance only and
   not the mechanism: a disabled button emits no n_clicks and the rail's click
   handler never sees it. */
.rail-btn:disabled,
.rail-btn:disabled:hover {
  opacity: 0.25;
  color: var(--scuba-muted);
  cursor: default;
}

/* Active destination — the rail carries rail-active-<view>, mirroring the
   right-panel-type store. Full brightness plus an edge bar: with labels
   present, a lit icon alone is too weak a signal for "you are here".
   Selectors generated from RIGHT_PANEL_VIEWS above.
   Gated on body:not(.right-closed): the store keeps remembering the last
   destination while the panel is shut (so reopening lands back on it), but
   nothing should look selected in a rail with no panel behind it. */
body:not(.right-closed) #panel-rail.rail-active-umap .rail-btn[data-view="umap"],
body:not(.right-closed) #panel-rail.rail-active-network .rail-btn[data-view="network"],
body:not(.right-closed) #panel-rail.rail-active-info .rail-btn[data-view="info"],
body:not(.right-closed) #panel-rail.rail-active-table .rail-btn[data-view="table"],
body:not(.right-closed) #panel-rail.rail-active-structure .rail-btn[data-view="structure"],
body:not(.right-closed) #panel-rail.rail-active-help .rail-btn[data-view="help"] {
  color: var(--scuba-white);
}
body:not(.right-closed) #panel-rail.rail-active-umap .rail-btn[data-view="umap"]::before,
body:not(.right-closed) #panel-rail.rail-active-network .rail-btn[data-view="network"]::before,
body:not(.right-closed) #panel-rail.rail-active-info .rail-btn[data-view="info"]::before,
body:not(.right-closed) #panel-rail.rail-active-table .rail-btn[data-view="table"]::before,
body:not(.right-closed) #panel-rail.rail-active-structure .rail-btn[data-view="structure"]::before,
body:not(.right-closed) #panel-rail.rail-active-help .rail-btn[data-view="help"]::before {
  content: "";
  position: absolute;
  left: 0;
  top: 4px;
  bottom: 4px;
  width: 2px;
  background: var(--scuba-accent);
  border-radius: 0 2px 2px 0;
}

.rail-icon-umap {
  -webkit-mask-image: url('/assets/icons/umap.svg');
          mask-image: url('/assets/icons/umap.svg');
}
.rail-icon-network {
  -webkit-mask-image: url('/assets/icons/GCN.svg');
          mask-image: url('/assets/icons/GCN.svg');
}
.rail-icon-info {
  -webkit-mask-image: url('/assets/icons/info.svg');
          mask-image: url('/assets/icons/info.svg');
}
.rail-icon-table {
  -webkit-mask-image: url('/assets/icons/table.svg');
          mask-image: url('/assets/icons/table.svg');
}
.rail-icon-structure {
  -webkit-mask-image: url('/assets/icons/structure.svg');
          mask-image: url('/assets/icons/structure.svg');
}
.rail-icon-help {
  -webkit-mask-image: url('/assets/icons/help.svg');
          mask-image: url('/assets/icons/help.svg');
}

/* flex:1 with justify-content:flex-end (below) makes this strip stretch back to
   the panel-type switch while its chips sit flush right — so most of it is empty
   space lying over the live graph. pointer-events:auto on the strip itself made
   that empty stretch swallow drags: in GCN mode the graph is plainly visible
   there but couldn't be panned. #panel-header-right is pointer-events:none for
   exactly this reason; the opt-back-in belongs on the children that actually
   draw something, not on their full-width flex container. */
#panel-right-network-controls {
  pointer-events: none;
}
#panel-right-network-controls > * {
  pointer-events: auto;
}

/* Right panel network controls (chip group in header) */
#panel-right-network-controls {
  display: none;
  flex: 1;
  align-items: center;
  justify-content: flex-end;
  min-width: 0;
}

/* #panel-right-chip-group's layout comes from .chip-group (z_overrides.css),
   like every other group's. */

/* ── Criteria strip (active selection terms) ─────────────────────────────── */
/* Chips wrap rather than scroll: a term the user cannot see is the exact
   failure this strip exists to prevent, so it grows downward instead of
   hiding its tail. */

/* Right ctrl-stack mirrors left — right-edge aligned */
#ctrl-stack-right {
  top: auto;
  bottom: var(--ctrl-bottom);
  max-height: calc(100vh - var(--ctrl-top) - var(--ctrl-bottom));
  background: var(--scuba-panel-bg);
  /* The chrome's one border: 1px var(--scuba-wash-15), as on #control-stack
     (z_overrides.css), the panel headers and the collapse handles. This was
     0.5px #373A46 — a half-pixel line has no whole device pixel to land on, so
     the browser fakes it by fading, and how thin it ends up looking changes
     with the zoom level. Next to a real 1px neighbour it just reads as a
     lighter border. The colour is a wash either way: #373A46 is within a shade
     of white-at-15% over this background. */
  border: 1px solid var(--scuba-wash-15);
  /* Flush with the page edge, so the outer border would be a line drawn on the
     screen edge — same reason #control-stack drops its border-left. */
  border-right: none;
  overflow-y: auto;
  overflow-x: hidden;
}

#btn-collapse-controls-right {
  top: auto;
  bottom: var(--ctrl-bottom);
}

/* Suppress separator on right color-control-wrap, same as left */
#color-control-wrap-right::before { display: none !important; }

/* Right-panel-specific size-legend layout mirrors left */
#size-legend-m-right .ctrl-legend-items {
  width: 100%;
  justify-content: space-between;
  gap: 0;
}

#size-legend-m-right .ctrl-legend-item {
  flex: 1;
  justify-content: center;
}

/* ── Upload page: sunburst card fills right column height ────────── */
#ud-chart-card {
  flex: 1 1 0;
  min-height: 0;
  overflow: hidden;
  position: relative;
}

/* ── Upload page accordion — dark theme ──────────────────────────── */
.ud-upload-accordion {
  flex: 1 1 0;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  border-radius: 10px;
  border: 0.5px solid var(--scuba-border-accent);
  background-color: var(--scuba-panel-bg);
}

.ud-upload-accordion .accordion-item {
  background-color: var(--scuba-panel-bg);
  border-color: var(--scuba-border-accent);
  flex-shrink: 0;
}

/* Any open item fills remaining space */
.ud-upload-accordion .accordion-item:has(.accordion-collapse.show) {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.ud-upload-accordion .accordion-collapse.show {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.ud-upload-accordion .accordion-body {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0 20px 16px;
  background-color: var(--scuba-panel-bg);
  border-top: 1px solid var(--scuba-wash-07);
}

.ud-upload-accordion .accordion-button {
  background-color: var(--scuba-panel-bg);
  color: var(--scuba-light-text);
  font-size: 12px;
  font-weight: 600;
  padding: 13px 20px;
  box-shadow: none !important;
}

.ud-upload-accordion .accordion-button:not(.collapsed) {
  background-color: var(--scuba-panel-bg);
  color: var(--scuba-light-text);
}

.ud-upload-accordion .accordion-button:focus {
  border-color: transparent;
}

.ud-upload-accordion .accordion-button::after,
.ud-upload-accordion .accordion-button:not(.collapsed)::after {
  filter: invert(0.65);
}

/* Standard app-wide hover highlight (var(--hover-bg), defined in
   z_overrides.css — same token #ud-nav-bar/.dash-dropdown-option use) —
   accordion headers (title + summary blurb) and catalog dataset rows.
   !important is needed here because both already set their own background
   via inline style (selected/unselected row color, section background),
   which would otherwise always win over a plain class rule regardless of
   :hover. */
.ud-upload-accordion .accordion-button:hover,
.ud-upload-accordion .accordion-button:not(.collapsed):hover {
  background-color: var(--hover-bg) !important;
}
.ud-catalog-row:hover {
  background-color: var(--hover-bg) !important;
}
/* ── Welcome modal: proportional type scale ──────────────
   Same idea as #panel-pane-info's --info-font above: one base custom property
   every size in the modal is an em multiple of, so a field added later with no
   font-size of its own inherits proportional sizing instead of a fixed pixel
   size that can clip once the modal is narrower than its resting width.

   Container-query units (cqw, tried first, matching #panel-pane-info) did not
   visibly resize here OR reportedly on #panel-pane-info itself — same
   observation on the one CSS mechanism both share, so this drops container
   queries entirely rather than chasing a second explanation. vw instead:
   plain viewport-width units need no containment setup (no container-type,
   no separate query element — #welcome-modal-content is still the wrapper the
   fix-attempt before this one introduced, kept only because nothing needs it
   removed, not because vw requires it) and have had universal support for
   long enough that they are not a plausible new place for the same failure.

   The modal's own width is a direct function of viewport width (92%, capped
   at 1080px — see #welcome-modal-body below), so vw can track it exactly
   rather than approximate it. clamp(MIN, mid, MAX), mid a straight line
   through two calibration points rather than a single BASE/2 + coefficient
   term (widened on report that the first pass — a 2px MIN..MAX spread —
   was too subtle to notice while actually dragging a window edge):
   mid = 1174px, where 92vw first equals the modal's 1080px cap -> MAX
   (WELCOME_FONT_SIZE, the point past which the modal's own width stops
   growing, so the font should stop too); mid = 850px (a routine half-screen
   resize, not an extreme) -> MIN (WELCOME_FONT_SIZE - 4). Solving that pair
   for slope/intercept gives 1.235vw - 3.5px; below 850px the clamp floors
   at MIN rather than keep falling, so text never shrinks past legibility. */
#welcome-modal-body {
  --welcome-font: clamp(
    9px,
    1.235vw - 3.5px,
    13px
  );
  font-size: var(--welcome-font);
}

#welcome-modal-content, #welcome-modal-content * {
  font-size: inherit;
}

/* Qualified with #welcome-modal-content, not bare classes: a bare class
   (0,1,0) loses to the ID in "#welcome-modal-content *" (1,0,0) above and
   silently collapses back to the base size — the exact trap #info-card-gene's
   own comment warns about, missed here on the first pass (2026-08-30), which
   is why raising WELCOME_FONT_SIZE only ever moved the base 1:1 instead of
   the intro/title text visibly amplifying it. */
#welcome-modal-content .welcome-intro {
  font-size: 1.334em;   /* 16/12 of --welcome-font */
}
#welcome-modal-content .welcome-subtext {
  font-size: 1.084em;   /* 13/12 of --welcome-font */
}
#welcome-close {
  font-size: 1.667em;   /* 20/12 of --welcome-font */
}
#welcome-modal-content .welcome-explore-btn {
  font-size: 1.417em;   /* 17/12 of --welcome-font */
  font-weight: 700;
}
#welcome-modal-content .welcome-card-footer {
  font-size: 0.917em;   /* 11/12 of --welcome-font */
}
.welcome-explore-btn:hover {
  text-decoration: underline;
}

/* ======================================================
   Top bar layout
   ====================================================== */
#top-bar {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 0 6px;
}

#nav-bar {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
}

#top-bar-center {
  display: flex;
  align-items: stretch;
  gap: 8px;
  flex-shrink: 0;
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}


#top-bar .nav-btn,
#nav-bar .btn {
  --bs-btn-color:        var(--bs-body-color);
  --bs-btn-hover-color:  var(--scuba-white);
  --bs-btn-active-color: var(--scuba-white);
  --bs-btn-focus-box-shadow: none;
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
  width: 21px;
  height: 21px;
  padding: 0;
  flex-shrink: 0;
  border-radius: 4px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}

#top-bar .nav-btn:disabled,
#nav-bar .btn:disabled,
#top-bar .nav-btn.disabled,
#nav-bar .btn.disabled {
  opacity: 0.25 !important;
  cursor: default !important;
}

#top-bar .nav-btn:disabled:hover,
#nav-bar .btn:disabled:hover,
#top-bar .nav-btn:disabled *,
#nav-bar .btn:disabled * {
  background-color: transparent !important;
}

#top-bar .nav-btn img,
#nav-bar img {
  width: 13px;
  height: 13px;
  display: block;
}

#search-wrap {
  position: relative;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  border: 1px solid transparent;
  border-radius: 4px;
  background: transparent;
  z-index: 5000;
  transition: background 0.1s, border-color 0.1s;
}

/* Collapsed: icon-only nav-button appearance */
#search-wrap:not(.search-open) {
  width: 21px;
  height: 21px;
  overflow: hidden;
}
#search-wrap:not(.search-open) #search-input-container {
  width: 21px;
  height: 21px;
  justify-content: center;
  cursor: pointer;
  border-radius: 4px;
}
#search-wrap:not(.search-open) #search-input-container:hover {
  background-color: var(--hover-bg) !important;
}
#search-wrap:not(.search-open) .input-icon {
  position: static;
  transform: none;
  left: auto;
  pointer-events: auto;
  cursor: pointer;
}
#search-wrap:not(.search-open) #search-input {
  width: 0 !important;
  padding: 0 !important;
  opacity: 0;
  pointer-events: none;
  /* Collapsing snaps shut instantly rather than shrinking through its own
     text over the input's normal 0.18s width transition (meant for the
     smooth open) — #search-wrap itself already snaps its 21px box with no
     transition, so an animated child inside an instantly-clipped, centered
     flex row was the flicker: for a moment the wide (still-shrinking) input
     pushed the icon out of the visible 21px slice ("icon disappears"),
     showing a clipped sliver of the fading text instead ("text still
     present") until the shrink caught up and re-centered the icon. */
  transition: none;
}
#search-wrap:not(.search-open) #btn-clear-search {
  display: none !important;
}
#search-wrap:not(.search-open) #search-results {
  display: none !important;
}

/* Expanded */
#search-wrap.search-open {
  border-color: var(--scuba-wash-15);
  background: var(--scuba-search-open-bg);
}
#search-wrap.search-open:hover,
#search-wrap.search-open:focus-within {
  background: var(--scuba-hover-bg-2);
  border-color: var(--scuba-wash-40);
}

#search-input-container {
  flex: 1;
  position: relative;
  display: flex;
  align-items: center;
}

/* Upload-page datasource collapse body — scrollable flex column */
.ud-file-body {
  scrollbar-width: thin;
  scrollbar-color: var(--sb-thumb) transparent;
}
.ud-file-body::-webkit-scrollbar { width: 4px; }
.ud-file-body::-webkit-scrollbar-track { background: transparent; }
.ud-file-body::-webkit-scrollbar-thumb { background: var(--sb-thumb); border-radius: 2px; }

/* Upload-page history list — scrollable with standard SCUBA scrollbar.
   No background/border of its own (unlike before) — each entry is now its
   own button-styled row (see _history_panel's _WRAP/_WRAP_SEL), same as the
   demo catalog's rows, rather than a set of plain rows in one shared box. */
.ud-history-list {
  flex: 1 1 0;
  min-height: 60px;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--sb-thumb) transparent;
}
.ud-history-list::-webkit-scrollbar { width: 4px; }
.ud-history-list::-webkit-scrollbar-track { background: transparent; }
.ud-history-list::-webkit-scrollbar-thumb { background: var(--sb-thumb); border-radius: 2px; }

/* Upload-page flat buttons — kills all Bootstrap gradients, colors, shadows */
.ud-flat-btn {
  font-size: 11px !important;
  font-weight: 400 !important;
  color: var(--scuba-muted) !important;
  background-color: var(--scuba-flat-btn-bg) !important;
  background-image: none !important;
  border: 1px solid var(--scuba-wash-28) !important;
  border-radius: 4px !important;
  box-shadow: none !important;
  padding: 5px 10px !important;
  opacity: 1 !important;
}
.ud-flat-btn:hover {
  background-color: var(--scuba-hover-bg-2) !important;
  background-image: none !important;
  color: var(--scuba-muted) !important;
}
.ud-flat-btn:focus,
.ud-flat-btn:active {
  background-color: var(--scuba-hover-bg-2) !important;
  background-image: none !important;
  color: var(--scuba-muted) !important;
  box-shadow: none !important;
  outline: none !important;
}
.ud-flat-btn:disabled,
.ud-flat-btn[disabled] {
  background-color: var(--scuba-flat-btn-bg) !important;
  background-image: none !important;
  color: var(--scuba-muted) !important;
  opacity: 0.45 !important;
  cursor: not-allowed !important;
}

.dive-in-launch-button:not([disabled]) {
  display: flex !important;
  flex-direction: column !important;
  align-items: center !important;
  justify-content: center !important;
  background-color: var(--scuba-flat-btn-bg) !important;
  background-image: none !important;
  border: 1px solid var(--scuba-wash-40) !important;
  border-radius: 50% !important;
  color: var(--scuba-light-text) !important;
  padding: 0 !important;
  font-weight: 400 !important;
  font-size: 13px !important;
  letter-spacing: 1px !important;
  text-transform: uppercase !important;
  text-align: center !important;
  text-shadow: none !important;
  line-height: 1.2 !important;
  box-shadow: 0 0 14px 4px rgba(117, 203, 245, 0.35), 0 4px 15px rgba(0, 0, 0, 0.3) !important;
  transition: background-color 0.2s ease-in-out !important;
  width: 72px !important;
  height: 72px !important;
}
.dive-in-launch-button:not([disabled]):hover {
  background-color: var(--hover-bg) !important;
  background-image: none !important;
  cursor: pointer !important;
  border-color: var(--scuba-wash-40) !important;
  color: var(--scuba-light-text) !important;
  text-shadow: none !important;
}
.dive-in-launch-button[disabled] {
  background-color: var(--scuba-panel-bg) !important;
  background-image: none !important;
  border-color: var(--scuba-disabled-border) !important;
  color: var(--scuba-disabled-text) !important;
  box-shadow: none !important;
  cursor: not-allowed !important;
}

.contcb-card {
  padding: 8px;
  border-radius: 4px;
}

/* Legend titles read as the chips' category labels do — same size, caps,
   tracking and muting as .chip-cat in z_overrides.css, since they name the same
   kind of thing: what the symbols below are measuring. Keep the two in step.
   The margin is the gap to the value list; it used to be undone by a
   `margin: 0 !important` in z_overrides, which is why there was none. */
.contcb-title {
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  line-height: 1.2;
  text-align: left;
  margin-bottom: 8px;
  color: var(--scuba-muted);
  opacity: 0.6;
}

.contcb-txt {
  font-size: 11px;
  text-align: center;
  color: var(--scuba-muted);
}

.contcb-bar {
  height: 220px;
  width: 12px;
  margin: 0 auto;
  border-radius: 4px;
  border: 1px solid var(--scuba-border-dark);
}

.btn-close {
  background: transparent;
  border: none;
  color: var(--scuba-wash-70);
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  padding: 0;
}

.btn-close:hover {
  color: var(--scuba-white);
}

/* Bootstrap owns this class name too, and its rule is `.btn-close:focus {
   box-shadow: 0 0 0 .25rem rgba(13,110,253,.25); opacity: 1 }` — so clicking
   the × left a blue focus ring glowing around it for as long as the button kept
   focus, which is until the modal it closes goes away. A mouse click is not a
   request to see where the keyboard is.

   Scoped to :focus:not(:focus-visible) rather than plain :focus: focus-visible
   is exactly the browser's own "did this focus come from the keyboard"
   judgement, so tabbing to the × still shows the ring and can still be seen. */
.btn-close:focus:not(:focus-visible) {
  box-shadow: none;
  outline: none;
}

/* ── Floating info card ─────────────────────────────────── */
.panel-drag-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 5px 8px;
  height: 30px;
  flex-shrink: 0;
  border-bottom: 1px solid var(--scuba-wash-10);
  cursor: move;
  user-select: none;
}


/* ── 8 resize handles (injected by panel_resize.js) ────── */
.panel-rh {
  position: absolute;
  z-index: 20;
}
.panel-rh-nw { top: 0;    left: 0;   width: 10px; height: 10px; cursor: nw-resize; }
.panel-rh-ne { top: 0;    right: 0;  width: 10px; height: 10px; cursor: ne-resize; }
.panel-rh-sw { bottom: 0; left: 0;   width: 10px; height: 10px; cursor: sw-resize; }
.panel-rh-se { bottom: 0; right: 0;  width: 10px; height: 10px; cursor: se-resize; }
.panel-rh-n  { top: 0;    left: 10px; right: 10px; height: 5px;  cursor: n-resize; }
.panel-rh-s  { bottom: 0; left: 10px; right: 10px; height: 5px;  cursor: s-resize; }
.panel-rh-w  { top: 10px; bottom: 10px; left: 0;   width: 5px;   cursor: w-resize; }
.panel-rh-e  { top: 10px; bottom: 10px; right: 0;  width: 5px;   cursor: e-resize; }


/* ======================================================
   Landing page
   ====================================================== */
#cell-selector-page {
  position: fixed;
  top: var(--topbar-h);
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: stretch;
  justify-content: center;
  overflow: hidden;
}

#landing-inner {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  width: 100%;
  max-width: 1200px;
  height: 100%;
  padding: 15px 32px 10px;
  box-sizing: border-box;
  gap: 6px;
  overflow: hidden;
}


/* Row, not column: [fixed legend gutter][chart column]. See layout.py's
   #landing-body comment for why the gutter sits outside #landing-chart-frame. */
#landing-body {
  display: flex;
  flex-direction: row;
  align-items: stretch;
  flex: 1 1 0;
  min-height: 0;
}

/* Constant slot. Never display:none and never flex-shrink — an empty gutter
   under the non-module readouts is the whole point, so the plot area keeps the
   same width whichever readout is selected. */
#landing-legend-gutter {
  flex: 0 0 128px;
  width: 128px;
  min-width: 128px;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding-left: 6px;
  box-sizing: border-box;
  opacity: 0;
  transition: opacity 0.12s ease;
  pointer-events: none;
  /* Anchor for #landing-gene-card below. Has no effect on the in-flow mod
     legend, so the Mod readout's centring is untouched. */
  position: relative;
}

/* Cell-fraction legend — the third tenant of the gutter. Absolute for the same
   reason as the gene card below: the mod legend is hidden with visibility and
   keeps its box, so an in-flow sibling would be pushed out of the 128px slot. */
#landing-fc-legend {
  position: absolute;
  left: 6px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-family: var(--scuba-font);
  color: var(--scuba-muted);
  font-size: 11px;
  line-height: 1.35;
}

/* Gene card — the Expression/%-positive readouts' tenant of this slot.
   Absolutely positioned rather than a second flex child on purpose: the mod
   legend is hidden with visibility (not display), so it keeps its box, and an
   in-flow card would be pushed off the 128px gutter by that invisible box.
   Taking the card out of flow also guarantees it can never widen the gutter,
   which the chart and cell-icon geometry depend on staying constant. */
#landing-gene-card {
  position: absolute;
  left: 6px;
  right: 3px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-family: var(--scuba-font);
  font-size: 10.5px;
  line-height: 1.3;
  color: var(--scuba-muted);
  /* 128px is not much: break inside words rather than let a long description
     overflow the gutter and collide with the plot's tick labels. */
  overflow-wrap: break-word;
  word-break: break-word;
  hyphens: auto;
  pointer-events: auto;   /* gutter sets none; the HPA/OT links need it back */
}

.landing-gene-card-sym {
  font-size: 12.5px;
  font-weight: 700;
  color: var(--scuba-light-text);
  letter-spacing: 0.01em;
  line-height: 1.15;
}

/* Clamped, not free-running: a handful of genes carry long descriptions and the
   card is vertically centred, so an unbounded one would grow symmetrically into
   both the chart title above and the cell-icon row below. Full text on hover
   via the title attribute. */
.landing-gene-card-desc {
  color: var(--scuba-landing-desc-color);
  display: -webkit-box;
  -webkit-line-clamp: 5;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.landing-gene-card-syn {
  font-size: 9.5px;
  opacity: 0.7;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.landing-gene-card-loc {
  font-size: 9.5px;
  opacity: 0.8;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.landing-gene-card-loc-lbl {
  font-size: 8.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.6;
  display: block;
}

/* Essentiality reads as ordinary field content, matching the location and
   molecular-function values rather than standing out as a chip. It needs no
   label — "Essential" / "Non-essential" says what it is — so it's the value
   style alone, with no .landing-gene-card-loc-lbl above it. */
.landing-gene-card-ess {
  font-size: 9.5px;
  opacity: 0.8;
  overflow-wrap: break-word;
}

.landing-gene-card-links {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 6px;
  font-size: 9.5px;
  margin-top: 1px;
}

.landing-gene-card-links a {
  color: var(--scuba-accent);
  text-decoration: none;
  white-space: nowrap;
}

.landing-gene-card-links a:hover { text-decoration: underline; }

#landing-mod-legend {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-family: var(--scuba-font);
  color: var(--scuba-muted);
  font-size: 11px;
  line-height: 1.35;
}

/* Wraps rather than nowrap: the mod legend's two heads carry a user-supplied
   contrast level name ("higher in Non-inflamed"), which has no length bound —
   a nowrap head would run straight out of the fixed 128px gutter. */
.landing-legend-head {
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  opacity: 0.65;
  white-space: normal;
  overflow-wrap: anywhere;
}
/* Breathing room between the two direction groups only */
.landing-legend-head-2 { margin-top: 10px; }
/* The direction heads end in a contrast level name the user chose the case of
   ("Non-inflamed", "CD") — uppercasing it would misrepresent their label. */
.landing-legend-head-dir { text-transform: none; }

.landing-legend-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding-left: 2px;
  white-space: nowrap;
}

/* Cell-fraction direction rows — same reason as .landing-legend-head above:
   the label ends in a contrast level name, so it must be free to wrap. The
   swatch pins to the first line rather than centring against a two-line label. */
.landing-legend-row-wrap {
  align-items: flex-start;
  white-space: normal;
  overflow-wrap: anywhere;
}
.landing-legend-row-wrap .landing-legend-sw { margin-top: 2px; }

.landing-legend-sw {
  display: inline-block;
  flex: 0 0 auto;
  width: 11px;
  height: 11px;
  border-radius: 2px;
  border: 1px solid var(--scuba-muted);
  box-sizing: border-box;
}

.landing-legend-txt { opacity: 0.9; }

#landing-chips-row {
  display: none;
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  align-items: center;
  padding: 0;
}
/* Lock chip widths to their widest option so they never reflow on selection.
   Static chips: formula max(cat_chars×7, val_chars×9)+42px.
   Dynamic chips (splitby, contrast): width set via CSS variable by a clientside callback. */
#landing-chip-readout  { min-width: 186px !important; }
#ud-chip-splitby, #ud-chip-splitby-right, #landing-chip-splitby { min-width: var(--splitby-chip-min-width,  auto) !important; }
#landing-chip-ud-contrast                 { min-width: var(--contrast-chip-min-width, auto) !important; }

/* The landing group's own copy of the chip-group rules is gone: every group in
   the app is now one .chip-group ruleset in z_overrides.css, and this one was
   the fourth copy of it. #landing-chip-dataset was named there as "always
   first" and #landing-chip-readout as "always last" — neither is reliably
   true, since both are shown and hidden by callbacks. */

#landing-chart-frame {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding-top: 20px;
  opacity: 0;
  transition: opacity 0.12s ease;
}

/* Overlaid on top of the chart's own bottom region rather than taking its own
   flex space below it, so the plot area grows into that space. The chart's
   bottom margin (real Plotly margin — bars can never render into it) is
   measured live from this row's actual rendered height (see the JS figure
   builders in SCUBA.py / user_data/callbacks.py) rather than a fixed guess,
   which wasn't robust across window sizes. Fully transparent — no backdrop
   box. .landing-svg-label/.landing-cat-label used to carry a canvas-colored
   text-shadow here for legibility against whatever sits behind them; removed
   in both themes, so legibility now rests on --scuba-muted's own contrast
   alone. */
#landing-cell-row {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  flex-direction: row;
  align-items: stretch;
  padding-top: 10px;
  padding-bottom: 12px;
  box-sizing: border-box;
  gap: 3px;
  opacity: 0;
  transition: opacity 0.15s;
}

.landing-cat-group {
  display: flex;
  flex-direction: column;
  min-width: 0;
  transition: opacity 0.2s;
}

.landing-cat-cells {
  display: flex;
  flex-direction: row;
  align-items: stretch;
  flex: 1;
  min-width: 0;
}

.landing-svg-slot {
  flex: 1;
  min-width: 0;
  overflow: visible;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  transition: opacity 0.2s;
}

.landing-svg-img {
  display: block;
  opacity: 0.75;
  transition: opacity 0.15s;
}

.landing-svg-slot:hover .landing-svg-img {
  opacity: 1;
}

.landing-svg-label {
  font-size: 12px;
  color: var(--scuba-muted);
  white-space: nowrap;
  text-align: center;
  pointer-events: none;
}

.landing-cat-label {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  font-size: 11px;
  font-weight: 400;
  color: var(--scuba-muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  white-space: nowrap;
  margin-top: 6px;
  pointer-events: none;
}

.landing-cat-label::before {
  content: '';
  align-self: stretch;
  height: 5px;
  margin: 0 2px 4px;
  border-bottom: 1px solid var(--scuba-muted);
  border-left: 1px solid var(--scuba-muted);
  border-right: 1px solid var(--scuba-muted);
}

/* Split-by select: replace Bootstrap's dark chevron with a muted-tone one */
#ud-splitby-select.form-select {
  --bs-form-select-bg-img: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path fill='none' stroke='%23888' stroke-width='2' d='m2 5 6 6 6-6'/></svg>");
}

/* ── Touch devices: suppress all hover visual effects ───────────────────── */
@media (hover: none) {
  #split-divider:hover                                            { background: transparent; }
  #btn-clear-search:hover                                        { color: revert; }
  .search-result-item:hover                                      { background: revert; color: revert; }
  .dash-dropdown-option:hover                                    { background-color: revert !important; color: revert !important; }
  #btn-save-edit:hover                                           { background: revert; }
  #btn-delete-node:hover                                         { background: revert; }
  #search-wrap:not(.search-open) #search-input-container:hover   { background-color: revert !important; }
  #search-wrap.search-open:hover                                 { background: revert; border-color: revert; }
  .ud-flat-btn:hover                                             { background-color: revert !important; background-image: revert !important; color: revert !important; }
  .dive-in-launch-button:not([disabled]):hover                   { background-color: revert !important; border-color: revert !important; color: revert !important; transform: none !important; }
  .btn-close:hover                                               { color: revert; }
  .landing-svg-slot:hover .landing-svg-img                       { opacity: revert; }
  .ud-upload-accordion .accordion-button:hover                   { background-color: revert !important; }
  .ud-catalog-row:hover                                          { background-color: revert !important; }
}

