/* OncoPTM v2 shell: STRUCTURAL ONLY.
 *
 * This file adds the top menubar and the document views (Guide / FAQ / Give) and
 * does nothing else. It deliberately inherits v1's existing dark theme from
 * ../dashboard/styles_v1.3.css: the tan-paper restyle is a separate, later step,
 * so that layout changes and visual changes can be reviewed independently.
 */

/* Always reserve the scrollbar track. Without this, opening a long FAQ answer can
 * push the page past the viewport, the scrollbar appears, and every centered
 * element jumps left by its width, and the accordion looked like it was resizing the
 * questions. Reserving the gutter makes the content box width constant whether or
 * not the page scrolls. */
html {
    scrollbar-gutter: stable;
}

/* Fixed app shell: the menubar and the sidebar are furniture, not content.
 *
 * Previously the menubar was position:sticky while the sidebar sat in normal flow, so
 * scrolling the hotspots table pinned the top bar but slid the filter panel up out of
 * view. The two bars appeared to come unstuck from each other. Now the viewport is the
 * frame: body never scrolls, and the sidebar and the content pane each scroll
 * independently inside it, so both bars stay put no matter what.
 *
 * v1 sets .dashboard-wrapper { min-height: 100vh }; overridden here, or the wrapper
 * would be a full viewport tall *below* the menubar and force a page scrollbar. */
html, body { height: 100%; }
body {
    display: flex;
    flex-direction: column;
    height: 100vh;
    overflow: hidden;      /* no page scroll; the panes below scroll instead */
}
.op-menubar {
    position: static;      /* sticky is redundant once the body cannot scroll */
}
.dashboard-wrapper {
    flex: 1 1 auto;
    min-height: 0;         /* lets the flex child actually shrink so children can scroll */
    overflow: hidden;
}
/* The sidebar should fit without scrolling: it is a fixed set of filters, and having to
 * scroll to reach one you cannot see is the worst case for a control panel. Achieved by
 * removing/condensing content below rather than by clipping. `overflow-y: auto` stays as
 * a safety net so a short window or a browser zoom can still reach everything instead of
 * silently cutting controls off. If a scrollbar ever appears here, the fix is to condense
 * further, not to hide the overflow. */
.sidebar {
    overflow-y: auto;
    overscroll-behavior: contain;   /* scrolling past its end must not move the page */
}
.sidebar .filter-section {
    /* Beats the inline `style="gap: 12px"` on the element. */
    gap: 7px !important;
}

/* The drawer had two entry points, this one and a "Studies: N" button in the menubar.
 * The menubar copy is the one that went: the drawer slides out of the left edge, so its
 * trigger belongs on that edge next to the filters it scopes, not diagonally opposite the
 * panel it opens. The menubar button and its styles are deleted, not hidden: nothing in
 * v1 or the shell wrote to it, so there was no reason to keep a dead node around. */
.sidebar .control-group:has(> #btn-open-cohort) { order: 0; }

/* Loss Type offers Missense and In-frame Indel, but the events are 601,848 missense to
 * 393 in-frame indel (0.07%), so unticking either changes almost nothing visible while
 * costing a permanent slot in a panel that must not scroll. Hidden with both boxes left
 * ticked, so every event still counts; nothing is silently excluded. Hidden rather than
 * deleted because applyFilters() reads .checked on both unconditionally. */
.sidebar .control-group:has(> label[for="filter-loss-type"]) { display: none !important; }

/* Loss/Gain Mechanism moves up beside PTM Type and Analysis Mode, which is where it
 * belongs: all three answer "which events am I looking at", while everything below is a
 * narrowing filter. Reordered with flex `order` rather than by moving the markup, so v1's
 * DOM, and every querySelector into it, is untouched.
 *
 * Explicit numbers, not a single negative value: `order: -1` on the mechanism group alone
 * would lift it above PTM Type rather than to third. Everything unnamed shares order 9 and
 * so keeps its existing relative DOM order, since equal order values fall back to document
 * order. Matched via :has() because these three .control-group wrappers carry no ids of
 * their own; the ids are on the controls inside them. */
.sidebar .filter-section > * { order: 9; }
.sidebar .control-group:has(#ptm-toggle) { order: 1; }
.sidebar .control-group:has(#mode-loss) { order: 2; }
.sidebar .control-group:has(#filter-mechanism) { order: 3; }

/* The hotspot controls sit immediately under those three, because the table they drive is
 * what this front end opens on. Without explicit orders they would fall into the unnamed
 * order-9 pool and land below Search Gene and the cohort button, halfway down the rail, even
 * though the markup puts them right after Analysis Mode. Group Hotspots By first: it decides
 * what a row even IS in that table, so the three narrowing options below it read as
 * qualifications of it.
 *
 * There is no separate order for the evidence-tier control any more. It was a <select> in a
 * group of its own, injected into a host div that had to be dissolved with
 * `display: contents` before it could be ordered at all; it is now a checkbox inside the
 * Hotspot Options group that order 5 already places, so it moves with its neighbours and
 * needs no rule. */
.sidebar .control-group:has(#hotspot-bucket-toggle) { order: 4; }
.sidebar .control-group:has(#hotspot-pool-wrap) { order: 5; }
.main-content {
    min-height: 0;
    overflow-y: auto;
    /* v1's 40px is sized for a page that opens with a title block. This one opens with a
     * toolbar, and 40px of empty space above a toolbar reads as a rendering fault. */
    padding-top: 14px;
    /* Likewise v1's 40px bottom padding, which sat under a table now sized to the viewport
     * and so only pushed a second scrollbar onto .main-content. */
    padding-bottom: 12px;
}

/* Nothing follows the hotspots card, so its bottom margin is 20px of dead space between
 * the table and the padding below it. */
#hotspots-view > .card { margin-bottom: 0; }

/* ---- the strip between the menubar and the sticky table header ----
 *
 * WHAT THE GAP ACTUALLY WAS. The `padding-top: 14px` set just above, and not any margin on
 * the card. A sticky element does not stick to the top of its scroll container's border box;
 * it sticks inside the container's padding, because the sticky rectangle is the scrollport
 * INSET BY THE SCROLL CONTAINER'S PADDING. .main-content is the scroll container in v2, so
 * `top: 0` on the table's <thead> resolved to 14px below the menubar and the rows kept
 * scrolling through that 14px strip of page background forever. Nothing about the sticky
 * positioning itself was wrong: `top: 0` was correct, the ancestor chain was correct, and no
 * amount of reading the <thead> rule in styles_v1.3.css would have shown it, because the
 * offending declaration is on an element five levels up in a different file.
 *
 * Zeroed rather than compensated for with a negative `top`, which is the other way to close
 * it. A negative offset would be a v2-only number written into a rule that v1 and the Tauri
 * build also read, and there the scroll container is the viewport with no menubar above it,
 * so the same negative value would pull the header off the top of the window. The padding is
 * v2's, so v2 is where it is removed.
 *
 * Only for the hotspots view, and only while it is the visible one. This is the one view that
 * opens directly onto a full-bleed table; the others open on a card that needs the breathing
 * room. Same :has() test and the same reasoning about inline display styles as the full-bleed
 * rule in styles_v1.3.css, which is the rule this pairs with: that one zeroes the left and
 * right page inset, this one zeroes the top. */
.main-content:has(#views-container > #hotspots-view:not([style*="display: none"])) {
    padding-top: 0;
}

/* The hotspots table used to have its own inner scroller, capped here to the content pane's
 * height. That cap is gone on purpose: a table that scrolls inside a fixed-height box can
 * never let the pager below it leave the viewport, so the pager read as permanently pinned
 * even though nothing set `position: sticky`. The table now grows to its full height and
 * .main-content scrolls instead, which puts the pager at the bottom of the content where you
 * reach it after the last row. The sticky `thead` keeps `top: 0` and pins correctly, because
 * .main-content is the scroll container and already starts below the menubar. */

/* ---------------------------------------------------------------- menubar -- */

/* The sidebar is a fixed 280px + 1px right border, and box-sizing is border-box
 * globally, so the brand block is pinned to exactly 281px. That makes the tab row
 * begin precisely where the sidebar ends instead of straddling the boundary. */
:root { --op-sidebar-w: 281px; }

.op-menubar {
    flex: 0 0 auto;
    display: flex;
    align-items: stretch;
    padding: 0 20px 0 0;
    background: var(--bg-menubar);
    border-bottom: 1px solid var(--border-color-strong);
    position: sticky;
    top: 0;
    z-index: 60;
}

.op-brand {
    display: flex;
    align-items: center;
    justify-content: center;   /* centered over the sidebar column below it */
    flex: 0 0 var(--op-sidebar-w);
    padding: 14px 16px;
    /* No divider: the sidebar's own right border already marks the column edge, and a
     * second rule inside the menubar read as a seam cutting the bar in two. */
}
/* A <button> now, so the UA chrome comes off explicitly: colour is inherited rather than
 * left as buttontext, which is the one that would have turned the wordmark grey. */
.op-wordmark {
    appearance: none;
    background: none;
    border: 0;
    padding: 0;
    font-family: inherit;
    color: inherit;
    cursor: pointer;
    font-size: 1.6rem;
    font-weight: 600;
    letter-spacing: -0.015em;
    line-height: 1.1;
}
.op-wordmark:focus-visible { outline: 2px solid var(--primary); outline-offset: 3px; }

/* "Onco" + "PTM" in two colours, because the split is the actual subject of the site: an
 * oncology cohort on one side, a post-translational modification on the other. It also puts
 * the accent in the top left corner, which is what makes olive read as this site's colour
 * by the time you meet it again on a tab or a ticked box. */
.op-wordmark em {
    font-style: normal;
    color: var(--primary);
}

.op-tabs {
    display: flex;
    align-items: stretch;
    gap: 2px;
    flex: 1 1 auto;
    padding-left: 10px;
}

/* Tabs carry both chrome hues, and they are the clearest place to see why the split is worth
 * having: straw arrives under the cursor and leaves with it, olive stays on the one tab you
 * are actually on. The indicator is a pseudo-element rather than border-bottom so hover and
 * active share one geometry and neither changes the tab's box, which would nudge the label. */
.op-tab {
    appearance: none;
    position: relative;
    background: none;
    border: 0;
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    padding: 0 15px;
    border-radius: 6px 6px 0 0;
    cursor: pointer;
    transition: color 0.14s, background-color 0.14s;
}
.op-tab::after {
    content: '';
    position: absolute;
    left: 9px;
    right: 9px;
    bottom: 0;
    height: 2px;
    border-radius: 2px 2px 0 0;
    background: transparent;
    transition: background-color 0.14s, left 0.14s, right 0.14s;
}

.op-tab:hover {
    color: var(--text-primary);
    background-color: var(--straw-wash);
}
.op-tab:hover::after { background: var(--straw-line); }

/* The active cap runs the full width of the tab and sits ON the menubar's bottom rule, so it
 * reads as that tab being open onto the page below rather than as an underlined word. */
.op-tab.is-active {
    color: var(--primary-light);
    font-weight: 600;
}
.op-tab.is-active::after {
    background: var(--primary);
    left: 0;
    right: 0;
    height: 2.5px;
}
/* Hovering the tab you are already on must not hand it back to straw: you have not moved. */
.op-tab.is-active:hover {
    background-color: transparent;
}
.op-tab.is-active:hover::after { background: var(--primary); }

.op-tab:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: -3px;
    border-radius: 6px;
}

/* v1's .header row holds two things, and neither belongs here.
 *
 * Its title ("513-Study Cohort Sequon Loss Overview" plus a sentence naming the PTM type
 * and cohort) restates what is already on screen: the active tab, the PTM type in the
 * sidebar, and the study count on the sidebar's cohort button. Its .op-legacy-nav button
 * row is superseded by the menubar.
 *
 * Both were hidden individually before, which left an empty row that still drew the 32px
 * .main-content flex gap beneath it, so every view began a row lower than it needed to.
 * Hiding the row itself reclaims the gap as well.
 *
 * display:none rather than deletion, for both: app_v1.3.js writes into the title nodes on
 * every cohort and PTM change, and shell.js drives view transitions by calling .click() on
 * the legacy buttons so v1's own listeners stay in charge. Both work fine on a
 * display:none element; deleting the markup would mean guarding v1's code instead. */
.main-content > .header { display: none !important; }

/* Hotspot granularity had a block of overrides here (flex-direction, width, text-align) whose
 * only job was to undo an earlier vertical-stack treatment and force the control back into a
 * horizontal segmented bar in the card header. The control has since moved onto the sidebar
 * rail, where .mode-toggle is already a horizontal row and every one of those declarations
 * restated a default, so the block is deleted rather than carried along. Its wrapping
 * behaviour on the fixed-width rail is set once, next to #ptm-toggle's, in styles_v1.3.css.
 *
 * The reasoning worth keeping: it must stay a row and not a stack. The table below is sized
 * to the viewport, and as a 116px stack this was the single biggest consumer of the space
 * above it, most of why the table used to run past the bottom of the window. */

/* ------------------------------------------ cohort selector as a drawer ---- */
/* v1 presents study selection as a centered, screen-dimming modal. Here it is
 * re-presented as a drawer that slides out of the left menu and stays out until
 * a cohort is applied. Purely a CSS re-skin of v1's existing markup: every id
 * inside is untouched, so applyCohortSelection() / openCohortModal() and their
 * listeners keep working exactly as before.
 *
 * --op-menubar-h is measured and set by shell.js, so the drawer tucks under the
 * menubar and the tabs stay clickable while it is open, which is what lets
 * someone read Guide or FAQ without first choosing studies. */

#cohort-modal-overlay.cohort-overlay {
    position: fixed;
    top: var(--op-menubar-h, 62px);
    left: var(--op-sidebar-w);
    /* Fills everything right of the sidebar. Previously capped at
     * `min(940px, calc(100vw - var(--op-sidebar-w)))`, which was meant to keep the
     * dashboard visible alongside it, but the drawer covers the content pane anyway, so
     * the cap bought nothing and just left a dead strip of dimmed dashboard on any window
     * wider than ~1220px. Anchoring both edges instead of setting a width means it tracks
     * window resizes with no JS. */
    right: 0;
    bottom: 0;
    background: none;            /* no full-screen dim: the app stays visible */
    backdrop-filter: none;
    align-items: stretch;
    justify-content: flex-start;
    z-index: 55;                 /* below .op-menubar (60) */
    animation: none;
}

#cohort-modal-overlay .cohort-modal,
#cohort-modal-overlay .cohort-modal.is-landing-gate {
    width: 100%;
    height: 100%;
    max-height: none;
    border-radius: 0;
    border-top: 0;
    border-bottom: 0;
    border-left: 0;
    /* No right border or right-cast shadow now that the drawer runs to the window edge:
     * the border showed as a stray 1px line hugging the right edge of the screen and the
     * shadow fell off-screen entirely. The drawer's only real edge is its left one, against
     * the sidebar, so the shadow casts that way instead. */
    border-right: 0;
    box-shadow: -14px 0 44px rgba(74, 66, 38, 0.14);
    animation: opDrawerOut 0.18s ease;
}

@keyframes opDrawerOut {
    from { transform: translateX(-14px); opacity: 0.4; }
    to   { transform: translateX(0);     opacity: 1; }
}

/* ---- cohort picker: collapsed groups + filter box ---- */
/* Collapsed is the default, so the picker opens as ~28 cancer-type rows rather than
 * 535 checkboxes. Only .op-enhanced groups collapse, so if shell.js fails to run the
 * list stays fully expanded and usable rather than becoming invisible. */
#cohort-groups .cohort-group.op-enhanced > .cohort-studies-list { display: none; }
#cohort-groups .cohort-group.op-enhanced.is-open > .cohort-studies-list { display: block; }

#cohort-groups .cohort-group.op-enhanced > .cohort-group-header {
    display: flex;
    align-items: center;
    gap: 10px;
}

/* Collapsed, a group is one row, so v1's generous card spacing left ~83px of air per
 * cancer type and only five fit on screen. Tightened so the whole list is scannable
 * without scrolling. Only applied to collapsed groups; an open one keeps its breathing
 * room around the studies inside it. */
#cohort-groups .cohort-group.op-enhanced {
    margin-bottom: 0 !important;
    border-bottom: 1px solid var(--border-color);
}
/* The real spacing between cancer types. #cohort-groups is a flex column with a 20px row
 * gap, so the groups sit 20px apart no matter what their own margins say, which is why
 * zeroing .cohort-group's margin-bottom had no visible effect. Now that groups are expanded
 * by default, 20px of dead space between one category's last study and the next category's
 * title read as a much bigger break than a category change deserves. */
#cohort-groups {
    gap: 4px !important;
}

