
/* 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: #0a1628;
  color: #fff;
  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: #75CBF5;
  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-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) {
  /* !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: #000019 !important;
}

#app-root {
  width: 100vw;
  height: 100vh;
  background: #000019;
}

/* ======================================================
   Split-screen layout
   ====================================================== */
#top-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--topbar-h);
  background: #0a0c1b;
  border-bottom: 1px solid rgba(255,255,255,0.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: #07091a;
  border-left: 1px solid rgba(255,255,255,0.10);
  z-index: 1800;
  overflow: hidden;
  /* Query container for #panel-pane-info's --info-font (below): its top/
     right/bottom/width above give it a size that never depends on its own
     content, which is what container-type: size requires. `size`, not
     `inline-size` — the info card's height (a short window, or a tall one)
     matters as much as the divider-driven width does.

     This is the box the old @container rules named "multi-panel" queried,
     back when the info card floated free and could only be narrow. Docking
     it removed that box (main_design.md, the panel-rail rewrite) and the
     rescaling went with it — deliberately, on the reasoning that a docked
     panel has a divider to widen instead. Re-enabled here because the ask
     changed: proportional text for whatever room the panel actually has,
     not just a narrow-panel floor. */
  container-type: size;
}

#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: rgba(255,255,255,0.04);
  display: flex;
  align-items: center;
  justify-content: center;
  user-select: none;
}

#split-divider:hover {
  background: rgba(255,255,255,0.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%;
}

#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
   ====================================================== */
.popover {
  background-color: #141726;
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: var(--scuba-muted);
}


/* ======================================================
   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: #141726;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 4px;
  z-index: 5000;
}

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

.search-result-item {
  color: #bbb;
  padding: 1px 10px;
  font-size: 14px;
  font-weight: 400;
  cursor: pointer;
  border-radius: 4px;
  margin: 1px 4px;
}

.search-result-item:hover {
  background: rgba(255, 255, 255, 0.15);
  color: #bbb;
}

/* 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: rgba(255, 255, 255, 0.15);
  color: #bbb;
}

/* ======================================================
   Category dropdown (my-multi-dropdown)
   ====================================================== */
#my-multi-dropdown {
  width: 100%;
  max-width: 100%;
  background-color: #141726 !important;
  border: 1px solid rgba(255, 255, 255, 0.15) !important;
  border-radius: 4px !important;
  box-sizing: border-box !important;
  min-height: 3.4vw !important;
  z-index: 4000 !important;
  color: #ffffff !important;
}

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

#my-multi-dropdown:focus-within {
  border-color: rgba(255, 255, 255, 0.4) !important;
}

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

#my-multi-dropdown input {
  color: #ffffff !important;
}

/* Arrow + clear X */
#my-multi-dropdown svg {
  fill: #DDD !important;
  color: #DDD !important;
}

#my-multi-dropdown [class*="indicatorContainer"] svg {
  fill: #DDD !important;
}

#my-multi-dropdown [class*="clearIndicator"],
#my-multi-dropdown [class*="dropdownIndicator"] {
  color: #DDD !important;
  padding: 4px !important;
}

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

/* Popover menu */
.dash-dropdown-content {
  background-color: #141726 !important;
  border: 1px solid rgba(255, 255, 255, 0.15) !important;
  border-radius: 4px !important;
  padding: 4px !important;
  z-index: 9999 !important;
  color: #ddd !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: #ddd !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: rgba(255, 255, 255, 0.15) !important;
  color: #fff !important;
}

.dash-dropdown-option[aria-selected="true"] {
  background-color: rgba(255, 255, 255, 0.12) !important;
  color: #fff !important;
}

/* ======================================================
   Inline edit panel (in info box)
   ====================================================== */