#cohort-groups .cohort-group.op-enhanced > .cohort-group-header {
    margin: 0 !important;
    /* Tightened from 9px. Groups are expanded by default now, so these headers are
     * separators inside a long list rather than the list itself, and the air between one
     * category's last study and the next category's title was reading as a bigger break
     * than the category change warranted. */
    padding: 5px 4px !important;
    border-bottom: 0 !important;
    cursor: pointer;
}
#cohort-groups .cohort-group.op-enhanced.is-open > .cohort-studies-list {
    padding-bottom: 4px;
}

/* Expand/Collapse all. In the preset row but visually distinct and pushed right, because
 * it changes visibility rather than selection: grouping it with the presets would imply it
 * ticks boxes. */
.op-expand-all-btn {
    appearance: none;
    margin-left: auto;
    background: none;
    border: 0;
    border-bottom: 1px dotted var(--border-color);
    border-radius: 0;
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 0.72rem;
    padding: 0 0 1px;
    cursor: pointer;
    white-space: nowrap;
}
.op-expand-all-btn:hover {
    color: var(--primary-light);
    border-bottom-color: var(--primary-light);
}

/* The min-likelihood filter is hidden in the sidebar: it only applies to a scoring
 * mode that isn't surfaced here, so the control reads as a knob that does nothing.
 * Hidden rather than removed because app_v1.3.js reads #filter-likelihood's value on
 * every filter pass; the element must exist and keep its default of 0.00. */
#filter-likelihood-group { display: none !important; }

/* The line under the Studies button carried either "No filters applied" (restating
 * the button above it, which already reads "All 535 Studies") or a truncated run of
 * raw study codes, "ACC_TCGA_GDC, ACC, ACC, CHOL_ICGC_2017, CH…", which names a
 * near-arbitrary first few of a selection the user just made in the drawer. Neither
 * tells you anything the button doesn't. Hidden, not removed: updateCohortButton()
 * writes to it unconditionally. */
#cohort-summary { display: none !important; }

/* Source/licence table on the Data tab. The doc views cap children at 90ch for
 * readability, which is too narrow for a three-column table, so this one opts out. */
.op-doc .op-sources {
    max-width: 100%;
    width: 100%;
    border-collapse: collapse;
    margin: 18px 0 22px;
    font-size: 0.9rem;
}
.op-doc .op-sources th,
.op-doc .op-sources td {
    text-align: left;
    padding: 9px 12px 9px 0;
    border-bottom: 1px solid var(--border-color, var(--border));
    vertical-align: top;
}
.op-doc .op-sources th {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
}
.op-doc .op-sources td:first-child { white-space: nowrap; font-weight: 600; }
.op-doc .op-sources a { color: var(--primary-light); }

.op-group-chevron {
    appearance: none;
    background: none;
    border: 0;
    padding: 4px;
    margin: -4px 0 -4px -4px;
    cursor: pointer;
    flex: 0 0 auto;
    line-height: 0;
}
/* Chevron drawn in CSS, matching the FAQ accordion so the two read as one idiom. */
.op-group-chevron::before {
    content: '';
    display: block;
    width: 7px;
    height: 7px;
    border-right: 2px solid var(--text-secondary);
    border-bottom: 2px solid var(--text-secondary);
    transform: rotate(-45deg);
    transition: transform 0.15s ease;
}
.cohort-group.is-open > .cohort-group-header .op-group-chevron::before {
    transform: rotate(45deg);
}
.op-group-chevron:hover::before { border-color: var(--primary-light); }

/* Selected/total per group, so a collapsed group still says what is inside it. */
.op-group-count {
    margin-left: auto;
    flex: 0 0 auto;
    font-size: 0.72rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
    background: var(--bg-header);
    border: 1px solid var(--border-color);
    border-radius: 10px;
    padding: 2px 8px;
}
/* Partial selections are NOT colour-coded. Amber read as a warning about a state that is
 * completely ordinary: "16 of 27 studies ticked" is the normal result of choosing a cohort,
 * not a problem to flag, and the fraction already says so in plain text. Fully-deselected
 * groups stay muted, which is a legibility choice rather than a status one: 0/27 is
 * information you are usually scanning past. */
.op-group-count.is-none { color: var(--text-muted); }

/* The drawer's right-hand column duplicates the sidebar's PTM Type and Analysis Mode
 * toggles. Two live copies of the same control on screen at once invites the question
 * of which one is authoritative. Hidden, not removed, because app_v1.3.js builds
 * #ptm-toggle-gate's buttons at runtime and keeps both copies in sync; deleting the
 * node would mean editing v1's code. The sidebar copies remain. */
#cohort-modal-overlay .cohort-modal-col-right { display: none !important; }

.op-cohort-filter-wrap { padding: 0 4px 12px; }
#op-cohort-filter {
    /* Inset from the drawer edges rather than running flush into them. */
    width: calc(100% - 8px);
    margin: 0 4px;
    box-sizing: border-box;
    background: var(--bg-card);
    color: var(--text-primary);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 9px 12px;
    font-family: inherit;
    font-size: 0.85rem;
}
#op-cohort-filter:focus {
    outline: none;
    border-color: var(--primary);
}
#op-cohort-filter::placeholder { color: var(--text-muted); }

/* Preset shortcuts, so the common cohorts don't require hunting through 28 groups. */
.op-preset-row {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    padding: 10px 4px 0;
}
.op-preset-btn {
    appearance: none;
    background: var(--bg-header);
    border: 1px solid var(--border-color-strong);
    border-radius: 999px;
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 0.74rem;
    font-weight: 500;
    padding: 5px 12px;
    cursor: pointer;
    transition: color 0.12s, border-color 0.12s, background-color 0.12s;
}
.op-preset-btn:hover {
    color: var(--text-primary);
    border-color: var(--primary);
}

/* Lit when the current selection exactly matches this preset. See refreshPresetState.
 * Deliberately a faint tint rather than a solid fill: these are a shortcut for setting the
 * checkboxes, not a mode you are in, and a fully saturated pill would read as the latter
 * and compete with the Apply button for attention. */
.op-preset-btn.is-active {
    background: var(--bg-sidebar);         /* one plate deeper than the resting pill */
    border-color: var(--primary);
    color: var(--text-primary);
}

/* Study rows: two lines instead of three squeezed columns.
 *
 * v1 lays each row out as a flex row of [checkbox | id | full name | badge | count].
 * .cohort-study-code sets min-width:80px but keeps the default flex-shrink:1, so any
 * id longer than 80px was shrunk below its text width and, with no overflow rule,
 * spilled over the name beside it: "ccle_broad_201Encyclopedia". The names also had
 * so little room left that most wrapped to three or four lines.
 *
 * Restyled as a grid with the id above the name, both in one full-width column. Every
 * id now has room, names mostly fit on one line, and the left edges line up regardless
 * of id length. Selector-only: v1's markup, ids and listeners are untouched, and this
 * is scoped under #cohort-modal-overlay so the legacy dashboard keeps its own layout. */
#cohort-modal-overlay .cohort-study-item {
    display: grid;
    /* col 3 is the model-system badge, col 4 the patient count. Giving the badge its
     * own auto column means it collapses to zero width on the rows that lack one,
     * instead of needing a "does this row have a badge" rule CSS cannot express. */
    grid-template-columns: 14px minmax(0, 1fr) auto auto;
    column-gap: 10px;
    align-items: center;
    /* Tightened from 7px. Each row is a single line of text now that the study id is
     * hidden and the name spans the column, so the padding that separated a two-line
     * row from its neighbour is just dead space, and the list is 535 rows long, where
     * 8px per row is ~4300px of extra scrolling. */
    padding: 2px 0 !important;
    line-height: 1.35;
}
#cohort-modal-overlay .cohort-study-item input[type="checkbox"] {
    grid-column: 1;
    grid-row: 1 / span 2;
    margin-top: 3px;
}
/* The raw study id ("amc_brca_2016") is dropped and the human name promoted to be THE
 * label. The id is an internal cBioPortal key: nobody picking a cohort recognises studies
 * by it, and having it lead each row pushed the name that actually identifies the study
 * ("Ampullary Carcinoma (Baylor College of Medicine, Cell Reports 2016)") into grey
 * secondary text. Hidden rather than removed because v1 writes the id into this node and
 * the row's data-study-id, which the filter and preset code reads. */
#cohort-modal-overlay .cohort-study-code { display: none; }

#cohort-modal-overlay .cohort-study-fullname {
    grid-column: 2;
    grid-row: 1 / span 2;   /* now the only text in the column, so take both rows */
    align-self: center;
    font-size: 0.85rem;
    line-height: 1.4;
    /* Was muted secondary text; it is the primary label now. */
    color: var(--text);
}
#cohort-modal-overlay .cohort-study-model-tag {
    grid-column: 3;
    grid-row: 1 / span 2;
    align-self: center;
}
#cohort-modal-overlay .cohort-study-patient-count {
    grid-column: 4;
    grid-row: 1 / span 2;
    align-self: center;
    margin-left: 0 !important;
}

/* ---------------------------------------------- document views (prose) ----- */

/* The doc view is now the scroll container, since <body> no longer scrolls. It spans
 * the full width so its scrollbar sits at the window edge rather than partway across
 * the page, and the 90ch reading measure is applied to its children instead.
 *
 * Capping the children rather than the container is also what keeps the FAQ from
 * shifting on expand: this is a flex item of <body>, and an auto cross-axis margin
 * suppresses flex stretch, so a max-width here left the column sized by fit-content.
 * It measured 685px collapsed and jumped to the 90ch cap the moment an answer opened,
 * moving every question sideways. Full-width container plus capped children removes
 * the fit-content path entirely. */
.op-doc {
    flex: 1 1 auto;
    width: 100%;
    min-width: 0;
    min-height: 0;
    overflow-y: auto;
    scrollbar-gutter: stable;
    padding: 38px 32px 90px;
    font-size: 1rem;
    line-height: 1.7;
    color: var(--text-primary);
}
/* One centred reading column, done by the container rather than by each child.
 *
 * Two earlier attempts both failed, and the reasons are worth keeping:
 *
 *  1. Auto side-margins on .op-doc itself. It is a flex item, and auto cross-axis margins
 *     suppress the default `stretch`, so it sized to fit-content, which is what made the
 *     FAQ change width whenever a question expanded.
 *
 *  2. `max-width: 90ch; margin-inline: auto` on each child. Two separate bugs. `ch` scales
 *     with the element's OWN font-size, so the cap came out 944px on a <p> but 1236px on
 *     an <h2>; and more fundamentally, `.op-doc h2 { margin: 38px 0 14px }` is specificity
 *     (0,1,1) against the child rule's (0,1,0), and a `margin` SHORTHAND resets left/right
 *     to 0, so headings, lists and tables lost their auto-centring and sat flush at the
 *     padding edge while paragraphs stayed centred ~92px in.
 *
 * A single-track grid fixes both classes of problem at once: the track is centred by the
 * container, so no child margin can knock the column out of alignment, and every child
 * inherits the same track width regardless of its font-size. Child margins now only shift
 * content WITHIN the track, which is what the list indents actually want. */
.op-doc {
    display: grid;
    grid-template-columns: minmax(0, 59rem);
    justify-content: center;
    align-content: start;
}
/* Long tables and <pre> would otherwise widen the track past its cap. */
.op-doc > * { min-width: 0; }

/* The prose sections reuse v1's dashboard card classes, which are wrong for
 * reading: .chart-card adds 24px padding, a 16px radius and backdrop-filter
 * (which creates a containing block and makes them scroll strangely), and
 * .metric-card is display:flex;align-items:center, which lays paragraphs out
 * sideways. Strip all of it here so documents read as continuous text. Done in
 * CSS rather than by editing the fragments so the writing stays portable. */
.op-doc .chart-card,
.op-doc .card-header,
.op-doc .card-body {
    background: none;
    border: 0;
    border-radius: 0;
    padding: 0;
    margin: 0;
    box-shadow: none;
    backdrop-filter: none;
    display: block;
    overflow: visible;
    max-height: none;
}
.op-doc .chart-card { margin-bottom: 26px; }
.op-doc .card-title { font-size: 1.15rem; font-weight: 600; margin-bottom: 10px; }

/* Reclaimed as a quiet callout instead of a dashboard tile. */
.op-doc .metric-card {
    display: block;
    background: var(--bg-header);
    border: 0;
    border-left: 2px solid var(--border-color-active);
    border-radius: 0;
    padding: 14px 18px;
    margin: 18px 0;
    box-shadow: none;   /* a callout is flush with the page, not a card lifted off it */
}
.op-doc .metric-card p { margin: 0 0 6px; }
.op-doc .metric-card p:last-child { margin-bottom: 0; }