#edit-panel label {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.5);
  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: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.2) !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: rgba(255, 255, 255, 0.4) !important;
  background: rgba(255, 255, 255, 0.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: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.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: #171725;
}
#panel-table-container tbody td:first-child,
#panel-table-container thead th:first-child {
  position: sticky;
  left: 0;
  background-color: #171725;
}
#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 rgba(255, 255, 255, 0.14);
  border-top-color: #75CBF5;
  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.

     clamp(MIN, mid, MAX): mid is 16px (INFO_FONT_SIZE, one knob
     for the whole scale's centre) plus min(width term, height term) — the
     SMALLER of the two wins, so a short window can still pull the size down
     even while the panel is wide. cqw/cqh resolve against #panel-right's own
     box (container-type: size, declared there), not #panel-pane-info's
     padded content box, since a size container cannot query itself.

     The two terms are NOT symmetric, on purpose. Width is what the divider
     actually drags, so it is the one meant to carry the visible range on its
     own: 1.2cqw matches INFO_FONT_SIZE at #panel-right's ~500px resting
     width (33vw of a ~1500px window) and reaches MAX by itself once the
     panel passes roughly 760px — comfortably inside a divider drag, or the
     panel's own full-width snap. Height's 1.83cqh is calibrated to sit ABOVE
     the width term for any ordinarily tall window (over ~330px of panel
     height, i.e. a ~380px browser window under the 52px topbar) so it stops
     mattering there and never caps growth the width term should otherwise
     get; it only becomes the smaller — and so the binding — term in a
     genuinely short window, which is the one case this is meant to guard.

     An earlier, symmetric version of these two coefficients matched
     INFO_FONT_SIZE at the resting size but with height weighted equally to
     width — which meant an ordinarily-tall-but-not-huge window (under
     ~1375px) capped growth however far the panel was widened, however wide
     #panel-right actually reached: the whole point of a divider drag went
     silently inert once past a fairly modest width. Reported directly against
     a real window (a fully-snapped, nearly full-width panel that still read
     nowhere near MAX). This shape keeps the same min()-of-two-terms
     structure — a short window can still shrink the type — without making
     height the accidental ceiling on ordinary width-driven growth.

     MIN is -1px off INFO_FONT_SIZE; MAX is +7px — both raised once already
     from -3px/+3px, then again on the growth side to +5px, each time on
     report that the previous number was still too small in practice.

     This replaces a narrower attempt at the same idea: two @container steps
     of 1px each, keyed to a floating card's width alone (#multi-panel, removed
     when the card docked — see the note on #panel-right's own container-type
     for that history). This one is continuous rather than stepped, answers
     for height as well as width, and needs no container-name since nothing
     here uses an @container rule — cqw/cqh as plain length units already
     resolve against the nearest size container. */
  --info-font: clamp(
    15px,
    8px + min(1.2cqw, 1.83cqh),
    23px
  );
  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 — #07091a is #panel-right's own colour, 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: #07091a;
  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: rgba(255,255,255,0.6) !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: #141726;
  border: 1px solid rgba(255, 255, 255, 0.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: #141726;
  border-left: 1px solid rgba(255,255,255,0.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: #ccc;
}

/* 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: #fff;
}
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: #75CBF5;
  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: #141726;
  /* The chrome's one border: 1px rgba(255,255,255,0.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 rgba(255,255,255,0.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 #373A46;
  background-color: #141726;
}

.ud-upload-accordion .accordion-item {
  background-color: #141726;
  border-color: #373A46;
  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: #141726;
  border-top: 1px solid rgba(255,255,255,0.07);
}

.ud-upload-accordion .accordion-button {
  background-color: #141726;
  color: #ddd;
  font-size: 12px;
  font-weight: 600;
  padding: 13px 20px;
  box-shadow: none !important;
}

.ud-upload-accordion .accordion-button:not(.collapsed) {
  background-color: #141726;
  color: #ddd;
}

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

/* ======================================================
   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;
  border-right: 1px solid rgba(255,255,255,0.10);
  padding-right: 4px;
  margin-right: 2px;
}

#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:  #fff;
  --bs-btn-active-color: #fff;
  --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;
}
#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: rgba(255,255,255,0.15);
  background: #1a1d2e;
}
#search-wrap.search-open:hover,
#search-wrap.search-open:focus-within {
  background: #282f48;
  border-color: rgba(255,255,255,0.4);
}

#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: #1e2236 !important;
  background-image: none !important;
  border: 1px solid rgba(255,255,255,0.28) !important;
  border-radius: 4px !important;
  box-shadow: none !important;
  padding: 5px 10px !important;
  opacity: 1 !important;
}
.ud-flat-btn:hover {
  background-color: #282f48 !important;
  background-image: none !important;
  color: var(--scuba-muted) !important;
}
.ud-flat-btn:focus,
.ud-flat-btn:active {
  background-color: #282f48 !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: #1e2236 !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: #1E2236 !important;
  background-image: none !important;
  border: 1px solid rgba(255, 255, 255, 0.4) !important;
  border-radius: 50% !important;
  color: #dddddd !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: #272A42 !important;
  background-image: none !important;
  cursor: pointer !important;
  border-color: rgba(255, 255, 255, 0.4) !important;
  color: #dddddd !important;
  text-shadow: none !important;
}
.dive-in-launch-button[disabled] {
  background-color: #141726 !important;
  background-image: none !important;
  border-color: #3a3d4a !important;
  color: #5c6070 !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 #222;
}

.btn-close {
  background: transparent;
  border: none;
  color: rgba(255,255,255,0.7);
  cursor: pointer;
  font-size: 16px;
  line-height: 1;
  padding: 0;
}

.btn-close:hover {
  color: #fff;
}

/* 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 rgba(255, 255, 255, 0.1);
  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: #999;
  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: #999;
  /* 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: #ddd;
  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: #aaa;
  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: #75CBF5;
  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: #999;
  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 #999;
  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, #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 — legibility against whatever's behind the icons/labels comes from the
   text-shadow on .landing-svg-label/.landing-cat-label instead. */
#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: #999;
  white-space: nowrap;
  text-align: center;
  pointer-events: none;
  text-shadow: 0 0 4px #000019, 0 0 4px #000019, 0 1px 3px #000019;
}

.landing-cat-label {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  font-size: 11px;
  font-weight: 400;
  color: #999;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  white-space: nowrap;
  margin-top: 6px;
  pointer-events: none;
  text-shadow: 0 0 4px #000019, 0 0 4px #000019, 0 1px 3px #000019;
}

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

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