/* Tables and code get the full container width even though prose is capped. */
.op-doc table, .op-doc pre { width: 100%; }
.op-doc h2 {
    font-size: 1.3rem;
    font-weight: 600;
    margin: 38px 0 14px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--border-color);
}
.op-doc h2:first-child { margin-top: 0; }
.op-doc h3 {
    font-size: 1rem;
    font-weight: 600;
    margin: 26px 0 8px;
}
.op-doc p, .op-doc li { color: var(--text-secondary); }
.op-doc ul, .op-doc ol { margin: 10px 0 16px 22px; }
.op-doc li { margin-bottom: 7px; }
.op-doc dt { font-weight: 600; margin-top: 18px; color: var(--text-primary); }
.op-doc dd { margin: 6px 0 0 0; color: var(--text-secondary); }
.op-doc code {
    background: var(--bg-header);
    border: 1px solid var(--border-color);
    border-radius: 4px;
    padding: 1px 5px;
    font-size: 0.85em;
}
.op-doc pre {
    background: var(--bg-header);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 14px;
    overflow-x: auto;
    font-size: 0.82rem;
}
.op-doc pre code { background: none; border: 0; padding: 0; }
.op-doc a { color: var(--primary-light); }
.op-doc table { width: 100%; border-collapse: collapse; margin: 14px 0 20px; font-size: 0.86rem; }
.op-doc th, .op-doc td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border-color); }
.op-doc th { color: var(--text-secondary); font-weight: 600; text-transform: uppercase; font-size: 0.72rem; letter-spacing: 0.05em; }
.op-doc blockquote {
    margin: 16px 0;
    padding: 2px 0 2px 16px;
    border-left: 2px solid var(--border-color-active);
    color: var(--text-secondary);
}
.op-doc-loading { color: var(--text-secondary); font-style: italic; }

/* ------------------------------------------------- feedback form ---------- */
/* THE FIRST FORM CONTROLS THIS PROJECT HAS EVER PUT INSIDE A DOC PAGE, which is why all of
 * this is new rather than a reuse of something above.
 *
 * EVERY selector is prefixed `.op-doc `, without exception. The doc views and the dashboard
 * share one document and one cascade: the dashboard is full of <input>, <select> and <button>
 * that already carry their own long-settled rules in styles_v1.3.css, and shell.css loads
 * second, so an unprefixed .op-fb-input would be harmless only until the day someone reuses
 * a name. The prefix makes leakage impossible rather than unlikely.
 *
 * It also stays out of styles_v1.3.css deliberately: the form exists only inside a doc
 * fragment injected into #feedback-view, the Tauri desktop build stages no menubar and no doc
 * views at all, and the hotspot modal can never match .op-doc. This is the one direction the
 * placement law allows.
 *
 * SURFACES ARE WARM, not grey. Fields sit on --bg-card with a --border-color hairline, the
 * fallback panel takes --straw-tint over --straw-line because it is a transient state the
 * reader has to act on, and the pressed rating takes --primary-tint. Grey on grey has been
 * pushed back on before and there is no reason to reach for it here. */
.op-doc .op-fb {
    display: flex;
    flex-direction: column;
    gap: 20px;
    margin: 18px 0 26px;
    max-width: 46rem;
}

.op-doc .op-fb-lead {
    margin-bottom: 4px;
    max-width: 46rem;
}

/* THE HONEYPOT'S HIDING PLACE, and the method is the whole point of it.
 *
 * NOT display:none, NOT visibility:hidden and NOT a zero opacity. Those are the three things
 * a form-filling bot checks before deciding a field is bait, so any of them turns the trap
 * into a signal. Positioning it off the left edge of the page leaves a field that computes as
 * ordinary in every way a script can cheaply test, while no sighted reader is ever within a
 * screen width of it.
 *
 * The 1px box with overflow hidden is what stops the off-screen content from widening the
 * page: an absolutely positioned box does not contribute to layout, but a stray inline
 * scrollbox can, and a document that scrolls 9,999px sideways would be a very visible way to
 * hide something.
 *
 * Keyboard and screen-reader users are excluded by the markup, not by this rule:
 * tabindex="-1" keeps it out of the tab order and aria-hidden keeps it out of the
 * accessibility tree. Off-screen positioning ALONE would leave it reachable by both, which is
 * the classic way a honeypot ends up trapping the people least able to see what happened. */
.op-doc .op-fb-website {
    position: absolute;
    left: -9999px;
    top: auto;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.op-doc .op-fb-row {
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}

.op-doc .op-fb-label {
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--text-primary);
}

/* "optional" rides inside the label rather than sitting beside it, so the fact that a field
 * can be skipped is read at the same moment as the field's name. Muted and smaller so it
 * cannot be mistaken for part of the field's name, and NOT uppercased: it is a word in a
 * sentence, not a section marker. */
.op-doc .op-fb-opt {
    margin-left: 4px;
    font-size: 0.74rem;
    font-weight: 400;
    color: var(--text-muted);
}

/* font-family: inherit on both, because a UA leaves form controls on the system font and a
 * message box set in Arial inside a page set in Outfit reads as a third-party widget bolted
 * on. This form is part of the page. */
.op-doc .op-fb-text,
.op-doc .op-fb-input {
    width: 100%;
    padding: 10px 12px;
    border: 1px solid var(--border-color);
    border-radius: 8px;
    background: var(--bg-card);
    color: var(--text-primary);
    font-family: inherit;
    font-size: 0.9rem;
    line-height: 1.55;
}
.op-doc .op-fb-text::placeholder {
    color: var(--text-muted);
}
.op-doc .op-fb-text:focus-visible,
.op-doc .op-fb-input:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 1px;
    border-color: var(--primary);
}
/* Vertical only. Horizontal resize on a field inside a flex column lets a reader drag the
 * control wider than the page and start the whole document scrolling sideways. */
.op-doc .op-fb-text {
    resize: vertical;
    min-height: 8rem;
}

/* The three identity fields are the least important thing on the page and are last in the
 * markup for that reason. auto-fit rather than a fixed three columns so they become one
 * column on a narrow window without a media query, and 15rem because an email address is the
 * widest thing any of them holds. */
.op-doc .op-fb-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 14px;
}

/* The reason checkboxes render as pill "bubbles". The native control stays visible for
 * clarity and keyboard use, and the pill around it fills in when checked, matching the same
 * .is-on look the rating buttons use. It is a checkbox group, not a select, precisely so any
 * combination of the three can be on at once. Like .op-fb-rate this is a flex fieldset whose
 * <legend> the UA renders above the content box rather than as a flex item. */
.op-doc .op-fb-kinds {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    border: 0;
    min-width: 0;
}
.op-doc .op-fb-kinds .op-fb-label {
    margin-bottom: 6px;
}
.op-doc .op-fb-kind {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 7px 14px;
    border: 1px solid var(--border-color);
    border-radius: 999px;
    background: var(--bg-card);
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 0.86rem;
    font-weight: 600;
    cursor: pointer;
    user-select: none;
}
.op-doc .op-fb-kind input {
    accent-color: var(--primary);
    cursor: pointer;
}
/* :has() is what fills the pill when its own checkbox is checked. Every browser this SPA
 * targets supports it; without it the pill simply stays outlined and the native tick still
 * shows the state, so the fallback is legible rather than broken. */
.op-doc .op-fb-kind:has(input:checked) {
    border-color: var(--primary);
    background: var(--primary-tint);
    color: var(--text-primary);
}
.op-doc .op-fb-kind:focus-within {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* A <fieldset> keeps its UA border even after the universal margin/padding reset, so it has
 * to be turned off by hand. display: flex puts the ten buttons in a row; the <legend> is
 * placed above the fieldset's anonymous content box by the UA and is therefore NOT a flex
 * item, which is exactly the layout wanted and the reason the legend is not being fought. */
.op-doc .op-fb-rate {
    --fb-rate-btn: 2.7rem;
    --fb-rate-gap: 6px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--fb-rate-gap);
    border: 0;
    min-width: 0;
}
.op-doc .op-fb-rate .op-fb-label {
    margin-bottom: 6px;
}

.op-doc .op-fb-rate-btn {
    width: var(--fb-rate-btn);
    padding: 7px 0;
    border: 1px solid var(--border-color);
    border-radius: 8px;
    background: var(--bg-card);
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 0.9rem;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    text-align: center;
    cursor: pointer;
}
/* The same .is-on the 3D annotation picker uses, styled the same way: one idea, one look.
 * aria-pressed carries the state for assistive technology and shell.js keeps the two in step;
 * the class is what the eye reads. */
.op-doc .op-fb-rate-btn.is-on {
    border-color: var(--primary);
    background: var(--primary-tint);
    color: var(--text-primary);
}
.op-doc .op-fb-rate-btn:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* "Poor" and "Excellent" name the two ENDS of the scale, so they have to sit under the ends
 * of the scale or they say nothing. flex-basis 100% drops them onto their own line, and the
 * max-width is the button row's exact width computed from the two custom properties above --
 * ten buttons and the nine gaps between them -- so the labels stay pinned under buttons 1 and
 * 10 if either value is ever changed. */
.op-doc .op-fb-rate-ends {
    flex: 1 0 100%;
    display: flex;
    justify-content: space-between;
    max-width: calc(var(--fb-rate-btn) * 10 + var(--fb-rate-gap) * 9);
    font-size: 0.72rem;
    color: var(--text-muted);
}

.op-doc .op-fb-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 14px;
}

/* The one primary control on the page, and the one place here that spends olive. That is what
 * the chrome hue is reserved for: --primary appears nowhere in the data palette, so filling a
 * button with it can never be misread as a statement about loss, gain or a PTM type. */
.op-doc .op-fb-send {
    padding: 10px 20px;
    border: 1px solid var(--primary);
    border-radius: 8px;
    background: var(--primary);
    color: #fff;
    font-family: inherit;
    font-size: 0.88rem;
    font-weight: 600;
    cursor: pointer;
}
.op-doc .op-fb-send:hover {
    background: var(--primary-light);
    border-color: var(--primary-light);
}
.op-doc .op-fb-send:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* aria-live, so this line is announced the moment it is written. It carries both the "please
 * write a message first" refusal and the "sending is not switched on yet" explanation, which
 * is why it is full-strength secondary ink rather than muted: it is the answer to the click,
 * not a footnote. */
.op-doc .op-fb-status {
    font-size: 0.84rem;
    line-height: 1.5;
    color: var(--text-secondary);
}

/* The no-endpoint path. Straw rather than a grey well, because this panel is the site telling
 * the reader that their words have gone nowhere yet and that they now have to carry them out
 * by hand: it is a transient state that needs acting on, which is the job straw does
 * everywhere else in this project.
 *
 * The [hidden] rule is not optional. The panel ships with the hidden attribute and shell.js
 * removes it; a bare display: flex here would outrank the UA's [hidden] rule and the fallback
 * would be on screen from the moment the page loaded, telling every reader that sending had
 * failed before they had typed anything. */
.op-doc .op-fb-fallback {
    display: flex;
    flex-direction: column;
    gap: 10px;
    align-items: flex-start;
    margin: 0 0 26px;
    padding: 16px 18px;
    max-width: 46rem;
    border: 1px solid var(--straw-line);
    border-radius: 10px;
    background: var(--straw-tint);
}
.op-doc .op-fb-fallback[hidden] { display: none; }

/* Monospace, and readonly rather than disabled. The reader is expected to select this text
 * and paste it into a mail client, which a disabled control does not allow, and a fixed-pitch
 * face makes the Rating / Name / Page label column line up so the block is legible as a
 * report rather than as a wall. */
.op-doc .op-fb-copy {
    width: 100%;
    padding: 10px 12px;
    border: 1px solid var(--straw-line);
    border-radius: 8px;
    background: var(--bg-card);
    color: var(--text-primary);
    font-family: var(--font-mono);
    font-size: 0.82rem;
    line-height: 1.55;
    resize: vertical;
}
.op-doc .op-fb-copy:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 1px;
}

/* Secondary to .op-fb-send on purpose: outlined, not filled. Two filled olive buttons on one
 * page would make the reader choose between them, and by the time this panel is on screen the
 * only useful action left is copying. */
.op-doc .op-fb-copybtn {
    padding: 8px 16px;
    border: 1px solid var(--primary);
    border-radius: 8px;
    background: var(--bg-card);
    color: var(--primary-light);
    font-family: inherit;
    font-size: 0.84rem;
    font-weight: 600;
    cursor: pointer;
}
.op-doc .op-fb-copybtn:hover {
    background: var(--primary-tint);
}
.op-doc .op-fb-copybtn:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* What is collected and where it goes. Small, but not hidden and not italicised into a
 * disclaimer: it names the page URL and the browser identification string that the report
 * carries, and it is the only place the reader is told that nothing leaves the browser. */
.op-doc .op-fb-privacy {
    max-width: 46rem;
    font-size: 0.8rem;
    line-height: 1.6;
    color: var(--text-muted);
}

/* ------------------------------------------------- FAQ accordion ---------- */
/* Native <details>/<summary>: no JS, keyboard accessible, and the answers stay
 * in the DOM while collapsed so they remain indexable. */

.op-faq-cat { margin-bottom: 34px; }
.op-faq-cat:first-child h2 { margin-top: 0; }
.op-faq-cat h2 { margin-bottom: 2px; }

.op-faq-item {
    border-bottom: 1px solid var(--border-color);
}
.op-faq-item summary {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 14px 4px;
    cursor: pointer;
    font-weight: 500;
    color: var(--text-primary);
    list-style: none;           /* Firefox */
    transition: color 0.12s;
}
.op-faq-item summary::-webkit-details-marker { display: none; }
.op-faq-item summary::marker { content: ''; }
.op-faq-item summary:hover { color: var(--primary-light); }

/* Chevron drawn in CSS so there's no icon-font dependency. Rotates on open. */
.op-faq-item summary::before {
    content: '';
    flex: 0 0 auto;
    width: 7px;
    height: 7px;
    margin-top: 7px;
    border-right: 2px solid var(--text-secondary);
    border-bottom: 2px solid var(--text-secondary);
    transform: rotate(-45deg);
    transition: transform 0.15s ease;
}
.op-faq-item[open] > summary::before {
    transform: rotate(45deg);
}
.op-faq-item[open] > summary { color: var(--primary-light); }

.op-faq-item summary code {
    font-weight: 400;
}

.op-faq-a {
    padding: 0 4px 18px 31px;   /* 31px aligns the answer under the question text */
}
.op-faq-a > *:first-child { margin-top: 0; }
.op-faq-a p { margin-bottom: 12px; }
.op-faq-a p:last-child { margin-bottom: 0; }
.op-faq-a ul { margin: 10px 0 4px 0; padding-left: 20px; }

/* Inline stat strip used by the evidence-tier answer. */
.op-faq-stats {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin: 14px 0 16px;
}
.op-faq-stats span {
    background: var(--bg-header);
    border: 1px solid var(--border-color);
    border-radius: 6px;
    padding: 8px 14px;
    font-size: 0.82rem;
    color: var(--text-secondary);
}
.op-faq-stats strong {
    color: var(--text-primary);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/* Guide / FAQ / Give are documents, not data views. They sit outside
 * .dashboard-wrapper so the cohort landing gate can't hide them: prose has to be
 * readable before anyone has picked a study. shell.js swaps the wrapper out
 * wholesale; this class is the CSS-side backstop. */
body.op-doc-mode .dashboard-wrapper { display: none !important; }
/* Same backstop for every pane that replaces the wrapper rather than living inside it,
 * which is the docs plus the gene view. Kept separate from op-doc-mode because that
 * class also carries the prose typography, which the gene view must not get. */
body.op-panel-mode .dashboard-wrapper { display: none !important; }

/* ------------------------------------------------- gene lookup view ------- */
/* Deliberately inherits v1's card language (.chart-card, .card-header, .metric-card and
 * the existing custom properties) rather than introducing anything new. The only local
 * additions are the search field, the six-row PTM table and the two omics bar lists,
 * none of which v1 has an equivalent for. */

#genes-view {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 0;
    overflow-y: auto;
    scrollbar-gutter: stable;
}
.op-gv {
    max-width: 1180px;
    margin: 0 auto;
    padding: 26px 28px 70px;
    display: flex;
    flex-direction: column;
    gap: 20px;
}

/* ---- search ---- */
/* Sticky so the box a lookup view is built around never scrolls off. The negative top
 * padding plus matching offset keeps the autocomplete list from showing content sliding
 * underneath it through the gap. */
.op-gv-search {
    position: sticky;
    top: -26px;
    z-index: 5;
    padding: 26px 0 12px;
    margin-top: -26px;
    background: var(--bg-main);
}
/* Hidden while a gene panel is up: the search has done its job and the answer wants the
 * height. Explicit because the element carries a position, and a UA [hidden] rule is the
 * kind of thing an author display declaration silently beats later. */
.op-gv-search[hidden] { display: none; }
.op-gv-field {
    display: flex;
    align-items: center;
    gap: 14px;
}
/* A gene symbol is an identifier, so the field you type it into is mono. That also makes the
 * field legible as an input at a glance without a label: nothing else on the page is mono and
 * left-aligned in a 1030px box. The border is the strong hairline, because the faint one on a
 * 60px-tall white box on warm paper left the field with no edge at all. */
#op-gv-input {
    flex: 1 1 auto;
    min-width: 0;
    background: var(--bg-card);
    border: 1.5px solid var(--border-color-strong);
    border-radius: 10px;
    color: var(--text-primary);
    font-family: var(--font-mono);
    font-size: 1.05rem;
    font-weight: 500;
    letter-spacing: 0;
    padding: 14px 18px;
    outline: none;
    transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}
#op-gv-input::placeholder {
    color: var(--text-muted);
    font-weight: 400;
    font-family: var(--font-family);
}
#op-gv-input:hover { border-color: var(--straw); }
#op-gv-input:focus {
    border-color: var(--primary);
    box-shadow: 0 0 0 3px var(--primary-glow);
}
.op-gv-ac {
    position: absolute;
    left: 0;
    right: 0;
    margin: 2px 0 0;
    padding: 6px;
    list-style: none;
    background: var(--bg-card);
    border: 1px solid var(--border-color-active);
    border-radius: 10px;
    box-shadow: 0 18px 40px rgba(74, 66, 38, 0.18);
    /* Sized to hold GV_AC_MAX rows outright. A scrollbar inside a 10-row hint list is a
     * control the visitor has to operate to see results they were already offered. */
    max-height: 380px;
    overflow-y: auto;
}
.op-gv-ac li {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
    padding: 8px 12px;
    border-radius: 7px;
    cursor: pointer;
    font-size: 0.92rem;
}
.op-gv-ac li b { color: var(--text-primary); font-weight: 600; }
.op-gv-ac li em {
    font-style: normal;
    font-size: 0.78rem;
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}
/* Pointer hover and the keyboard cursor are the same kind of thing here -- a candidate you
 * have not committed to -- so both are straw, and neither is olive. */
.op-gv-ac li:hover,
.op-gv-ac li.is-on { background: var(--straw-tint); }
.op-gv-ac li.is-on { box-shadow: inset 3px 0 0 var(--straw); }
.op-gv-ac-cold { color: var(--text-muted); }

.op-gv-body { display: flex; flex-direction: column; gap: 20px; }
.op-gv-loading { color: var(--text-secondary); font-style: italic; }

/* ---- empty state ---- */
.op-gv-chips { display: flex; flex-wrap: wrap; gap: 8px; }

/* The gene suggestions are a GRID, not the wrapping flex row the related-genes list uses.
 * A flex row packs chips by their own width, so a set of gene symbols -- which run from
 * three characters to eight -- ends as a full first row and a short ragged second one. The
 * column count steps with width and GV_TOP_CHIPS is 12, which divides by all four counts,
 * so the block is always complete rows. */
.op-gv-start .op-gv-chips {
    display: grid;
    grid-template-columns: repeat(6, minmax(0, 1fr));
    gap: 8px;
}
/* Symbol hard left, count hard right, so the numbers line up down each column instead of
 * floating wherever the symbol's length leaves them. */
.op-gv-start .op-gv-chip { justify-content: space-between; }
@media (max-width: 1080px) { .op-gv-start .op-gv-chips { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 760px)  { .op-gv-start .op-gv-chips { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 480px)  { .op-gv-start .op-gv-chips { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* The symbol is mono to match the field above it, since clicking a chip fills that field.
 * The count stays in the UI face: it is a quantity, not an identifier. */
.op-gv-chip {
    display: inline-flex;
    align-items: baseline;
    gap: 8px;
    background: var(--bg-card);
    border: 1px solid var(--border-color-strong);
    border-radius: 8px;
    color: var(--text-primary);
    font-family: var(--font-mono);
    font-size: 0.84rem;
    font-weight: 500;
    padding: 8px 12px;
    cursor: pointer;
    transition: border-color var(--transition-fast), background var(--transition-fast),
                box-shadow var(--transition-fast);
}
.op-gv-chip:hover {
    border-color: var(--straw);
    background: var(--straw-tint);
}
.op-gv-chip:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 1px;
}
.op-gv-chip span {
    font-weight: 400;
    font-size: 0.76rem;
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}
/* ---- gene header ---- */
.op-gv-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 14px;
}

/* Back to the gene suggestions. Aligned to `center` against the row's `baseline`, because
 * an icon has no text baseline and would otherwise sit low against the 2rem symbol. */
.op-gv-back {
    appearance: none;
    align-self: center;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex: 0 0 auto;
    background: none;
    border: 1px solid var(--border-color);
    border-radius: 8px;
    color: var(--text-secondary);
    cursor: pointer;
    transition: color 0.12s, border-color 0.12s;
}
.op-gv-back:hover { color: var(--text-primary); border-color: var(--primary); }
.op-gv-back i, .op-gv-back svg { width: 17px; height: 17px; }
.op-gv-head h2 {
    font-size: 2rem;
    font-weight: 700;
    letter-spacing: -0.015em;
    margin: 0;
}
/* Labelled, not a bare word: a reader asked what the "Other" tag beside the protein name
 * was for, which is the correct question to ask of a value with no name on it. The key takes
 * the same treatment as the protein card's FAMILY label, so the two read as one pattern. */
.op-gv-loc { font-size: 0.85rem; color: var(--text-primary); }
.op-gv-loc-k {
    margin-right: 6px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-size: 0.68rem;
    color: var(--text-secondary);
}

/* The drug-target chip sits on the row's baseline with the compartment chip. .badge and
 * .badge-drug carry the rest, in styles_v1.3.css, so the gene page and the hotspot modal
 * cannot end up with two different-looking answers to one question. */
#op-gv-drug { align-self: center; }

/* ---- .op-out: the ONE link-out treatment ----
 *
 * NOT HERE ANY MORE. The whole block moved to styles_v1.3.css, and the move is the fix for a
 * parity bug rather than tidying. .op-out is written by the SHARED seams in app_v1.3.js:
 * protHeaderHtml puts it on the UniProt accession in the description header, and
 * renderAlphaFold puts it on the AlphaFold link in the structure provenance line. Both of
 * those paint the hotspot modal as well as the Genes view, and the Tauri desktop build stages
 * index.html, app_v1.3.js, styles_v1.3.css, pagination.css and sequon_presets.json only. So
 * every reader of the desktop app, and of dashboard/index.html, got browser-default blue
 * underlined links where the web shell got the olive pill: no tint, no radius, no glyph.
 *
 * Nothing moved with it. Every custom property it spends (--primary-tint, --straw-tint,
 * --straw-line, --primary-light, --primary, --font-family) was already declared in
 * styles_v1.3.css.
 *
 * Do not move any of it back, and do not add a copy here. shell.css loads AFTER
 * styles_v1.3.css, so a duplicate would silently win on the live site and the two front ends
 * would drift again without either stylesheet looking wrong on its own. */

/* The one statement this view makes in prose: a gene with annotated sites and zero
 * events reads as a broken page unless it is said outright. */
.op-gv-note {
    background: var(--bg-header);
    border-left: 2px solid var(--amber);
    border-radius: 0 8px 8px 0;
    padding: 12px 16px;
    font-size: 0.88rem;
    color: var(--text-secondary);
}
.op-gv-note strong { color: var(--text-primary); }

/* ---- PTM table ---- */
.op-gv-ptm {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.87rem;
}
.op-gv-ptm th,
.op-gv-ptm td {
    padding: 9px 12px;
    border-bottom: 1px solid var(--border-color);
    text-align: left;
    vertical-align: middle;
}
.op-gv-ptm thead th {
    color: var(--text-secondary);
    font-size: 0.68rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    white-space: nowrap;
}
.op-gv-ptm tbody th {
    font-weight: 600;
    color: var(--text-primary);
    white-space: nowrap;
}
.op-gv-ptm tbody tr:last-child th,
.op-gv-ptm tbody tr:last-child td { border-bottom: 0; }
.op-gv-ptm .op-gv-n { text-align: right; font-variant-numeric: tabular-nums; }
.op-gv-ptm .op-gv-study {
    color: var(--text-secondary);
    font-size: 0.78rem;
    font-family: var(--font-family);
}
.op-gv-na { color: var(--text-muted); cursor: help; border-bottom: 1px dotted var(--text-muted); }

.op-gv-barcell { width: 30%; min-width: 130px; }
.op-gv-barcell i {
    display: inline-block;
    vertical-align: middle;
    height: 6px;
    border-radius: 3px;
    background: var(--primary);
    max-width: calc(100% - 54px);
}
.op-gv-barcell span {
    display: inline-block;
    margin-left: 8px;
    font-size: 0.76rem;
    color: var(--text-secondary);
    font-variant-numeric: tabular-nums;
}

/* Three states, and telling them apart is the point of the table.
 *   default   = sites exist and are mutated
 *   is-quiet  = sites exist, nothing mutated them: the count still reads, dimmed
 *   is-absent = no site of this type to begin with: nothing happened and nothing could
 *
 * Each carries a left-edge marker as well as a dimming level, because dimming alone is
 * only legible next to a brighter row, and a gene where every type is quiet would have
 * nothing to compare against. The marker colours are what the legend swatches show. */
.op-gv-ptm tbody th {
    border-left: 2px solid var(--primary);
    padding-left: 12px;
}
.op-gv-ptm tr.is-quiet th {
    border-left-color: var(--text-secondary);
    color: var(--text-secondary);
    font-weight: 500;
}
.op-gv-ptm tr.is-quiet .op-gv-n { color: var(--text-secondary); }
.op-gv-ptm tr.is-absent th {
    border-left-color: var(--border-color);
    color: var(--text-muted);
    font-weight: 500;
}
.op-gv-ptm tr.is-absent .op-gv-n { color: var(--text-muted); font-weight: 500; }
.op-gv-say { color: var(--text-muted); font-size: 0.78rem; font-style: italic; }
.op-gv-ptm tr.is-quiet .op-gv-say { color: var(--text-secondary); }

.op-gv-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 18px;
    margin: 14px 0 0;
    font-size: 0.74rem;
    color: var(--text-muted);
}
.op-gv-key { display: inline-flex; align-items: center; gap: 6px; }
/* Swatches are the row markers, same shape and same colours, so the legend describes
 * something actually visible in the table rather than an abstract greyscale. */
.op-gv-key-hit::before,
.op-gv-key-quiet::before,
.op-gv-key-absent::before {
    content: '';
    width: 2px;
    height: 14px;
    border-radius: 1px;
}
.op-gv-key-hit::before { background: var(--primary); }
.op-gv-key-quiet::before { background: var(--text-secondary); }
.op-gv-key-absent::before { background: var(--border-color-strong); }

/* ---- protein description (UniProt) ----
 *
 * NOT HERE ANY MORE. Every .op-prot* rule moved to styles_v1.3.css, and the move is the fix
 * for a parity bug rather than tidying: renderProteinDescription paints the hotspot modal as
 * well as the Genes view, and the Tauri desktop build stages index.html, app_v1.3.js,
 * styles_v1.3.css, pagination.css and sequon_presets.json only. Every rule that lived here
 * was a rule the desktop app's modal never got, so the same protein description rendered as
 * a styled block on the web and as unstyled browser-default prose in the app. Do not move
 * any of it back. */

/* ---- colour key, 3D structure box, sequence wrapper ----
 *
 * NOT HERE ANY MORE either, and for the same staging reason as .op-prot* above. The hotspot
 * modal draws the same three things now (.op-legend for what the colours mean, .op-struct for
 * the 3Dmol box, and the sequence panel directly under it), so every one of those rules is in
 * styles_v1.3.css where the desktop build can see it.
 *
 * The class names lost their op-gv- prefix in the move because the prefix had become a lie:
 * .op-gv-struct was the class on the hotspot modal's viewer as well. .op-gv-ptmkey /
 * .op-gv-ptmk are .op-legend / .op-legend-k, and .op-gv-seqwrap is gone entirely, since with
 * both cards built the same way the .chart-card column gap already supplies that space. */

/* ---- mutated PTM sites: the lollipop ----
 * Full width of the content column, which it gets for free by being a .chart-card in the
 * .op-gv-body flex column, plus a fixed height so Chart.js sizes to the box rather than
 * deriving a width from an aspect ratio.
 *
 * min-width: 0 is the load-bearing line. A canvas in a flex or grid child defaults to
 * min-width:auto, so it refuses to shrink below its intrinsic width and pushes the whole
 * page wider instead: that is precisely the horizontal scrollbar this card must not create
 * on a narrow viewport. The height steps down on small screens because a 320px plot in a
 * 380px-wide window is nearly square and wastes the vertical run the residue axis needs. */
.op-gv-lolli {
    position: relative;
    /* A fixed-height flex item in .chart-card's column, which already supplies the 20px gap
     * under the header, so this adds no margin of its own. flex: 0 0 auto keeps the height a
     * height rather than a starting point a sibling could shrink. */
    flex: 0 0 auto;
    min-width: 0;
    height: 320px;
}
.op-gv-lolli canvas { display: block; }
@media (max-width: 720px) {
    .op-gv-lolli { height: 260px; }
}

/* ---- functional annotation track ----
 * Drawn to the same 1..length scale as the sequence above it, so a PTM site and the domain
 * or membrane leaflet it sits in line up vertically. Percent widths rather than pixels: the
 * track has to stay honest at every viewport width. */
/* position: relative because the hover tooltip is absolutely placed inside this box and is
 * clamped to its width, which is what keeps a tooltip on the last residue of a protein from
 * hanging off the page and adding a horizontal scrollbar. */
.op-gv-feat { margin-top: 22px; position: relative; }
.op-gv-feat:empty { display: none; }

/* The heading itself is .card-header + h4 + .card-subtitle out of styles_v1.3.css now, the
 * same three classes every other section title in this view uses. What is left here is only
 * the spacing that a card-header nested inside a card needs and an outer one does not. */
.op-gv-ft-head { margin-bottom: 12px; }

/* The view's own tooltip, replacing the browser's. Rectangular, dark, and small enough to
 * read as an annotation rather than a panel: range, type, and the description only when
 * UniProt has one. pointer-events:none so it can sit under the pointer without stealing the
 * mousemove that positions it. */
.op-gv-ft-tip {
    position: absolute;
    z-index: 6;
    max-width: min(320px, 100%);
    padding: 7px 10px;
    border-radius: 7px;
    background: var(--text-primary);
    color: var(--bg-card);
    box-shadow: 0 10px 24px rgba(74, 66, 38, 0.28);
    pointer-events: none;
    line-height: 1.4;
}
.op-gv-ft-tip[hidden] { display: none; }
/* The range is the identifier, so it is mono and tabular, matching the sequence ruler and
 * the residue positions everywhere else on the site. */
.op-gv-ft-tip b {
    display: block;
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-size: 0.82rem;
    font-weight: 500;
}
.op-gv-ft-tip span {
    display: block;
    font-size: 0.78rem;
    font-weight: 600;
}
.op-gv-ft-tip em {
    display: block;
    margin-top: 2px;
    font-style: normal;
    font-size: 0.75rem;
    opacity: 0.82;
}

/* ONE LANE IS NOW A COLUMN OF TRACKS, and every rule in this group follows from that.
 *
 * Features used to be absolutely positioned inside a single 20px track, so two features that
 * overlap in residue space were drawn on the same 20 pixels and the shorter one simply
 * covered the longer. On TP53 that hid four whole features, including the four widest context
 * blocks in the Domains lane, and the reader had no way to know anything was missing.
 * gvPackRows now greedy-packs each lane into rows in which no two features overlap, and emits
 * one .op-gv-ft-track per row inside an .op-gv-ft-rows column.
 *
 * align-items: start rather than center is the consequence: the name labels a lane that can
 * now be one row tall or eight, and a centred label on an eight-row lane floats away from the
 * lane's first track, which is where the eye starts reading. padding-top on the name lines its
 * cap height up with the top track instead. */
.op-gv-ft-lane {
    display: grid;
    grid-template-columns: 5.6rem minmax(0, 1fr);
    align-items: start;
    gap: 10px;
    margin-bottom: 5px;
}

.op-gv-ft-name {
    padding-top: 4px;
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-muted);
    text-align: right;
}

/* Every lane wears this wrapper, including the single-track PTM tick lane and the ruler, so
 * the CSS has exactly one shape to reason about rather than a special case per lane. min-width
 * on a grid item defaults to auto, i.e. min-content, and the tracks inside carry absolutely
 * positioned children whose min-content width is not zero, so without the 0 override a wide
 * feature would push the lane past its grid column and the whole card would scroll sideways. */
.op-gv-ft-rows {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}
/* .op-gv-ft-track already declares display: block, but it is now a flex child rather than an
 * inline-level child of the lane, and stating it here keeps the row a full-width band whatever
 * a future change does to the track rule. */
.op-gv-ft-rows > .op-gv-ft-track { display: block; }

/* NEVER DROP A FEATURE SILENTLY. Past GV_FEAT_MAX_ROWS rows the packer stops opening rows and
 * this line says, in words, exactly how many blocks are not on screen. Muted and small because
 * it is a statement about the drawing rather than about the protein, which is the same voice
 * .op-gv-note-sm uses further down. */
.op-gv-ft-more {
    display: block;
    margin-top: 3px;
    font-size: 0.68rem;
    color: var(--text-muted);
}

/* cursor: help sits on the TRACK, not on the blocks inside it, and that placement is the
 * whole point. The blocks are the thing being described but they are not the thing being
 * aimed at: shell.js picks the nearest block to the pointer anywhere along the lane, so a
 * three-pixel binding site answers a hover that lands beside it. Leaving the cursor on the
 * block would have told the reader the opposite, that only those three pixels are live,
 * which is exactly the impression that made this tooltip look broken. */
.op-gv-ft-track {
    position: relative;
    display: block;
    height: 20px;
    border-radius: 4px;
    background: var(--bg-sunken);
    cursor: help;
}

/* A one-residue feature still has to be clickable and visible, hence the pixel floor: a
 * width of 0.08% would round to nothing on any screen. */
.op-gv-ft-seg {
    position: absolute;
    top: 0;
    height: 100%;
    min-width: 3px;
    box-sizing: border-box;
    border: 1px solid;
    border-radius: 3px;
    font-size: 0.62rem;
    font-style: normal;
    font-weight: 600;
    line-height: 18px;
    letter-spacing: 0.01em;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    cursor: help;
}

.op-gv-ft-track.is-ticks {
    height: 12px;
    background: transparent;
    border-bottom: 1px solid var(--border-color);
}

.op-gv-ft-tick {
    position: absolute;
    top: 0;
    width: 3px;
    height: 100%;
    margin-left: -1px;
    border-radius: 1px;
    cursor: help;
}

/* The ruler is residue numbers, not features, so it has nothing to say on hover and must
 * not claim otherwise with a help cursor. */
.op-gv-ft-ruler .op-gv-ft-track {
    height: 14px;
    background: transparent;
    cursor: default;
}
.op-gv-ft-ruler i {
    position: absolute;
    top: 0;
    font-size: 0.64rem;
    font-style: normal;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    transform: translateX(-50%);
}
.op-gv-ft-ruler i.is-end { transform: translateX(-100%); }

.op-gv-ft-legend {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 16px;
    margin-top: 12px;
    padding-top: 10px;
    border-top: 1px solid var(--border-color);
}
.op-gv-ft-key {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 0.74rem;
    color: var(--text-secondary);
}
.op-gv-ft-key i {
    width: 14px;
    height: 11px;
    border: 1px solid;
    border-radius: 2px;
}
.op-gv-ft-key i.is-tick {
    width: 4px;
    height: 13px;
    border: 0;
}

@media (max-width: 720px) {
    .op-gv-ft-lane { grid-template-columns: 1fr; gap: 3px; }
    .op-gv-ft-name { text-align: left; }
}

/* ---- the 3D annotation picker (#op-gv-3dpick) ----
 *
 * GENES VIEW ONLY, and that is a difference of SUBJECT rather than drift, exactly like
 * #op-gv-feat above it. The picker chooses among whole-protein annotation layers: PTM sites,
 * mutated sites, binding and active sites, domains, topology. The hotspot modal is opened on
 * ONE residue of one gene, so there is no layer choice it could offer and no host it could
 * paint into. Because the modal can never match any selector below, these rules are correct
 * here and would be dead weight in styles_v1.3.css.
 *
 * The swatches carry an inline background written by shell.js and nothing else, because the
 * colour is the data: PTM takes GV_PTM_STYLE[0], mutated sites take --loss, the per-class
 * feature layers take their lane's own fill, and the two per-feature layers (Domains and
 * repeats, Regions and motifs) take the colour of their FIRST feature on this protein out of
 * GV_3D_RAMP. Nothing here may assign a colour to a swatch.
 *
 * Olive is spent on .is-on, which is allowed and is the point of the doctrine: this is a
 * control saying which layers are switched on, i.e. chrome, and olive is the one hue reserved
 * for chrome precisely because it appears nowhere in the data palette.
 *
 * TWO LEVELS, and the nesting is the meaning. .op-gv-3dpick-row is the layer row, unchanged.
 * .op-gv-3dpick-feats is the per-feature chooser that appears UNDER a per-feature layer that
 * is switched on, listing that layer's individual domains or regions so the reader can pick
 * one instead of taking the whole class in one colour. It is indented and rule-marked rather
 * than merely spaced, because a flat second row of chips would read as more layers. */
.op-gv-3dpick {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
}
/* Columns, top-aligned: each child is one layer's button with that layer's chips stacked
 * under it, so an open layer grows downwards in its own lane instead of pushing a shared row
 * of chips around. align-items: flex-start is what keeps the seven buttons on one line when
 * one of them has opened a long list beneath it. */
.op-gv-3dpick-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 8px;
}
.op-gv-3dpick-col {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
    min-width: 0;
}
/* The host is written empty before any data arrives and is a flex child of .chart-card's 20px
 * column gap, so an empty picker would otherwise open a 20px hole between the colour key and
 * the model box on every gene while the fetch is in flight. Same idea as .op-legend:empty in
 * styles_v1.3.css: emptiness is the signal, and no JS writes style.display here. */
.op-gv-3dpick:empty { display: none; }

.op-gv-3dpick-label {
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.op-gv-3dpick-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    border: 1px solid var(--border-color);
    border-radius: 999px;
    background: var(--bg-card);
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 0.78rem;
    cursor: pointer;
}
.op-gv-3dpick-btn i {
    width: 10px;
    height: 10px;
    border-radius: 2px;
    flex: 0 0 auto;
}
.op-gv-3dpick-btn b {
    font-weight: 600;
    color: var(--text-muted);
}
/* .is-on is the same class name the feedback tab's rating buttons use, so "this control is
 * pressed" is one idea styled once in one voice: olive edge, olive tint, full-strength ink.
 * The state is also on aria-pressed, which shell.js keeps in step; the class exists so the
 * pressed look does not depend on an attribute selector that is easy to miss when reading. */
.op-gv-3dpick-btn.is-on {
    border-color: var(--primary);
    background: var(--primary-tint);
    color: var(--text-primary);
}
/* Disabled means the layer has nothing to draw on this protein, or that drawing it would
 * exceed GV_3D_MAX_RESI. The button keeps its label and its count so the reader can see WHY
 * it is off rather than finding a layer silently missing from the row. */
.op-gv-3dpick-btn[disabled] {
    opacity: 0.45;
    cursor: default;
}
.op-gv-3dpick-btn:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

/* ---- the drug binding row (.op-gv-bind) ----
 *
 * Same skeleton as the 3D picker above it, deliberately: both are rows of pressable chips
 * under one card, and giving them two layouts would make a single control read as two
 * components.
 *
 * The ONE difference is what a pressed DRUG chip is coloured, and that difference is the
 * point. Orange means the drug everywhere in this feature -- it is the colour the ligand is
 * drawn in on the model, fixed across every view state -- so the chip that puts a drug on
 * screen is the same orange as the thing it puts there. The mode buttons below keep the house
 * olive, because they choose a view and not a molecule.
 *
 * That colour is a literal rather than a theme token for the same reason: it has to equal the
 * WebGL colour in binding_overlay.js, which no stylesheet can reach. Named once, here. */
.op-gv-bind {
    --bind-drug: #ff7a45;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
}
/* Most genes have no solved complex, so this host is empty on most pages. Collapsing it from
 * the stylesheet rather than from JS is what keeps those pages laid out exactly as they were
 * before this feature existed. Same idea as .op-gv-3dpick:empty above. */
.op-gv-bind:empty { display: none; }

.op-gv-bind-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
}
.op-gv-bind-label {
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.op-gv-bind-btn,
.op-gv-bind-mode {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    border: 1px solid var(--border-color);
    border-radius: 999px;
    background: var(--bg-card);
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 0.78rem;
    cursor: pointer;
}
/* The PDB id: the drug chip's equivalent of the layer button's residue count, which is to say
 * the evidence behind the chip, subordinate to its name. */
.op-gv-bind-btn b {
    font-weight: 600;
    font-size: 0.72rem;
    letter-spacing: 0.02em;
    color: var(--text-muted);
}
.op-gv-bind-btn.is-on {
    border-color: var(--bind-drug);
    background: color-mix(in srgb, var(--bind-drug) 14%, transparent);
    color: var(--text-primary);
}
.op-gv-bind-btn.is-on b { color: var(--bind-drug); }
.op-gv-bind-mode.is-on {
    border-color: var(--primary);
    background: var(--primary-tint);
    color: var(--text-primary);
}
.op-gv-bind-btn:focus-visible,
.op-gv-bind-mode:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}

.op-gv-bind-chk {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 0.78rem;
    color: var(--text-secondary);
    cursor: pointer;
}
/* Disabled in "Model only", where there is no experimental chain for a contact residue to be
 * marked on. Dimmed rather than removed, so the control does not appear and vanish as the
 * reader moves between view states. */
.op-gv-bind-chk.is-off {
    opacity: 0.45;
    cursor: default;
}
.op-gv-bind-chk b {
    font-weight: 600;
    color: var(--text-muted);
}

.op-gv-bind-note {
    font-size: 0.74rem;
    color: var(--text-muted);
}
/* Why an overlay could NOT be drawn: RCSB unreachable, or a stored alignment that no longer
 * reproduces against the model on screen. It reads as a warning rather than as provenance
 * because it describes something missing from the picture, not something in it. */
.op-gv-bind-note.is-warn { color: var(--loss, #c62828); }

.op-gv-3dpick-note {
    font-size: 0.74rem;
    color: var(--text-muted);
}

/* ---- the per-feature chooser (.op-gv-3dpick-feats) ----
 *
 * WHY IT EXISTS. A layer that paints one colour over every residue of the protein tells the
 * reader nothing, and on TP53 that is exactly what "Domains" did: UniProt gives it no Domain
 * feature at all, 20 Regions covering all 393 residues six deep, and the old layer painted
 * their union in a single beige. Splitting domains from regions fixed the label; this fixes
 * the drawing. Each feature is drawn in its OWN colour, and where the whole set cannot be
 * told apart the chooser is where the reader names the one they want.
 *
 * The chip swatch is again inline and again the data: it is the exact colour that feature is
 * painted in the model, assigned once over the layer's whole feature list so a chip's colour
 * never moves as its neighbours are switched on and off. Nothing here may assign one. */
.op-gv-3dpick-feats {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 5px;
    /* A COLUMN now, not a wrapping row: the chooser lives inside its own layer's column, so
     * its chips stack straight down under the button that opened them. The rule is still the
     * indent, tying the list to that button and stopping it reading as more layers. */
    flex-direction: column;
    align-items: stretch;
    padding: 6px 0 6px 10px;
    border-left: 2px solid var(--border-color);
}
.op-gv-3dpick-fh {
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-muted);
    /* Nothing needed to keep this on its own line any more: the chooser is a column. */
}
.op-gv-3dpick-f {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 9px;
    border: 1px solid var(--border-color);
    border-radius: 999px;
    background: var(--bg-card);
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 0.74rem;
    cursor: pointer;
    /* Stacked, so each chip fills its column and the counts line up down the right edge
     * instead of landing wherever each label happens to end. A feature name is a sentence
     * fragment ("Interaction with ZNF385A"), not a word, so the column is still capped. */
    justify-content: space-between;
    text-align: left;
    max-width: 18rem;
}
.op-gv-3dpick-f i {
    width: 10px;
    height: 10px;
    border-radius: 2px;
    flex: 0 0 auto;
}
/* The residue range, in the same weight and muted ink the layer buttons give their counts,
 * because it answers the same kind of question: how much of the protein is this. */
.op-gv-3dpick-f b {
    font-weight: 600;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    flex: 0 0 auto;
}
/* Olive again, and for the same reason as .op-gv-3dpick-btn.is-on: pressed is chrome. The
 * feature's own colour stays in the swatch, where it is the data. */
.op-gv-3dpick-f.is-on {
    border-color: var(--primary);
    background: var(--primary-tint);
    color: var(--text-primary);
}
.op-gv-3dpick-f:focus-visible {
    outline: 2px solid var(--primary);
    outline-offset: 2px;
}
/* Its own line under the chips, not beside them: this is the sentence saying why nothing is
 * selected yet, and a reader who has just clicked a layer and seen no change has to be able
 * to find it without hunting along a wrapped row. */
.op-gv-3dpick-fnote {
    flex: 0 0 100%;
    font-size: 0.74rem;
    line-height: 1.5;
    color: var(--text-muted);
}

@media (max-width: 720px) {
    .op-gv-3dpick-f { max-width: 100%; }
}

/* The protein summary's lead-plus-remainder rules are gone from here, not merely unused:
 * the block is rendered by renderProteinDescription in app_v1.3.js now and its rules are up
 * with the rest of the protein description above. shell.css loads after styles_v1.3.css, so
 * a surviving .op-gv-more would have quietly won against the shared class the day the same
 * rules are relocated into the shared stylesheet for the v1 front end. */

/* The sequence grid is .seq-panel in styles_v1.3.css now, shared with the hotspot modal and
 * rendered by renderSequencePanel. The typography there is THIS view's 0.79rem / 1.85 / 10px,
 * kept over the modal's tighter set because a panel that fills residue backgrounds needs the
 * leading. The rules that used to live here had to go rather than merely stop being used:
 * shell.css loads after styles_v1.3.css, so a surviving .op-gv-seq would have quietly won
 * against the shared class on the one element both could match. */

/* ---- omics ---- */
/* Layout lives on .omics-bubble-grid in styles_v1.3.css now: this panel is rendered by v1's
 * renderHpaCard so that the genes view and the hotspot popup cannot drift apart. All this
 * wrapper still owes is a place for the loading line to sit. */
.op-gv-omics { min-width: 0; }

/* Which gene the panels are about is written by omicsCardHtml, which gives the span its own
 * id and ink, so this view no longer styles it: the hotspot modal names its gene the same way
 * in the same place. */
.op-gv-omics-wait { margin-top: 4px; }
/* ---- interaction partners ---- */
/* Reuses the empty state's chip, so a partner and a top-burden suggestion look and behave
 * the same: both are buttons carrying data-gene, both routed by the one delegated handler. */
#op-gv-rel .op-gv-chips { margin-top: 2px; }
/* Partners with no page here are listed inside the footnote as plain text, deliberately not
 * as chips: a chip that does not open anything is worse than no chip. */
.op-gv-note-sm i { font-style: normal; color: var(--text-secondary); }

.op-gv-note-sm {
    margin: 12px 0 0;
    font-size: 0.78rem;
    color: var(--text-muted);
    line-height: 1.55;
}
.op-gv-note-sm b { color: var(--text-secondary); font-weight: 600; }
/* .op-gv-none / .op-gv-wait are declared in styles_v1.3.css, beside .op-prot-none, which says
 * the same thing in the same type. Both index.html files use them for the static placeholders
 * inside the structure and sequence cards, so the desktop build needs them and they cannot
 * live here. There was a fourth, .op-prot-wait, and it is gone: it duplicated .op-gv-wait
 * exactly, and renderProteinDescription now emits .op-gv-wait so there is one waiting sentence
 * in one class across both views. */

/* ---- not-found ---- */
.op-gv-state h4 { font-size: 1.05rem; margin: 0 0 8px; color: var(--text-primary); }
.op-gv-state p { margin: 0; font-size: 0.88rem; color: var(--text-secondary); line-height: 1.6; }

/* The structure card's heading and its provenance link on one line. Baseline-aligned so the
 * small grey link sits on the heading's baseline rather than being centred against its cap
 * height, which reads as a subtitle that failed to wrap. */
/* flex-direction: ROW, and it has to be said: .card-header is a flex COLUMN, so setting
 * display:flex here changed nothing and the link stayed on its own line under the title. */
.op-gv-struct-head {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 4px 10px;
}
.op-gv-struct-head .card-subtitle { margin: 0; }
/* margin-left:auto is what right-aligns the render styles against the title on the same line,
 * and what lets them drop to their own line intact when the card is too narrow for both. */
.op-gv-styles {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 14px;
    margin-left: auto;
}
/* Each group stays whole when the header wraps, so "Colour by" never ends up on one line with
 * its buttons on the next. */
.op-gv-styles-grp {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}
.op-gv-seq-head { margin-top: 4px; }

/* The pull-apart slider. Sized so the track is long enough to be worth dragging but short
 * enough to sit on the same row as the view-state buttons it belongs with. */
.op-gv-bind-sep {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 0.78rem;
    color: var(--text-secondary);
}
.op-gv-bind-sep input[type="range"] { width: 130px; accent-color: var(--bind-drug, #ff7a45); }
.op-gv-bind-sep b {
    font-weight: 600;
    color: var(--text-muted);
    min-width: 3.2em;   /* so the row does not twitch as the number grows a digit */
}
