/* --- base.css --- */

/* Base layout, body, header, scrollbar */
:root {
    /* Font-size scale. One source of truth; 1rem = 16px. */
    --fs-2xs:  0.625rem;   /* 10px - badges, counts, tags, toggles */
    --fs-xs:   0.6875rem;  /* 11px - small labels, group headers */
    --fs-sm:   0.75rem;    /* 12px - meta / secondary text, panel h4 */
    --fs-base: 0.8125rem;  /* 13px - body: inputs, buttons, links, names */
    --fs-lg:   0.9375rem;  /* 15px - panel & section titles */
    --fs-xl:   1rem;       /* 16px - headings */
    --fs-2xl:  1.125rem;   /* 18px - large headings */
    --fs-3xl:  1.375rem;   /* 22px - display stat */

    /* Core colour palette. One source of truth for the recurring values. */
    --bg:           #1a1a2e;   /* page background */
    --surface:      #16213e;   /* header / panel surface */
    --border:       #0f3460;   /* default panel border (blue) */
    --accent-color: #e94560;   /* pink accent: titles, links, highlights */
    --text-color:   #e0e0e0;   /* primary body text */
    --text-muted:   #888;      /* muted secondary text */
    --text-dim:     #666;      /* dim secondary text */
    --danger:       #e57373;   /* blocked / warning / error red */
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
    font-family: 'Segoe UI', system-ui, sans-serif;
    background: var(--bg);
    color: var(--text-color);
    height: 100vh;
    height: 100dvh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
header {
    background: var(--surface);
    padding: 12px 20px;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: 16px;
    flex-shrink: 0;
}
header h1 {
    font-size: var(--fs-xl);
    color: var(--accent-color);
    white-space: nowrap;
}
.search-container {
    flex: 1;
    position: relative;
    max-width: 500px;
}
#search {
    width: 100%;
    padding: 6px 12px;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--bg);
    color: var(--text-color);
    font-size: var(--fs-base);
    line-height: 18px;
    outline: none;
}
#search:focus { border-color: var(--accent-color); }

/* Visible focus ring for keyboard users on the viewer's inline-onclick
   controls (made focusable by keyboard-a11y.js). Only shows for keyboard
   focus (:focus-visible), so mouse clicks don't leave a lingering outline. */
[onclick]:focus-visible {
    outline: 2px solid var(--accent-color);
    outline-offset: 2px;
    border-radius: 2px;
}
#search-results {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-top: none;
    border-radius: 0 0 4px 4px;
    max-height: 450px;
    overflow-y: auto;
    z-index: 100;
    display: none;
}
#search-results.visible { display: block; }
.search-section-header {
    padding: 4px 12px;
    font-size: var(--fs-2xs);
    font-weight: bold;
    color: #aaa;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    background: var(--border);
    border-bottom: 1px solid #0a2647;
    position: sticky;
    top: 0;
    z-index: 1;
}
.search-item {
    padding: 6px 12px;
    cursor: pointer;
    font-size: var(--fs-base);
    border-bottom: 1px solid #0f346040;
}
.search-no-matches {
    padding: 10px 12px;
    font-size: var(--fs-base);
    color: var(--text-muted);
    font-style: italic;
}
.search-item:hover,
.search-item.is-active { background: var(--border); }
/* Suppress the pure-CSS :hover highlight while the user is driving
   the dropdown from the keyboard. Without this, a row that happens
   to sit under the mouse cursor when ArrowUp/ArrowDown is pressed
   stays visually highlighted alongside the keyboard-active row,
   making it ambiguous which one Enter will commit. The ``kbd-mode``
   class is added by ``search-ui.js`` on arrow-key navigation and
   removed on the next real mouse movement, so visible state always
   matches the latest input modality. */
#search-results.kbd-mode .search-item:hover { background: transparent; }
#search-results.kbd-mode .search-item.is-active { background: var(--border); }
.search-item .npc { color: var(--text-muted); font-size: var(--fs-xs); margin-left: 8px; }
.search-item-cross .cross-game-badge { margin-left: 8px; vertical-align: middle; }
/* In a search row the whole row is the link; the badge is a static
   label, so freeze its hover to the base look (the global
   .cross-game-badge:hover is for the standalone link in the info panel). */
.search-item-cross .cross-game-badge:hover {
    background: rgba(88, 166, 255, 0.12);
    border-color: rgba(88, 166, 255, 0.4);
}
.search-item-text .search-item-head { font-size: var(--fs-base); }
.search-item-text .search-snippet {
    font-size: var(--fs-xs);
    color: #b8b8b8;
    margin-top: 2px;
    line-height: 1.4;
    white-space: normal;
    word-break: break-word;
}
.search-item-text .snippet-speaker {
    color: var(--text-muted);
    font-weight: bold;
    margin-right: 2px;
}
.search-item-text .snippet-choice-label {
    color: var(--text-muted);
    font-weight: bold;
    font-style: italic;
    margin-right: 2px;
}
.search-item mark {
    background: var(--accent-color);
    color: #fff;
    padding: 0 2px;
    border-radius: 2px;
}
.header-nav-link {
    appearance: none;
    background: var(--bg);
    color: var(--text-color);
    font-family: inherit;
    font-size: var(--fs-base);
    line-height: 18px;
    padding: 6px 14px;
    border: 1px solid var(--border);
    border-radius: 4px;
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color 0.12s ease, color 0.12s ease;
}
.header-nav-link:hover {
    background: var(--border);
}
.header-nav-link[hidden] {
    display: none;
}
.header-right {
    display: flex;
    align-items: center;
    gap: 16px;
    margin-left: auto;
    flex-shrink: 0;
}
main {
    flex: 1;
    display: flex;
    overflow: hidden;
}
.empty-state {
    color: #555;
    font-style: italic;
    text-align: center;
    padding: 40px;
}

/* Failure banner rendered by viewer.js when data load (fetch or
   JSON.parse) fails. Hidden until populated; styled to be unmissable
   without clobbering the page chrome (search bar, panel headers). */
.app-error {
    background: #3a1414;
    color: #f8b3b3;
    border: 1px solid #a44;
    border-radius: 4px;
    padding: 12px 16px;
    margin: 12px 20px;
    font-size: var(--fs-base);
    flex-shrink: 0;
}

/* Scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--accent-color); }
/* The corner is the small square where the horizontal and vertical
   scrollbars meet on a 2-axis-scrolling container (`.panel-body`).
   Without this rule, browsers paint it plain white, which stands out
   against the dark panel background. Match the track colour so the
   corner blends into the scrollbar gutter. */
::-webkit-scrollbar-corner { background: var(--bg); }

/* Site disclaimer - pinned to the bottom of the middle panel
   (upstream/prerequisites) in 3-column dialogue view, and to the
   bottom of the info panel in single-panel views. Uses margin-top:auto
   so it sticks to the bottom even with short content. */
#upstream-content,
.layout-speaker #info-content {
    display: flex;
    flex-direction: column;
}
#upstream-content::after,
.layout-speaker #info-content::after {
    content: 'Built with the assistance of generative AI. All game content belongs to Supergiant Games.';
    display: block;
    padding: 12px 20px;
    margin-top: auto;
    font-size: var(--fs-2xs);
    color: #444;
    text-align: center;
    border-top: 1px solid #0f346040;
}

/* --- duplicates.css --- */

/* Cross-game duplicates view - speaker master / dialogue detail layout. */

.duplicates-view {
    padding: 0.5rem 0;
}

.duplicates-header {
    display: block;
    margin-bottom: 0.8rem;
    border: 1px solid var(--border);
    border-radius: 6px;
}

.duplicates-header h3 {
    margin: 0 0 0.4rem;
}

.duplicates-subtitle {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--fs-lg);
}

.duplicates-controls {
    margin-bottom: 1rem;
}

/* Match the main ``#search`` bar (styles/base.css) so the two search
   inputs read as one consistent control. */
.duplicates-search {
    width: 100%;
    max-width: 500px;
    padding: 6px 12px;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--bg);
    color: var(--text-color);
    font-size: var(--fs-base);
    line-height: 18px;
    outline: none;
}

.duplicates-search:focus {
    border-color: var(--accent-color);
}

.duplicates-empty {
    margin-top: 1rem;
}

/* Master-detail: speaker list on the left, the selected speaker's
   shared dialogues on the right. */
.duplicates-md {
    display: grid;
    grid-template-columns: minmax(160px, 220px) 1fr;
    align-items: start;
    gap: 1.2rem;
}

/* Speaker master list */
.duplicates-speakers {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.duplicates-speaker-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    width: 100%;
    padding: 0.4rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--surface);
    color: var(--text-color);
    font-size: var(--fs-base);
    text-align: left;
    cursor: pointer;
}

.duplicates-speaker-item:hover {
    background: var(--border);
}

.duplicates-speaker-item.is-active {
    background: var(--border);
    border-color: var(--accent-color);
}

.duplicates-speaker-name {
    font-weight: 600;
}

.duplicates-speaker-count {
    font-size: var(--fs-sm);
    color: var(--text-muted);
    background: rgba(255, 255, 255, 0.06);
    padding: 0.1rem 0.45rem;
    border-radius: 8px;
}

.duplicates-speaker-item.is-active .duplicates-speaker-count {
    color: var(--text-color);
}

/* Detail pane: the selected speaker's shared dialogues, tiled across
   the available width. */
.duplicates-detail-title {
    margin: 0 0 0.6rem;
    font-size: var(--fs-xl);
}

.duplicates-detail-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
    align-items: start;
    gap: 0.3rem 0.8rem;
}

.duplicates-entry {
    display: grid;
    grid-template-columns: max-content auto auto;
    align-items: center;
    justify-content: start;
    column-gap: 0.25rem;
}

.duplicates-name {
    display: inline-block;
    width: var(--dup-name-col, max-content);
    font-size: var(--fs-base);
    font-family: 'Consolas', monospace;
    color: var(--text-color);
}

.duplicates-game-link {
    font-size: var(--fs-base);
    color: var(--accent-color);
    cursor: pointer;
    text-decoration: none;
    padding: 0.25rem 0.55rem;
    border: 1px solid var(--border);
    border-radius: 3px;
    background: transparent;
    text-align: center;
}

.duplicates-game-link:hover {
    background: var(--border);
    border-color: var(--accent-color);
}

/* Keep the wider name-to-pill gap while the two game pills sit close together. */
.duplicates-entry > .duplicates-game-link:first-of-type {
    margin-left: 0.35rem;
}

/* --- eligibility.css --- */

/* Eligibility tracer view styles */

.eligibility-view {
    padding: 0.5rem 0;
}

.eligibility-target h3 {
    margin: 0 0 0.3rem 0;
    font-size: var(--fs-2xl);
}

.eligibility-target-link {
    color: var(--accent-color);
    cursor: pointer;
}

.eligibility-target-link:hover {
    text-decoration: underline;
}

.eligibility-target-meta {
    font-size: var(--fs-base);
    color: var(--text-muted);
    margin-bottom: 1rem;
}

/* Summary block */
.eligibility-summary {
    padding: 0.8rem 1rem;
    border-radius: 6px;
    background: #1a1a2a;
    border: 1px solid #3a3a4a;
    margin-bottom: 1rem;
}

.eligibility-status {
    font-size: var(--fs-xl);
    font-weight: 600;
    margin-bottom: 0.3rem;
}

.eligibility-status.eligibility-played {
    color: #6be06b;
}

.eligibility-status.eligibility-eligible {
    color: #ffb74d;
}

.eligibility-status.eligibility-blocked {
    color: #90a4ae;
}

.eligibility-status.eligibility-unobtainable {
    color: var(--danger);
}

.eligibility-status.eligibility-indeterminate {
    color: #aab2e6;
}

.eligibility-unobtainable-reasons {
    margin: 0.2rem 0 0;
    padding-left: 1.2rem;
    font-size: var(--fs-base);
    color: #c8d0d8;
    line-height: 1.5;
}

/* Mutually-exclusive alternate lock: the headline reason a dialogue is
   unobtainable. A boxed callout in the gold/tan scheme used by the alternates
   box in the dialogue detail view (``.alternates-header`` #c4a56e) so it reads
   as the same "mutually exclusive variants" concept and stands out from the
   plain reason list. */
.eligibility-alt-lock {
    display: flex;
    align-items: flex-start;
    gap: 0.5rem;
    margin: 0.4rem 0;
    padding: 0.5rem 0.7rem;
    border: 1px solid #c4a56e55;
    border-left: 3px solid #c4a56e;
    border-radius: 4px;
    background: #262017;
    font-size: var(--fs-base);
    line-height: 1.45;
    color: #d8cdb8;
}

.eligibility-alt-lock-icon {
    color: #c4a56e;
    font-size: 1.1em;
    line-height: 1.3;
}

.eligibility-alt-lock-title {
    display: block;
    margin-bottom: 0.15rem;
    color: #c4a56e;
}

.eligibility-detail {
    font-size: var(--fs-base);
    color: var(--text-muted);
    margin-bottom: 0.5rem;
}

.eligibility-progress-bar {
    height: 6px;
    background: #2a2a3a;
    border-radius: 3px;
    overflow: hidden;
    margin: 0.4rem 0 0.2rem;
}

.eligibility-progress-fill {
    height: 100%;
    background: #4caf50;
    border-radius: 3px;
    transition: width 0.3s;
}

.eligibility-progress-label {
    font-size: var(--fs-sm);
    color: var(--text-muted);
}
/* When a save is loaded and at least one prerequisite is already complete,
   the label carries a tooltip listing them - hint that with the standard
   dotted-underline + help cursor affordance. inline-block keeps the
   underline hugging the text rather than the full-width row. */
.eligibility-progress-label[data-tooltip] {
    display: inline-block;
    cursor: help;
    border-bottom: 1px dotted #88888866;
}

/* Explains that the prerequisite total spans the whole chain (indirect
   prerequisites included), addressing "why does this say N when there's
   only one immediate requirement?". Shown only when the chain runs deeper
   than the immediate level; hover reveals the fuller detail. */
.eligibility-chain-note {
    margin-top: 0.6rem;
    padding: 0.35rem 0.5rem 0.35rem 0.55rem;
    border-left: 2px solid #5a6b8c;
    background: rgba(255, 255, 255, 0.03);
    border-radius: 0 4px 4px 0;
    font-size: var(--fs-xs);
    font-style: italic;
    color: #9aa4b2;
    line-height: 1.4;
}
.eligibility-chain-note::before {
    content: "\24D8"; /* circled information source */
    margin-right: 0.35rem;
    font-style: normal;
    opacity: 0.85;
}
.eligibility-chain-note[data-tooltip] {
    cursor: help;
}

/* Inline play-priority badge appended to the status detail sentence. */
.eligibility-rank-inline {
    white-space: nowrap;
}
.eligibility-rank-unranked {
    cursor: help;
}

/* "Will play before this" note: surfaces the eligible siblings that the
   game would resolve ahead of this dialogue (H2 ordinal / H1 priority
   tier), or reassures the user it is next. The whole block carries the
   best-effort disclaimer tooltip, so flag it with the help cursor. */
.eligibility-play-order {
    margin-top: 0.5rem;
    padding: 0.4rem 0.55rem;
    border-left: 2px solid #c4a56e;
    background: rgba(196, 165, 110, 0.08);
    border-radius: 0 4px 4px 0;
    font-size: var(--fs-sm);
    color: var(--text-muted);
    line-height: 1.45;
    cursor: help;
}
.eligibility-play-order-next {
    color: #cdbb92;
    font-weight: 600;
}
.eligibility-play-order-head {
    margin-bottom: 0.3rem;
    color: #cdbb92;
    font-weight: 600;
}
.eligibility-play-order-list {
    list-style: none;
    margin: 0.1rem 0 0;
    padding: 0;
    /* Shared columns so badges, names and the Trace buttons each line up
       in one column (the Trace offset by the longest name in the list),
       mirroring the fixed-width-column alignment used elsewhere. */
    display: grid;
    grid-template-columns: max-content max-content max-content;
    justify-content: start;
    align-items: center;
    gap: 0.4rem 0.6rem;
    font-size: var(--fs-base);
}
.eligibility-play-order-item {
    display: contents;
}
.eligibility-play-order-badge,
.eligibility-play-order-name {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
}
.eligibility-play-order-trace {
    flex-shrink: 0;
    padding: 2px 10px;
    background: var(--bg);
    color: var(--text-color);
    border: 1px solid var(--border);
    border-radius: 4px;
    font-size: var(--fs-xs);
    cursor: pointer;
    text-decoration: none;
}
.eligibility-play-order-trace:hover {
    background: var(--border);
}
.eligibility-play-order-tag {
    font-size: var(--fs-2xs);
    color: #9a9ab0;
    border: 1px solid #4a4636;
    border-radius: 3px;
    padding: 0 4px;
    margin-left: 0.2rem;
    text-transform: uppercase;
    letter-spacing: 0.3px;
}

/* Unplayed list */
.eligibility-tree {
    margin-top: 1rem;
}

.eligibility-tree-header {
    margin: 0 0 0.2rem 0;
    font-size: var(--fs-lg);
    color: var(--text-color);
}

.eligibility-tree-hint {
    font-size: var(--fs-sm);
    color: var(--text-muted);
    margin-bottom: 0.6rem;
}

.eligibility-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.eligibility-item {
    padding: 0.4rem 0.6rem;
    border-radius: 4px;
    background: #1e1e2e;
    border: 1px solid #2a2a3a;
    cursor: pointer;
    transition: background 0.1s, border-color 0.1s;
}

.eligibility-item:hover {
    background: #2a2a3e;
    border-color: var(--accent-color);
}

.eligibility-item-main {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.eligibility-depth {
    font-size: var(--fs-xs);
    color: #78909c;
    font-weight: 600;
    min-width: 2.75rem;
    flex-shrink: 0;
    cursor: help;
}

.eligibility-item-name {
    font-family: 'Consolas', monospace;
    font-size: var(--fs-base);
    color: var(--text-color);
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.eligibility-item-meta {
    margin-top: 0.2rem;
    padding-left: 3.25rem;
    font-size: var(--fs-sm);
    color: var(--text-muted);
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
}

.eligibility-needed-by {
    color: #78909c;
}

.eligibility-ref {
    color: var(--accent-color);
    cursor: pointer;
}

.eligibility-ref:hover {
    text-decoration: underline;
}

/* Requirement group ("play any N of these") in the unplayed list */
.eligibility-group {
    padding: 0.4rem 0.6rem;
    border-radius: 4px;
    background: #1e1e2e;
    border: 1px solid #2a2a3a;
}

.eligibility-group-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

/* Timing (run-count) gate card in the "Timing gates" section */
.eligibility-gate {
    padding: 0.4rem 0.6rem;
    border-radius: 4px;
    background: #1e1e2e;
    border: 1px solid #2a2a3a;
}

.eligibility-gate-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-family: 'Consolas', monospace;
    font-size: var(--fs-base);
    color: var(--text-color);
}

.eligibility-gate-refs {
    margin: 0.3rem 0 0;
    padding-left: 1.4rem;
    font-size: var(--fs-sm);
    color: var(--text-muted);
    line-height: 1.5;
}

/* A gate option that has played in the save but not in the gate's scope -
   a near-miss: it has played, just not where this gate needs it, so it
   doesn't satisfy the gate (which stays blocked). */
.gate-ref-elsewhere {
    color: #4db6ac;
}

.eligibility-group-title {
    flex: 1;
    min-width: 0;
    font-size: var(--fs-base);
    font-weight: 600;
    color: #ffd08a;
}

.eligibility-group-options {
    margin-top: 0.3rem;
    padding-left: 2rem;
    display: flex;
    flex-direction: column;
    gap: 1px;
}

.eligibility-group-option {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.2rem 0.4rem;
    border-radius: 3px;
    cursor: pointer;
}

.eligibility-group-option:hover {
    background: #2a2a3e;
}

.eligibility-group-option-icon {
    flex-shrink: 0;
    color: #78909c;
}

.eligibility-group-option.played {
    opacity: 0.6;
}

.eligibility-group-option.played .eligibility-group-option-icon {
    color: #6be06b;
}

/* H2 OR-branch cards: an alternative requirement set in the tracer. */
.eligibility-branch-satisfied {
    border-color: #2f6b2f;
}

.eligibility-branch-satisfied .eligibility-group-option-icon {
    color: #6be06b;
}

.eligibility-branch-indeterminate {
    border-color: #3a3f6a;
}

.eligibility-branch-indeterminate .eligibility-group-option-icon {
    color: #aab2e6;
}

.eligibility-branch-note {
    padding: 0.2rem 0.4rem 0;
    font-size: var(--fs-xs);
    color: #9aa3b2;
}

.eligibility-branch-prereqs {
    margin-top: 0.3rem;
}

/* Requirement group node in the prerequisite tree */
.tree-group-row {
    cursor: default;
}

.tree-group-label {
    font-size: var(--fs-base);
    font-weight: 600;
    color: #ffd08a;
}

.tree-group-children {
    display: block;
}

/* Tree view */
.eligibility-tree-section {
    margin-top: 1.2rem;
    border-top: 1px solid #2a3a40;
    padding-top: 0.8rem;
}

.eligibility-tree-container {
    font-size: var(--fs-base);
    margin-top: 0.5rem;
}

.tree-node {
    margin-left: 1rem;
    border-left: 1px solid #3a4a50;
    padding-left: 0.6rem;
    margin-top: 0.15rem;
}

.tree-node-row {
    cursor: pointer;
    padding: 0.2rem 0.3rem;
    border-radius: 3px;
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
}

.tree-node-row:hover {
    background: #1e2a30;
}

.tree-chevron {
    flex-shrink: 0;
    width: 0.8em;
    font-size: 0.65em;
    transition: transform 0.15s;
    display: inline-block;
}

.tree-node.collapsed > .tree-node-row .tree-chevron {
    transform: rotate(0deg);
}

.tree-node:not(.collapsed) > .tree-node-row .tree-chevron {
    transform: rotate(90deg);
}

.eligibility-tree-container .tree-children {
    display: block;
    margin-top: 0.1rem;
}

.tree-node.collapsed > .tree-children {
    display: none;
}

.tree-link {
    cursor: pointer;
    opacity: 0.4;
    font-size: 0.8em;
    text-decoration: none;
}

.tree-link:hover {
    opacity: 1;
}

.eligibility-tree-container .save-badge {
    flex-shrink: 0;
}

.eligibility-tree-container .npc-tag {
    opacity: 0.7;
    font-style: italic;
}

.tree-played .tree-name {
    color: #6a8a6a;
    text-decoration: line-through;
    opacity: 0.7;
}

.tree-unplayed .tree-name {
    color: var(--text-color);
}

/* --- game-toggle.css --- */

/* Game toggle: two side-by-side pill buttons in the header that
   swap the viewer's active game. Styled to read as a segmented
   control so users can tell it's a single binary choice, not two
   independent actions. */

.game-toggle {
    display: inline-flex;
    flex-shrink: 0;
    border: 1px solid var(--border);
    border-radius: 4px;
    overflow: hidden;
}
.game-toggle[hidden] { display: none; }
.game-toggle-btn {
    appearance: none;
    border: none;
    background: var(--bg);
    color: var(--text-color);
    font-family: inherit;
    font-size: var(--fs-base);
    line-height: 18px;
    padding: 6px 14px;
    cursor: pointer;
    border-right: 1px solid var(--border);
    transition: background-color 0.12s ease, color 0.12s ease;
    /* Reserve the bold (.active) width via a hidden bold twin so toggling the
       active game never resizes the control - which would shift the flex
       search bar beside it. */
    display: inline-grid;
    justify-items: center;
    align-content: center;
}
.game-toggle-btn::after {
    content: attr(data-label);
    font-weight: 600;
    height: 0;
    overflow: hidden;
    visibility: hidden;
}
.game-toggle-btn:last-child { border-right: none; }
.game-toggle-btn:hover { background: var(--border); }
.game-toggle-btn.active {
    background: var(--accent-color);
    color: var(--bg);
    font-weight: 600;
}
.game-toggle-btn:focus-visible {
    outline: 2px solid var(--accent-color);
    outline-offset: -2px;
}

/* --- panels-badges.css --- */

/* Badges shown on textline rows and tree nodes. Each badge type uses
   distinct colour tones so the user can tell them apart at a glance:
   warm/cool for repeatability, neutral for self-reference, category
   hues for unresolved children, and warning red for blocked nodes. */

/* PlayOnce / Repeatable indicator. Always renders one of the two
   badges so the repeatability state is explicit rather than implied.
   Shared base pill used both in the details-panel header (pinned to the
   right of the `.h3-indicators` cluster) and on speaker-overview rows.
   Warm tone for the hard cap, cool tone for the default repeatable
   state. inline-flex + line-height keep the glyph and emoji centred. */
.play-once-badge {
    display: inline-flex;
    align-items: center;
    line-height: 1;
    padding: 1px 6px;
    border-radius: 3px;
    font-size: var(--fs-xs);
    font-weight: 500;
    flex-shrink: 0;
}
.play-once-badge.play-once-locked {
    background: #2e2018;
    color: #f5b78c;
    border: 1px solid #5a3a2a;
}
.play-once-badge.play-once-repeatable {
    background: #1c2a26;
    color: #a0d4be;
    border: 1px solid #2c4a3e;
}

/* Self-reference marker: a textline that lists itself in one of its
   own requirement fields. Always a benign cooldown/PlayOnce idiom,
   never a hard prereq; filtered out of the dependency tree but kept
   visible here so the user sees the game data faithfully. */
.req-item.self-ref {
    opacity: 0.75;
}
.self-ref-badge {
    display: inline-block;
    margin-left: 6px;
    padding: 0 5px;
    font-size: var(--fs-2xs);
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-radius: 3px;
    background: rgba(141, 153, 174, 0.20);
    color: #b8c2d4;
    border: 1px solid rgba(141, 153, 174, 0.40);
}

/* Tree-node badge for unresolved children - small inline pill that
   inherits the same color language as the req-item categories. */
.tree-unresolved-badge {
    font-size: var(--fs-2xs);
    margin-left: 6px;
    padding: 1px 5px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.06);
    color: var(--text-muted);
    font-style: italic;
}
.tree-unresolved-badge.unresolved-cat-back-compatibility {
    color: #c9a96e;
    background: rgba(201, 169, 110, 0.10);
}
.tree-unresolved-badge.unresolved-cat-typo-or-bug {
    color: var(--danger);
    background: rgba(224, 123, 123, 0.10);
}
.tree-unresolved-badge.unresolved-cat-cut-content {
    color: var(--text-muted);
    background: rgba(136, 136, 136, 0.10);
}
.tree-unresolved-badge.unresolved-cat-uncategorized {
    color: var(--danger);
    background: rgba(217, 122, 122, 0.12);
}

/* Tree-node badge: marks a dialogue that can never play (either because
   it is itself blocked, or because it is an unresolved ref that blocks
   other dialogues). */
.tree-blocked-badge {
    display: inline-block;
    margin-left: 6px;
    padding: 0 6px;
    font-size: var(--fs-2xs);
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-radius: 3px;
    background: rgba(231, 76, 60, 0.20);
    color: #ffb3a8;
    border: 1px solid rgba(231, 76, 60, 0.45);
}

/* --- panels-banners.css --- */

/* Info-panel banner shown when the user navigates to an unresolved ref. */
.unresolved-banner {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 8px 10px;
    border-radius: 4px;
    border-left: 3px solid var(--text-muted);
    background: rgba(255, 255, 255, 0.04);
    margin-top: 8px;
}
.unresolved-banner.unresolved-cat-back-compatibility {
    border-left-color: #c9a96e;
    background: rgba(201, 169, 110, 0.08);
}
.unresolved-banner.unresolved-cat-typo-or-bug {
    border-left-color: var(--danger);
    background: rgba(224, 123, 123, 0.08);
}
.unresolved-banner.unresolved-cat-cut-content {
    border-left-color: var(--text-muted);
    background: rgba(136, 136, 136, 0.08);
}
.unresolved-banner.unresolved-cat-uncategorized {
    border-left-color: var(--danger);
    background: rgba(217, 122, 122, 0.10);
}
.unresolved-cat-badge {
    font-size: var(--fs-xs);
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-color);
}
.unresolved-cat-reason {
    font-size: var(--fs-sm);
    color: #cfcfcf;
}
.unresolved-cat-desc {
    font-size: var(--fs-xs);
    color: #9aa0a6;
    font-style: italic;
    padding: 0 4px;
}

/* Banner shown in the info panel for a dialogue that can never play
   because at least one hard requirement field references undefined
   textlines. Also reused inside the unresolved-ref panel to list which
   dialogues that unresolved ref blocks. */
.blocked-banner {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 8px 10px;
    border-radius: 4px;
    border-left: 3px solid #e74c3c;
    background: rgba(231, 76, 60, 0.12);
    margin-top: 8px;
}
.blocked-banner-header {
    font-size: var(--fs-sm);
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #ffb3a8;
}
.blocked-banner-list {
    font-size: var(--fs-sm);
    color: #e8d2cf;
    line-height: 1.6;
}
.blocked-reason {
    font-size: var(--fs-sm);
    color: #e8d2cf;
    line-height: 1.5;
}
.blocked-reason strong {
    color: #fff;
    font-family: 'Consolas', monospace;
    font-weight: normal;
}
.blocked-ref {
    color: #ffb3a8;
    cursor: pointer;
    text-decoration: underline dotted;
}
.blocked-ref:hover {
    color: #fff;
}

/* Compact "Renamed" badge shown next to the textline name when the
   tool split a name collision. Tooltip carries the full explanation;
   the visible label is small enough not to crowd the priority and
   play-once badges that share the header row. */
.collision-badge {
    display: inline-block;
    font-size: var(--fs-xs);
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #f1c98a;
    background: rgba(224, 168, 87, 0.16);
    border: 1px solid rgba(224, 168, 87, 0.4);
    border-radius: 3px;
    padding: 1px 6px;
    margin-left: 6px;
    vertical-align: middle;
    cursor: help;
}

.cross-game-badge {
    display: inline-block;
    font-size: var(--fs-xs);
    font-weight: bold;
    color: #7ee8fa;
    background: rgba(88, 166, 255, 0.12);
    border: 1px solid rgba(88, 166, 255, 0.4);
    border-radius: 3px;
    padding: 1px 6px;
    margin-left: 6px;
    vertical-align: middle;
    cursor: pointer;
    text-decoration: none;
}

.cross-game-badge:hover {
    background: rgba(88, 166, 255, 0.25);
    border-color: rgba(88, 166, 255, 0.7);
}

/* Banner shown below the meta line when the textline was renamed by
   the tool. Lists the original name and links to the duplicate
   sibling siblings so the user can quickly compare definitions. The
   full engine-bug explanation lives on the in-banner "Renamed for
   Dialogue Explorer" label (matching the header badge tooltip), so
   hovering the sibling links does not pop up an obscuring tooltip. */
.collision-banner {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 8px 10px;
    border-radius: 4px;
    border-left: 3px solid #e0a857;
    background: rgba(224, 168, 87, 0.10);
    margin-top: 8px;
}
.collision-banner-header {
    font-size: var(--fs-sm);
    color: #f1c98a;
    line-height: 1.5;
}
.collision-banner-label {
    cursor: help;
    text-decoration: underline dotted;
    text-underline-offset: 2px;
}
.collision-banner-header code {
    font-family: 'Consolas', monospace;
    background: rgba(255, 255, 255, 0.06);
    padding: 0 4px;
    border-radius: 2px;
    color: #fff;
}
.collision-banner-body {
    font-size: var(--fs-sm);
    color: #e0d6c4;
    line-height: 1.6;
}
.collision-sibling-link {
    color: #f1c98a;
    cursor: pointer;
    text-decoration: underline dotted;
}
.collision-sibling-link:hover {
    color: #fff;
}

/* --- panels-dialogue.css --- */

/* Dialogue lines list in the details panel - shows the actual spoken
   text with the speaker name prefix. Italic prose with a coloured
   speaker tag to break up the wall of text. */
.dialogue-section { margin-top: 10px; }
.dialogue-section h4 { font-size: var(--fs-sm); color: var(--text-muted); margin-bottom: 6px; }
.dialogue-line {
    padding: 6px 10px;
    margin: 4px 0;
    font-size: var(--fs-sm);
    color: #ccd6e0;
    background: var(--bg);
    border-left: 3px solid var(--border);
    border-radius: 0 4px 4px 0;
    line-height: 1.5;
    font-style: italic;
}
.dialogue-line .speaker-name {
    font-style: normal;
    font-weight: 600;
    color: var(--accent-color);
}
.dialogue-line .speaker-name[data-tooltip] {
    cursor: help;
    border-bottom: 1px dotted #e9456066;
}
.dialogue-line .speaker-sep {
    font-style: normal;
    color: var(--accent-color);
    margin-right: 4px;
}

/* Structured choice-prompt rendering: cues that declare
   ``Choices = {...}`` get a three-line block - the prompt itself
   followed by one ``A: …`` / ``B: …`` line per option. The prompt
   gets the same border colour as a normal dialogue line; the option
   lines step the accent over to the same purple the synthetic-banner
   uses so they read as a structural branch point, not as additional
   spoken text. */
.dialogue-line.choice-prompt .choice-prompt-label {
    font-style: normal;
    font-weight: 600;
    color: var(--accent-color);
    margin-right: 4px;
}
.dialogue-line.choice-option {
    border-left-color: #6c5b7b;
    background: #1b1b2e;
    margin-left: 16px;
}
.dialogue-line.choice-option .choice-option-letter {
    font-style: normal;
    font-weight: 600;
    color: #b8a5ff;
    margin-right: 4px;
}
.dialogue-line .choice-link {
    color: #b8a5ff;
    cursor: pointer;
    text-decoration: underline;
    font-style: normal;
}
.dialogue-line .choice-link:hover { color: #fff; }
.choice-name {
    font-style: normal;
}
.choice-name[data-tooltip] {
    cursor: help;
}
/* When a choice-name sits inside a clickable choice-link the link
   action dominates the affordance: keep the cursor as ``pointer`` so
   the user sees "clickable" first; the tooltip still fires on hover
   regardless of cursor. */
.choice-link .choice-name[data-tooltip] {
    cursor: pointer;
}

/* --- panels-gamedata.css --- */

/* GameData group framing inside the details panel: same visual language
   as the tree-view group box, scaled for the denser req-item list. */
.gamedata-group-inline {
    border: 1px dashed #4a3a6b;
    border-radius: 4px;
    background: rgba(108, 91, 123, 0.08);
    padding: 4px 6px;
    margin: 4px 0;
}
.gamedata-group-header-inline {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: var(--fs-xs);
    margin-bottom: 2px;
    /* Whole header acts as the collapse toggle - matches the
       tree-view group header. Wired via delegation in info-panel.js. */
    cursor: pointer;
    padding: 2px 4px;
    margin-left: -4px;
    margin-right: -4px;
    border-radius: 3px;
}
.gamedata-group-header-inline:hover { background: rgba(108, 91, 123, 0.18); }
.gamedata-group-header-inline .toggle {
    min-width: 12px;
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-2xs);
    flex-shrink: 0;
}
.gamedata-group-header-inline .gamedata-group-label {
    font-family: 'Consolas', monospace;
    font-weight: 600;
    color: #b8a5ff;
}
.gamedata-group-header-inline .gamedata-group-count {
    font-size: var(--fs-2xs);
    color: var(--text-muted);
    margin-left: auto;
}
/* Collapsed by default + override via `.expanded` mirrors the
   tree-view children box (styles/tree.css). Keeping the same idiom
   means the same `.expanded` class controls visibility in both
   places. */
.gamedata-group-children-inline { display: none; }
.gamedata-group-children-inline.expanded { display: block; }

/* --- panels-requirements.css --- */

/* Requirement sections inside the details panel: one box per requirement
   field with category-specific colors so the user can scan a long list
   and tell required vs forbidden vs any-of at a glance. */
.req-section {
    margin-top: 10px;
    padding: 6px 8px 4px;
    border-radius: 4px;
    border-left: 3px solid #555;
    background: rgba(255, 255, 255, 0.025);
}
.req-section h4 {
    font-size: var(--fs-xs);
    margin: 0 0 6px;
    padding: 1px 6px;
    border-radius: 3px;
    display: inline-block;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    /* Whole pill acts as the collapse toggle. The colored bg per
       req-type stays as the visual affordance; cursor signals it's
       interactive. Wired via delegation in info-panel.js. */
    cursor: pointer;
}
.req-section h4 .toggle {
    /* Inline chevron prepended inside the colored pill. Inline-block
       so margin-right stays consistent regardless of the surrounding
       text run. */
    display: inline-block;
    font-size: var(--fs-2xs);
    opacity: 0.7;
    margin-right: 4px;
}
/* Collapsed by default + override via `.expanded` mirrors the
   tree-view children box (styles/tree.css) and the GameData inline
   group children (styles/panels-gamedata.css). Keeping the same
   idiom across all three collapse targets means the same `.expanded`
   class controls visibility everywhere in the details panel. */
.req-section-children { display: none; }
.req-section-children.expanded { display: block; }
.req-type-RequiredTextLines           { border-left-color: #2d6a4f; background: rgba(45, 106, 79, 0.06); }
.req-type-RequiredTextLinesThisRun    { border-left-color: #2d6a4f; background: rgba(45, 106, 79, 0.06); }
.req-type-RequiredTextLinesLastRun    { border-left-color: #2d6a4f; background: rgba(45, 106, 79, 0.06); }
.req-type-RequiredAnyTextLines        { border-left-color: #b07a3a; background: rgba(176, 122, 58, 0.06); }
.req-type-RequiredAnyOtherTextLines   { border-left-color: #b07a3a; background: rgba(176, 122, 58, 0.06); }
.req-type-RequiredAnyTextLinesThisRun { border-left-color: #b07a3a; background: rgba(176, 122, 58, 0.06); }
.req-type-RequiredAnyTextLinesLastRun { border-left-color: #b07a3a; background: rgba(176, 122, 58, 0.06); }
.req-type-RequiredFalseTextLines          { border-left-color: #a83a3a; background: rgba(168, 58, 58, 0.06); }
.req-type-RequiredFalseQueuedTextLines    { border-left-color: #a83a3a; background: rgba(168, 58, 58, 0.06); }
.req-type-RequiredFalseTextLinesThisRun   { border-left-color: #a83a3a; background: rgba(168, 58, 58, 0.06); }
.req-type-RequiredFalseTextLinesLastRun   { border-left-color: #a83a3a; background: rgba(168, 58, 58, 0.06); }
.req-type-RequiredAnyQueuedTextLines      { border-left-color: #b07a3a; background: rgba(176, 122, 58, 0.06); }
.req-type-RequiredMinAnyTextLines         { border-left-color: #8d7a4a; background: rgba(141, 122, 74, 0.06); }
.req-type-RequiredMaxAnyTextLines         { border-left-color: #8d7a4a; background: rgba(141, 122, 74, 0.06); }
.req-type-MinRunsSinceAnyTextLines        { border-left-color: #4a7882; background: rgba(74, 120, 130, 0.06); }
.req-type-MaxRunsSinceAnyTextLines        { border-left-color: #4a7882; background: rgba(74, 120, 130, 0.06); }
.req-type-other { border-left-color: #4a5a78; background: rgba(74, 90, 120, 0.06); }
.req-type-RequiredTextLines h4 { background: #1b4332; color: #95d5b2; }
.req-type-RequiredAnyTextLines h4 { background: #3d2b1f; color: #f4a261; }
.req-type-RequiredAnyOtherTextLines h4 { background: #3d2b1f; color: #e9c46a; }
.req-type-RequiredFalseTextLines h4 { background: #4a1520; color: #f07167; }
.req-type-RequiredFalseQueuedTextLines h4 { background: #4a1520; color: #f4845f; }
.req-type-RequiredFalseTextLinesThisRun h4 { background: #4a1520; color: #f4845f; }
.req-type-RequiredFalseTextLinesLastRun h4 { background: #4a1520; color: #f4845f; }
.req-type-RequiredTextLinesThisRun h4 { background: #1b4332; color: #95d5b2; }
.req-type-RequiredTextLinesLastRun h4 { background: #1b4332; color: #95d5b2; }
.req-type-RequiredAnyTextLinesThisRun h4 { background: #3d2b1f; color: #f4a261; }
.req-type-RequiredAnyTextLinesLastRun h4 { background: #3d2b1f; color: #f4a261; }
.req-type-RequiredAnyQueuedTextLines h4 { background: #3d2b1f; color: #f4a261; }
.req-type-RequiredMinAnyTextLines h4 { background: #2e2818; color: #d4b870; }
.req-type-RequiredMaxAnyTextLines h4 { background: #2e2818; color: #d4b870; }
.req-type-MinRunsSinceAnyTextLines h4 { background: #1e2e33; color: #7fc4d1; }
.req-type-MaxRunsSinceAnyTextLines h4 { background: #1e2e33; color: #7fc4d1; }
.req-type-other h4 { background: #2b2d42; color: #8d99ae; }
.req-item {
    padding: 3px 8px;
    margin: 2px 0;
    font-size: var(--fs-base);
    cursor: pointer;
    border-radius: 3px;
    transition: background 0.15s;
    display: flex;
    align-items: center;
}
/* Tier badge inside a requirement row - sits to the right so badges
   line up vertically down the requirements list, mirroring the tree
   view layout. */
.req-item .priority-badge { margin-left: auto; }
.req-item:hover { background: var(--border); }
.req-item.resolved { color: #a8dadc; }
.req-item.unresolved { color: var(--text-muted); font-style: italic; }
.req-item.unresolved::after { content: " \26A0  external"; font-size: var(--fs-2xs); color: var(--text-dim); }

/* Category-specific colors for unresolved refs - overrides the generic
   .unresolved color so the user can visually distinguish intentional
   (back-compat), buggy (typo-or-bug), missing (cut-content), and
   not-yet-triaged (uncategorized) references at a glance. */
.req-item.unresolved.unresolved-cat-back-compatibility {
    color: #c9a96e;
}
.req-item.unresolved.unresolved-cat-back-compatibility::after {
    content: " \1F501  back-compat";
    color: #c9a96e;
}
.req-item.unresolved.unresolved-cat-typo-or-bug {
    color: var(--danger);
}
.req-item.unresolved.unresolved-cat-typo-or-bug::after {
    content: " \26A0  typo/bug";
    color: var(--danger);
}
.req-item.unresolved.unresolved-cat-cut-content {
    color: var(--text-muted);
}
.req-item.unresolved.unresolved-cat-cut-content::after {
    content: " \2702  cut";
    color: var(--text-muted);
}
.req-item.unresolved.unresolved-cat-uncategorized {
    color: var(--danger);
}
.req-item.unresolved.unresolved-cat-uncategorized::after {
    content: " \26A0  uncategorized";
    color: var(--danger);
}

/* "Other requirements" list - non-textline gating fields rendered as
   monospace key=value lines (no navigation, no hover). */
.other-req-item {
    padding: 3px 8px;
    margin: 2px 0;
    font-size: var(--fs-sm);
    color: #8d99ae;
    font-family: 'Consolas', monospace;
}
/* Very light divider between consecutive rows so items that wrap
   across multiple lines stay visually distinguishable. Only applies
   from the second row onward; the first row has no separator above. */
.other-req-item + .other-req-item {
    border-top: 1px solid rgba(255, 255, 255, 0.05);
    padding-top: 5px;
}
/* Rows that carry a structured-form hover tooltip (every non-trivial
   ``otherRequirements`` row does) advertise the affordance with a
   ``cursor: help`` glyph. The tooltip itself is wired by the shared
   ``[data-tooltip]`` mechanism in ``tooltip.js`` / ``styles/panels-
   tooltips.css``. */
.other-req-item[data-tooltip] {
    cursor: help;
}
/* Path tail inside a resolved Path:<head> entry (e.g.
   ``CurrentRun.Hero.IsDead``). Already monospace via the parent, but
   nudged slightly to read as a quoted identifier. */
.other-req-item .other-req-path {
    color: #c9d1d9;
    background: rgba(255, 255, 255, 0.04);
    padding: 0 4px;
    border-radius: 3px;
}
/* AND joiner between multiple comparison/membership records that share
   a Path:<head> key (engine AND-combines them). */
.other-req-item .other-req-and {
    color: #6b7280;
    font-weight: 600;
    margin: 0 4px;
    letter-spacing: 0.5px;
}
/* Aggregation modifier clause appended to a Path record (e.g.
   ``(over the last 2 runs)``). Muted so the core predicate reads first. */
.other-req-item .other-req-mod {
    color: #8b949e;
    font-style: italic;
}
/* Function name inside a rendered FunctionName:<name> entry (e.g.
   ``RequiredAlive(Ids=[42]) = true``). Subtle tint to separate the
   identifier from the surrounding argument list. */
.other-req-item .other-req-func {
    color: #c9d1d9;
    font-weight: 600;
}

/* Alternative Requirement Groups (H2 ``OrRequirements``): a parent
   ``.req-section.req-type-or-group`` wraps one ``.or-branch`` per
   alternative. Distinctive amber accent so the reader can tell at a
   glance "any one of these branches is enough" vs the AND base block
   above ("all of these must hold"). The branch headers use ``<h5>``
   instead of ``<h4>`` to differentiate visually from the per-req-type
   sub-headers inside each branch. */
.req-type-or-group {
    border-left-color: #b07a3a;
    background: rgba(176, 122, 58, 0.05);
}
.req-type-or-group > h4 {
    background: #3d2b1f;
    color: #f4a261;
}
.or-branch {
    margin-top: 6px;
    padding: 4px 6px 4px 10px;
    border-left: 2px dashed rgba(176, 122, 58, 0.55);
    border-radius: 2px;
    background: rgba(176, 122, 58, 0.025);
}
.or-branch:first-child { margin-top: 2px; }
.or-branch-header {
    font-size: var(--fs-xs);
    margin: 0 0 4px;
    padding: 1px 6px;
    border-radius: 3px;
    display: inline-block;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    cursor: pointer;
    background: rgba(176, 122, 58, 0.15);
    color: #e9c46a;
}
.or-branch-header .toggle {
    display: inline-block;
    font-size: var(--fs-2xs);
    opacity: 0.7;
    margin-right: 4px;
}
.or-branch-children { display: none; }
.or-branch-children.expanded { display: block; }

/* NamedRequirements drill-in (H2): per-name expander rows inside an
   ``otherRequirements`` row keyed on ``NamedRequirements`` /
   ``NamedRequirementsFalse`` / ``NamedRequirementsCycle``. Each name
   becomes its own collapsible expander whose body re-renders the
   resolved inner requirement chain via the same machinery the host
   textline uses. A subtle teal accent distinguishes them from the
   amber OR-branch boxes (they share the "drill into a sub-set of
   requirements" idea but the semantics differ: NamedRequirements is
   an indirected reuse of a named gate, OrRequirements is a disjunctive
   composition). */
.named-req-item .named-req-label {
    font-weight: 600;
    color: #c9d1d9;
    margin-bottom: 8px;
}
.named-req-list {
    margin-left: 8px;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.named-req-flat,
.named-req-expand {
    padding: 2px 6px 2px 8px;
    border-left: 2px dashed rgba(80, 160, 160, 0.55);
    border-radius: 2px;
    background: rgba(80, 160, 160, 0.03);
}
.named-req-name {
    color: #c9d1d9;
    background: rgba(255, 255, 255, 0.04);
    padding: 0 4px;
    border-radius: 3px;
}
.named-req-suffix {
    color: #6b7280;
    font-size: var(--fs-xs);
    margin-left: 4px;
}
.named-req-header {
    font-size: var(--fs-xs);
    margin: 0 0 2px;
    padding: 1px 4px;
    border-radius: 3px;
    cursor: pointer;
    background: rgba(80, 160, 160, 0.10);
    color: #5dbfbf;
    display: inline-block;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.named-req-header:hover {
    background: rgba(80, 160, 160, 0.18);
}
.named-req-header .toggle {
    display: inline-block;
    font-size: var(--fs-2xs);
    opacity: 0.7;
    margin-right: 4px;
}
.named-req-children { display: none; padding: 4px 0 2px 4px; }
.named-req-children.expanded { display: block; }

/* --- panels-tooltips.css --- */

/* Tooltip affordances: every element that gets a [data-tooltip] popup
   (rendered by the floating layer in templates/viewer/tooltip.js) also
   gets a subtle dotted underline + help cursor so the user knows it is
   interactive. The friendly-name pattern is reused for speaker names,
   section names, and requirement-type labels. */

.meta .speaker-name[data-tooltip] {
    cursor: help;
    border-bottom: 1px dotted #88888866;
}
/* The dialogue detail panel's Owner/Partner names navigate to the speaker
   overview. Promote the click affordance above the generic tooltip hint:
   accent link colour, pointer cursor, and a trailing chevron so the name
   reads as a navigation link, not just a hover-for-info label. Scoped to
   ``.textline-info .meta`` so dialogue-line speakers, search rows, and the
   speaker overview keep their lighter treatment. Higher specificity than the
   ``[data-tooltip]`` rule above so it wins regardless of stylesheet order. */
.textline-info .meta .speaker-name.clickable {
    color: var(--accent-color);
    cursor: pointer;
    border-bottom-color: #e9456066;
}
.textline-info .meta .speaker-name.clickable::after {
    content: "\203a";
    margin-left: 3px;
    font-weight: 700;
    opacity: 0.75;
}
.textline-info .meta .speaker-name.clickable:hover {
    border-bottom-style: solid;
}
.textline-info .meta .speaker-name.clickable:hover::after {
    opacity: 1;
}
/* Matches the speaker-name affordance: only the friendly form gets the
   dotted underline + help cursor, so when no friendly mapping exists
   (raw key shown as-is) the section text looks plain. */
.meta .section-name[data-tooltip] {
    cursor: help;
    border-bottom: 1px dotted #88888866;
}
/* Search-dropdown affordances. ``renderSpeakerHtml`` and
   ``renderSectionHtml`` are both used inside ``.search-item`` so the
   owner pill and section pill get the same dotted-underline +
   help-cursor hint as in the details panel. ``.search-item`` matches
   both the name-match rows and the text-match rows (which also carry
   ``.search-item-text``). */
.search-item .speaker-name[data-tooltip] {
    cursor: help;
    border-bottom: 1px dotted #88888866;
}
.search-item .section-name[data-tooltip] {
    cursor: help;
    border-bottom: 1px dotted #88888866;
}

/* Requirement-type label affordance: dotted underline + help cursor when
   a friendly mapping exists (i.e. the tooltip will reveal the internal
   field name plus a plain-English blurb of what the check gates on).
   Mirrors the speaker-name and section-name patterns.
   The `.blocking-field` variant is bold to keep the visual weight the
   previous `<strong>` wrapper provided in the blocked-banner copy. */
.req-section h4 .req-type-name[data-tooltip] {
    cursor: help;
    border-bottom: 1px dotted currentColor;
    opacity: 0.9;
}
.blocked-banner .req-type-name.blocking-field {
    font-weight: bold;
}
.blocked-banner .req-type-name.blocking-field[data-tooltip] {
    cursor: help;
    border-bottom: 1px dotted #88888866;
}
.req-type-group-label[data-tooltip] {
    cursor: help;
    border-bottom: 1px dotted #88888866;
}

/* --- panels.css --- */

/* Panel layout chrome (split into panels-*.css files for finer scopes):
   - panels-badges.css         badge styles (play-once, self-ref, tree)
   - panels-banners.css        info-panel banners (unresolved, blocked)
   - panels-dialogue.css       dialogue line list
   - panels-gamedata.css       GameData group framing
   - panels-requirements.css   req-section / req-item / categories
   - panels-tooltips.css       [data-tooltip] affordances (dotted underlines)
*/

.panel {
    flex: 1;
    display: flex;
    flex-direction: column;
    border-right: 1px solid var(--border);
    min-width: 0;
    min-height: 0;
    overflow: hidden;
}
.panel:last-child { border-right: none; }
/* The header is rendered OUTSIDE the scroll container so scrolled
   content can't slide under it - no `position: sticky` needed - and it
   stays visible regardless of how far the body has been scrolled in
   either axis. */
.panel h2 {
    font-size: var(--fs-base);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 1px;
    background: var(--bg);
    padding: 16px 16px 12px;
    flex-shrink: 0;
    z-index: 10;
}
/* Scroll container for panel content. Scrolls on BOTH axes so:
   - tree rows that extend horizontally past the panel width don't wrap
     mid-word; the user scrolls right to follow deep chains.
   - long single-line content in any panel is reachable.
   Prose content (dialogue lines, requirement boxes) wraps normally
   because it has no `white-space: nowrap` rule. */
.panel-body {
    flex: 1;
    min-height: 0;
    overflow: auto;
    padding: 0 16px 16px;
}
.textline-info {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 12px;
    margin-bottom: 12px;
}
.textline-info h3 {
    color: var(--accent-color);
    font-size: var(--fs-lg);
    margin-bottom: 6px;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    row-gap: 6px;
    column-gap: 8px;
}
.textline-info h3 .name {
    word-break: break-all;
    min-width: 0;
    flex-shrink: 1;
}
/* Name + collision / cross-game badges, grouped so they stay together and
   grow to fill the row. The growth pushes the indicator cluster to the right
   when it fits on the same line; when the cluster wraps, this group fills the
   first row and the cluster lands left-aligned on the next. */
.textline-info h3 .h3-main {
    display: inline-flex;
    align-items: center;
    min-width: 0;
    flex: 1 1 auto;
}
/* The cross-game (game-switch) and collision badges beside the name match the
   indicator cluster's height and text size so the whole header row is uniform
   (the base badges are smaller and have no fixed height). */
.textline-info h3 .h3-main .cross-game-badge,
.textline-info h3 .h3-main .collision-badge {
    display: inline-flex;
    align-items: center;
    height: 20px;
    padding: 0 7px;
    font-size: var(--fs-sm);
    line-height: 1;
    box-sizing: border-box;
}
.textline-info .meta {
    font-size: var(--fs-sm);
    color: var(--text-muted);
    margin-bottom: 8px;
}
.textline-info .meta span { margin-right: 12px; }

/* Synthetic source banner: textlines and choices that were created by
   the merge step to materialise an implicit reference (e.g. a NPCData
   choice that points at an inline textline). The accent colour matches
   the `gamedata-group-*` purple to signal "synthesised, not authored". */
.synthetic-banner {
    background: #1b1b2e;
    border-left: 3px solid #6c5b7b;
    padding: 6px 10px;
    margin-top: 4px;
    margin-bottom: 8px;
    color: #c8b6ff !important;
    font-size: var(--fs-sm);
}
.synthetic-banner .choice-link {
    color: #b8a5ff;
    cursor: pointer;
    text-decoration: underline;
}
.synthetic-banner .choice-link:hover { color: #fff; }
.synthetic-banner code {
    background: #2a2a3e;
    padding: 1px 4px;
    border-radius: 2px;
    color: #e7d3a1;
}

/* Alternates / Mutually exclusive section */
.alternates-section {
    margin-top: 1.2rem;
    padding-top: 0.8rem;
    border-top: 1px solid #2a3a40;
}

.alternates-header {
    margin: 0 0 0.2rem 0;
    font-size: var(--fs-lg);
    color: #c4a56e;
}

.alternates-hint {
    font-size: var(--fs-sm);
    color: #7a8a8a;
    margin-bottom: 0.5rem;
}

.alternates-list {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.alternates-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.3rem 0.5rem;
    border-radius: 4px;
    cursor: pointer;
    background: #1a2228;
    border: 1px solid #2a3a40;
}

.alternates-item:hover {
    background: #243038;
    border-color: #3a5060;
}

.alternates-name {
    font-family: 'Consolas', monospace;
    font-size: var(--fs-base);
    color: var(--text-color);
}

.alternates-owner {
    font-size: var(--fs-sm);
    opacity: 0.6;
}

/* --- save-progress.css --- */

/* Save file upload button + status indicator in the header */

.save-upload {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-shrink: 0;
}

.save-upload-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 6px 14px;
    border-radius: 4px;
    background: var(--bg);
    border: 1px solid var(--border);
    color: var(--text-color);
    cursor: pointer;
    font-size: var(--fs-base);
    line-height: 18px;
    transition: background-color 0.12s ease, color 0.12s ease;
    white-space: nowrap;
}

.save-upload-btn:hover {
    background: var(--border);
}

.save-upload-btn:focus-visible {
    outline: 2px solid var(--accent-color);
    outline-offset: -2px;
}

.save-upload-icon {
    font-size: var(--fs-base);
}

.save-status {
    font-size: var(--fs-base);
    line-height: 18px;
    padding: 6px 10px;
    border-radius: 4px;
    white-space: nowrap;
}

.save-status.save-loaded {
    background: var(--bg);
    color: #8fbf8f;
    border: 1px solid var(--border);
}

.save-status.save-error {
    background: #3a1a1a;
    color: var(--danger);
    border: 1px solid #5a2a2a;
}

.save-status.save-mismatch {
    background: var(--bg);
    color: #c4b47e;
    border: 1px solid var(--border);
}

/* Progress badges on textlines in tree views */

.save-badge {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 0.3rem;
    flex-shrink: 0;
}

.save-badge.played {
    background: #4caf50;
}

.save-badge.eligible {
    background: #ffb74d;
}

.save-badge.blocked {
    background: #78909c;
}

.save-badge.unobtainable {
    background: var(--danger);
}

.save-badge.indeterminate {
    background: #8b95d6;
}

/* A line played in the save that still doesn't satisfy the specific
   run-scoped / run-count requirement it sits under (e.g. played, but not in
   the last run). Matches the eligibility tracer's near-miss highlight. */
.save-badge.near-miss {
    background: #4db6ac;
}

/* Detail panel save progress indicator */

.save-progress-pill {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.15rem 0.5rem;
    border-radius: 3px;
    font-size: var(--fs-sm);
    font-weight: 500;
    margin-left: 0.5rem;
    vertical-align: middle;
}

.save-progress-pill.played {
    background: #1a3a1a;
    color: #6be06b;
    border: 1px solid #2a5a2a;
}

.save-progress-pill.eligible {
    background: #3a2f1a;
    color: #ffb74d;
    border: 1px solid #5a4a2a;
}

.save-progress-pill.blocked {
    background: #1e2a30;
    color: #90a4ae;
    border: 1px solid #2a3a40;
}

.save-progress-pill.unobtainable {
    background: #2e1a1a;
    color: var(--danger);
    border: 1px solid #4a2a2a;
}

.save-progress-pill.indeterminate {
    background: #222640;
    color: #aab2e6;
    border: 1px solid #3a3f6a;
}

.save-progress-pill.clickable {
    cursor: pointer;
    text-decoration: none;
}

.save-progress-pill.clickable:hover {
    background: #263a44;
    border-color: #3a5060;
}

/* Eligibility-tracer entry button in the dialogue detail view (the
   discoverable counterpart to the clickable status pill above). Matches
   the header nav buttons' background so it reads as a peer control,
   without a loud call-to-action colour. */
.trace-eligibility-row {
    margin: 8px 0 2px;
}
.trace-eligibility-btn {
    display: inline-block;
    padding: 5px 12px;
    background: var(--bg);
    color: var(--text-color);
    border: 1px solid var(--border);
    border-radius: 4px;
    font-size: var(--fs-base);
    cursor: pointer;
    text-decoration: none;
}
.trace-eligibility-btn:hover {
    background: var(--border);
}

/* --- speaker-overview.css --- */

/* Speaker overview view.
 *
 * Hosted by ``#info-content`` (same mount the dialogue detail view
 * uses). When ``view=speaker`` is active, ``navigation.js`` adds the
 * ``layout-speaker`` class to ``<body>`` so the upstream + downstream
 * panels hide and ``#panel-info`` expands to full width.
 */

body.layout-speaker #panel-upstream,
body.layout-speaker #panel-downstream {
    display: none;
}
body.layout-speaker #panel-info {
    flex: 1;
    border-right: none;
}

/* Speaker name spans are clickable everywhere they appear (info-panel
 * Owner/Partner, dialogue line speakers, tree owner tags, ...). The
 * dotted-underline affordance from ``[data-tooltip]`` already hints at
 * "more info on hover"; the clickable class adds a pointer cursor and
 * a hover colour so the click affordance is discoverable too. */
.speaker-name.clickable,
.npc-tag.clickable {
    cursor: pointer;
}
.speaker-name.clickable:hover,
.npc-tag.clickable:hover {
    color: var(--accent-color);
}

.speaker-overview {
    padding: 4px 0 16px;
}
/* This view's title bar is a <header>, so it inherits the global page
   ``header`` rule (display:flex; align-items:center) which vertically
   mis-centres the differently-sized lines. Override to a 3-column grid
   matching the summary row below: the title block spans the first two
   columns and the right-hand aside occupies the third, so the aside lines
   up exactly with the summary's rightmost (Sections) column. */
.speaker-overview-header {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    align-items: start;
    gap: 16px;
    padding: 12px;
    border: 1px solid var(--border);
    border-radius: 6px 6px 0 0;
}
.speaker-overview-header-main {
    grid-column: 1 / 3;
    min-width: 0;
}
.speaker-overview-header-aside {
    grid-column: 3;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
}
.speaker-overview-header h3 {
    color: var(--accent-color);
    font-size: var(--fs-2xl);
    margin-bottom: 4px;
}
.speaker-overview-header .speaker-id {
    color: var(--text-muted);
    font-size: var(--fs-base);
    font-weight: normal;
}
.speaker-overview-header .speaker-description {
    color: #b8b8b8;
    font-size: var(--fs-base);
    font-style: italic;
    margin-bottom: 4px;
}
/* Similar-speaker pills: sit at the aside's left edge, which aligns with
   the summary's right column below. Rendered as clickable pill buttons so
   the navigation affordance is obvious. */
.speaker-similar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}
.speaker-similar-label {
    color: var(--text-muted);
    text-transform: uppercase;
    font-size: var(--fs-2xs);
    letter-spacing: 0.5px;
}
.speaker-similar-pill {
    display: inline-block;
    background: var(--border);
    color: #d8dbe6;
    border: 1px solid #1c4584;
    border-radius: 12px;
    padding: 3px 10px;
    font-size: var(--fs-sm);
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.speaker-similar-pill:hover {
    background: #1c4584;
    color: #fff;
    border-color: #2a5aa8;
}
.speaker-overview-header .speaker-ids {
    color: var(--text-muted);
    font-size: var(--fs-sm);
    margin-bottom: 4px;
    line-height: 1.6;
}
.speaker-overview-header .speaker-ids code {
    background: var(--border);
    color: #b8b8b8;
    padding: 1px 5px;
    border-radius: 3px;
    font-size: var(--fs-xs);
}
.speaker-overview-missing {
    padding: 16px;
}
.speaker-overview-missing h3 {
    color: var(--accent-color);
    margin-bottom: 8px;
}
.speaker-overview-missing code {
    background: var(--border);
    padding: 1px 4px;
    border-radius: 3px;
}

/* Summary section: a single 3-column row (owned / as-speaker / sections)
 * on the same grid as the header so columns line up. */
.speaker-summary {
    background: var(--surface);
    border: 1px solid var(--border);
    border-top: none;
    border-radius: 0 0 6px 6px;
    padding: 12px;
    margin-bottom: 12px;
}
.speaker-summary-row {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 16px;
}
/* Four columns when the save-progress cell is present, so it sits on the
   same aligned row as owned / as-speaker / sections instead of wrapping. */
.speaker-summary-row-4 {
    grid-template-columns: repeat(4, minmax(0, 1fr));
}
.speaker-summary-row + .speaker-summary-row {
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
}
.speaker-summary-cell {
    min-width: 0;
}
.speaker-summary-cell h4 {
    font-size: var(--fs-2xs);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 4px;
}
.speaker-summary-value {
    font-size: var(--fs-3xl);
    color: var(--text-color);
}
.speaker-summary-note {
    font-size: var(--fs-xs);
    color: var(--text-dim);
    font-style: italic;
}
.speaker-section-list {
    list-style: none;
    padding: 0;
    margin: 0;
    font-size: var(--fs-sm);
}
.speaker-section-list li {
    padding: 2px 0;
}

.speaker-priority-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}
.priority-chip {
    background: var(--border);
    color: #b8b8b8;
    border: 1px solid #1c4584;
    border-radius: 12px;
    padding: 4px 10px;
    font-size: var(--fs-sm);
    cursor: pointer;
    font-family: inherit;
}
.priority-chip:hover {
    background: #1c4584;
    color: var(--text-color);
}
.priority-chip.is-active {
    background: var(--accent-color);
    color: #fff;
    border-color: var(--accent-color);
}
/* A filter/eligibility chip whose selection would yield no dialogues:
   greyed out and unclickable (the button also carries ``disabled``). */
.priority-chip.is-disabled,
.priority-chip:disabled {
    background: #11223d;
    color: #5a6478;
    border-color: #173050;
    cursor: not-allowed;
    opacity: 0.55;
}
.priority-chip.is-disabled:hover,
.priority-chip:disabled:hover {
    background: #11223d;
    color: #5a6478;
}
.priority-chip .speaker-count {
    color: inherit;
}

/* Eligibility filter chips reuse the priority-chip shape; a leading
   status dot echoes the per-row save badge so the four save states are
   scannable at a glance. The dot colours mirror ``.save-badge`` in
   save-progress.css. */
.eligibility-chip-played::before,
.eligibility-chip-eligible::before,
.eligibility-chip-blocked::before,
.eligibility-chip-indeterminate::before,
.eligibility-chip-unobtainable::before {
    content: "";
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 5px;
}
.eligibility-chip-played::before { background: #4caf50; }
.eligibility-chip-eligible::before { background: #ffb74d; }
.eligibility-chip-blocked::before { background: #78909c; }
.eligibility-chip-indeterminate::before { background: #8b95d6; }
.eligibility-chip-unobtainable::before { background: var(--danger); }

/* Save-progress summary cell: the per-status counts list reuses the
   section-list rhythm; each row leads with the same coloured dot. */
.speaker-summary-eligibility .save-badge {
    margin-right: 5px;
}
.speaker-count {
    color: var(--text-muted);
    font-size: var(--fs-xs);
    margin-left: 4px;
}

/* Cross-speaker adjacency: two columns side by side. */
.speaker-adjacency {
    display: flex;
    gap: 12px;
    margin-bottom: 12px;
}
.speaker-adjacency-col {
    flex: 1;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 10px 12px;
    min-width: 0;
}
.speaker-adjacency-col h4 {
    font-size: var(--fs-xs);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin: 0;
    display: flex;
    align-items: center;
    gap: 5px;
}
.speaker-adjacency-dir { color: var(--accent-color); font-size: var(--fs-base); line-height: 1; }
.speaker-adjacency-head { margin-bottom: 6px; }
.speaker-adjacency-sub {
    font-size: var(--fs-xs);
    color: var(--text-dim);
    margin: 2px 0 0;
}
.speaker-adjacency-list {
    list-style: none;
    padding: 0;
    margin: 0;
    font-size: var(--fs-base);
    max-height: 360px;
    overflow-y: auto;
}
.speaker-adjacency-row {
    display: flex;
    align-items: baseline;
    gap: 7px;
    padding: 4px 4px;
    cursor: pointer;
    border-radius: 4px;
}
.speaker-adjacency-row:hover { background: #1c2c4d; }
/* Fixed-width name+id cell so every count chip lines up in a column
   (mirrors the duplicates view's --dup-name-col trick). */
.speaker-adjacency-nameid {
    width: var(--adj-name-col, auto);
    flex-shrink: 0;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.speaker-adjacency-chevron {
    font-size: 0.65em;
    color: var(--text-dim);
    transition: transform 0.15s;
    flex-shrink: 0;
}
.speaker-adjacency-item.expanded > .speaker-adjacency-row .speaker-adjacency-chevron {
    transform: rotate(90deg);
}
/* Count chip sits right after the speaker name (no eye-travel to a far
   right edge) and reads as the dependency tally. */
.speaker-adjacency-count {
    flex-shrink: 0;
    font-size: var(--fs-sm);
    font-weight: 600;
    color: #dfe2f0;
    background: var(--border);
    border: 1px solid #1c4584;
    border-radius: 10px;
    padding: 0 8px;
    min-width: 22px;
    text-align: center;
}
.speaker-adjacency-item:not(.expanded) > .speaker-adjacency-detail { display: none; }
.speaker-adjacency-detail {
    list-style: none;
    margin: 1px 0 6px 18px;
    padding: 4px 0 4px 10px;
    border-left: 1px solid #1c4584;
    font-size: var(--fs-sm);
}
/* Each link is shown stacked: the depending dialogue (emphasised) above
   the line(s) it ``requires`` (indented + muted), so "what depends on
   what" reads clearly even with long names. */
.speaker-adjacency-detail-row {
    padding: 4px 0;
    border-bottom: 1px solid #0f346066;
}
.speaker-adjacency-detail-row:last-child { border-bottom: none; }
.speaker-adjacency-dep {
    display: inline-block;
    font-weight: 600;
    font-size: var(--fs-sm);
    color: #cdcde0;
}
.speaker-adjacency-dep:hover { color: var(--accent-color); }
.speaker-adjacency-reqs {
    margin-left: 16px;
    margin-top: 1px;
    color: #9a9ab0;
    line-height: 1.45;
}
.speaker-adjacency-reqs .textline-link { font-size: var(--fs-sm); color: #aab0c8; }
.speaker-adjacency-reqs .textline-link:hover { color: var(--accent-color); }
.speaker-adjacency-req-label {
    color: #6a6a82;
    font-style: italic;
    margin-right: 4px;
}
.speaker-adjacency-sep { color: #555; }
.speaker-adjacency-detail-empty { color: var(--text-dim); font-style: italic; padding: 2px 0; }
.speaker-adjacency-self {
    font-size: var(--fs-2xs);
    color: #c4a56e;
    border: 1px solid #4a4636;
    border-radius: 3px;
    padding: 0 4px;
    text-transform: uppercase;
    letter-spacing: 0.3px;
    flex-shrink: 0;
}
.speaker-adjacency-name {
    color: #b8b8b8;
}
.speaker-id-inline {
    color: var(--text-dim);
    font-size: var(--fs-xs);
}

/* Textline list (the main body): controls strip + grouped sections. */
.speaker-textlines {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 10px 12px;
}
.speaker-textline-controls {
    display: flex;
    gap: 8px;
    align-items: center;
    padding-bottom: 8px;
    margin-bottom: 8px;
    border-bottom: 1px solid var(--border);
    flex-wrap: wrap;
}
.speaker-control-label {
    font-size: var(--fs-xs);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}
.speaker-textlines-empty {
    color: var(--text-dim);
    font-style: italic;
    padding: 8px 0;
}
.speaker-textline-group + .speaker-textline-group {
    margin-top: 12px;
    padding-top: 8px;
    border-top: 1px solid var(--border);
}
.speaker-textline-group-header {
    font-size: var(--fs-xs);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 4px;
    display: flex;
    align-items: center;
    gap: 6px;
    cursor: pointer;
    user-select: none;
}
.speaker-textline-group-header:hover { color: #b8b8b8; }
.speaker-group-chevron {
    font-size: 0.7em;
    color: var(--text-dim);
    transition: transform 0.15s;
    display: inline-block;
}
.speaker-textline-group.expanded > .speaker-textline-group-header .speaker-group-chevron {
    transform: rotate(90deg);
}
.speaker-textline-group:not(.expanded) > .speaker-textline-list {
    display: none;
}
.speaker-textline-list {
    list-style: none;
    padding: 0;
    margin: 0;
}
.speaker-textline-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 5px 0;
    border-bottom: 1px solid #0f346040;
}
.speaker-textline-row:last-child { border-bottom: none; }
/* Mutually-exclusive alternate variants, clustered into one labelled box
   within a section (gold accent shared with the dialogue detail view). */
.speaker-alt-group {
    list-style: none;
    margin: 3px 0;
    padding: 1px 8px 2px;
    border: 1px solid #4a4a5a;
    border-radius: 4px;
    background: #1e1e28;
}
.speaker-alt-group-label {
    font-size: var(--fs-2xs);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: #c4a56e;
    padding: 2px 0;
}
.speaker-alt-group-rows {
    list-style: none;
    padding: 0;
    margin: 0;
}
.textline-link {
    color: #b8b8b8;
    cursor: pointer;
    font-size: var(--fs-base);
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.textline-link:hover { color: var(--accent-color); }
/* The dialogue name is the row's primary element: brighter and heavier
   than the badges that trail it. */
.speaker-textline-row > .textline-link {
    color: #c8c8c8;
    font-weight: 500;
    font-size: var(--fs-base);
}
.speaker-textline-badges {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}
/* Normalise the priority (#N/M or tier) and Play-once/Repeatable pills
   to one shared size with vertical centring, so they read as a matched
   pair and stay subordinate to the dialogue name. */
.speaker-textline-badges .priority-badge,
.speaker-textline-badges .play-once-badge {
    display: inline-flex;
    align-items: center;
    line-height: 1;
    font-size: var(--fs-sm);
    padding: 3px 7px;
    border-radius: 4px;
}

.muted {
    color: var(--text-dim);
    font-style: italic;
}

/* Search-result rows for the speaker section: matches the existing
 * search-item rhythm; the npc suffix shows the internal id. */
.search-item-speaker {
    font-weight: 500;
}
.search-item-speaker .npc {
    margin-left: 6px;
}

/* --- tooltip.css --- */

/* Custom mouse-following tooltip element.
   Hosts the floating popup rendered by the JS layer in
   ``templates/viewer/tooltip.js`` for any element carrying a
   ``data-tooltip`` attribute. One element appended to
   ``<body>`` at boot, repositioned via inline ``style.left``/``top``
   to follow the cursor, toggled via the ``.visible`` class.

   Colour palette intentionally echoes the dark info-panel cards
   (``var(--surface)`` background + ``var(--border)`` border) so it reads as part
   of the viewer, while the slightly darker background, drop shadow,
   and small offset keep it distinguishable from the underlying
   surface. */
#custom-tooltip {
    position: fixed;
    pointer-events: none;
    top: 0;
    left: 0;
    z-index: 10000;
    max-width: 420px;
    padding: 6px 10px;
    font-family: 'Segoe UI', system-ui, sans-serif;
    font-size: var(--fs-sm);
    line-height: 1.5;
    color: var(--text-color);
    background: #0e1530;
    border: 1px solid var(--border);
    border-radius: 4px;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.55);
    /* ``pre-line`` preserves explicit ``\n`` from the source string
       and otherwise wraps long single lines at the max-width. */
    white-space: pre-line;
    /* Visible-by-default state is suppressed via ``visibility`` so
       the element keeps a measurable bounding box for the JS layer
       to read ``offsetWidth``/``offsetHeight`` against. */
    visibility: hidden;
    opacity: 0;
    transition: opacity 0.08s linear;
}
#custom-tooltip.visible {
    visibility: visible;
    opacity: 1;
}

/* --- tour.css --- */

/* Guided onboarding tour: spotlight + coachmark card.
 *
 * Driven by templates/viewer/tour.js (positions are set inline from JS).
 * Three stacked layers above everything else:
 *   .tour-overlay   - transparent full-screen click-blocker (keeps the page
 *                     non-interactive so the user follows the tour)
 *   .tour-spotlight - the dim: a transparent box over the highlighted target
 *                     with a huge spreading box-shadow that darkens the rest
 *                     of the screen, leaving the target visible through the
 *                     cut-out. JS sets its position/size; a 0-size centred
 *                     spotlight dims the whole screen for a no-target step.
 *   .tour-card      - the coachmark (title, body, Back/Next/Skip + opt-out)
 *
 * Self-contained (base + its own mobile @media); no other file styles these.
 */

.tour-overlay {
    position: fixed;
    inset: 0;
    z-index: 100000;
    background: transparent;
    /* Catch every click so the page behind can't be interacted with mid-tour
       (a stray click could navigate away and strand the tour's targets).
       Interactive steps flip this to ``pointer-events: none`` from JS so the
       user can reach the highlighted target (e.g. to hover a badge). */
    cursor: default;
}

/* While a tour is open, the hover tooltip must sit above the tour layers so
   interactive steps (hover a badge / the dotted internal-id text) can show it;
   its default z-index is far below the overlay. */
body.tour-open #custom-tooltip {
    z-index: 100003;
}

.tour-spotlight {
    position: fixed;
    z-index: 100001;
    border-radius: 6px;
    /* The dim. The spread radius is larger than any viewport so the shadow
       always reaches every edge regardless of where the cut-out sits. */
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.6);
    /* Visual highlight ring around the target. */
    outline: 2px solid var(--accent-color);
    outline-offset: 2px;
    pointer-events: none;
    transition: top 0.18s ease, left 0.18s ease, width 0.18s ease, height 0.18s ease;
}

/* No-target steps centre a zero-size cut-out: the dim (box-shadow) still
   covers the screen, but the highlight ring would show as a tiny square, so
   drop it. */
.tour-spotlight-untargeted {
    outline: none;
}

.tour-card {
    position: fixed;
    z-index: 100002;
    box-sizing: border-box;
    width: max-content;
    max-width: min(340px, calc(100vw - 16px));
    background: var(--surface);
    border: 1px solid var(--accent-color);
    border-radius: 8px;
    padding: 14px 16px;
    color: var(--text-color);
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5);
    font-size: var(--fs-base);
    line-height: 1.45;
}

.tour-card-title {
    color: var(--accent-color);
    font-size: var(--fs-lg);
    margin: 0 0 6px;
}

.tour-card-text {
    margin: 0;
    color: #d0d4dd;
}

.tour-card-footer {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 14px;
}

.tour-card-step {
    font-size: var(--fs-xs);
    color: #8a93a6;
    /* Push the buttons to the right of the step counter. */
    margin-right: auto;
}

.tour-btn {
    appearance: none;
    font-family: inherit;
    font-size: var(--fs-sm);
    padding: 6px 14px;
    border-radius: 4px;
    border: 1px solid var(--border);
    background: var(--bg);
    color: var(--text-color);
    cursor: pointer;
    transition: background-color 0.12s ease, border-color 0.12s ease;
}

.tour-btn:hover {
    background: var(--border);
}

.tour-btn-next {
    background: var(--accent-color);
    border-color: var(--accent-color);
    color: #fff;
}

.tour-btn-next:hover {
    background: #d13551;
    border-color: #d13551;
}

.tour-btn:disabled {
    opacity: 0.4;
    cursor: default;
}

/* The "Don't show tutorials again" opt-out: a quiet text link under the
   action row so it never competes with Next/Skip. */
.tour-disable {
    appearance: none;
    background: none;
    border: none;
    padding: 8px 0 0;
    margin: 0;
    color: #8a93a6;
    font-family: inherit;
    font-size: var(--fs-xs);
    text-decoration: underline;
    cursor: pointer;
}

.tour-disable:hover {
    color: #c0c6d2;
}

/* Replay / help control: a small fixed "?" button (re-runs the onboarding
   tours after they've been seen or disabled). Floating rather than in the
   header so it doesn't disturb the header's packed, responsive layout. Sits
   below the tour layers but above page content. */
.tour-help-btn {
    position: fixed;
    bottom: calc(14px + env(safe-area-inset-bottom));
    right: calc(14px + env(safe-area-inset-right));
    z-index: 90;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 1px solid var(--border);
    background: var(--surface);
    color: #c0c6d2;
    font-family: inherit;
    font-size: var(--fs-lg);
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
    transition: background-color 0.12s ease, color 0.12s ease;
}

.tour-help-btn:hover {
    background: var(--border);
    color: #fff;
}

/* Secondary emphasis: a ring around small repeated elements (e.g. every status
   dot) within a step's spotlit area, to point at all of them at once. ``outline``
   follows the element's border-radius (so round dots get a round ring) and
   ``outline-offset`` leaves a small gap between the dot and the ring; a gentle
   pulse on that gap, plus a soft glow, draws the eye. Applied/removed per step
   by the engine. */
.tour-emphasis {
    outline: 2px solid var(--accent-color);
    outline-offset: 2px;
    box-shadow: 0 0 7px 2px rgba(233, 69, 96, 0.55) !important;
    animation: tour-emphasis-pulse 1.6s ease-in-out infinite;
}

@keyframes tour-emphasis-pulse {
    0%, 100% { outline-offset: 2px; }
    50% { outline-offset: 4px; }
}

/* Phone: the card becomes a bottom sheet so it never fights the small
   viewport or overlaps the highlighted target (JS leaves its top/left unset
   on phones and scrolls the target into the upper area). */
@media (max-width: 599px) {
    .tour-card {
        left: calc(8px + env(safe-area-inset-left));
        right: calc(8px + env(safe-area-inset-right));
        bottom: calc(18px + env(safe-area-inset-bottom));
        top: auto !important;
        width: auto;
        max-width: none;
    }
    /* Pinned to the top instead: used when the highlighted target sits at the
       page bottom and can't be scrolled clear of a bottom sheet, so the card
       moves above it. */
    .tour-card.tour-card-pinned-top {
        top: calc(18px + env(safe-area-inset-top)) !important;
        bottom: auto;
    }
    /* Touch-sized controls. */
    .tour-btn {
        min-height: 38px;
        padding-left: 26px;
        padding-right: 26px;
    }
    .tour-help-btn {
        width: var(--tap-min);
        height: var(--tap-min);
    }
}

/* --- tree.css --- */

/* Tree view nodes, edges, labels */
.tree-node {
    margin-left: 16px;
    border-left: 1px solid var(--border);
    padding-left: 12px;
}
.tree-node.root { margin-left: 0; border-left: none; padding-left: 0; }

/* Right-edge alignment strategy.
   The visible panel-body has `overflow: auto`; we want a horizontal
   scrollbar to appear when deep nesting or long textline names push
   content past the panel's visible width, AND every box at every depth
   to share the same right edge so framing stays visually consistent
   regardless of which branch is expanded.

   Default block layout already gives us right-edge alignment for free:
   a `width: auto` block element fills its parent's content box minus
   its own margins, so `child.right === parent.right` recursively. We
   just need ONE element to drive the intrinsic width:

     - `.tree-node.root` is sized to `max-content` so the entire tree
       grows as wide as the widest descendant's natural content.
     - `min-width: 100%` keeps it at least as wide as the panel-body
       so narrow trees don't render with empty space on the right.

   Inner containers (`.tree-node`, `.tree-children`, `.req-type-group`,
   `.gamedata-group`, ...) intentionally have no explicit width: their
   default `width: auto` makes them fill their parent's content box,
   which means every box's right edge matches the root's right edge.
   `.tree-label` itself is `width: max-content; min-width: 100%;` so
   its natural text width propagates up to the root's max-content
   computation while it still stretches to fill its container row. */
.tree-node.root {
    width: max-content;
    min-width: 100%;
}
.tree-label {
    padding: 4px 8px;
    margin: 2px 0;
    font-size: var(--fs-base);
    cursor: pointer;
    border-radius: 3px;
    display: flex;
    align-items: center;
    gap: 6px;
    /* Keep each row on a single line; the panel-body scrolls horizontally
       so long names extend right rather than wrapping mid-word.
       `white-space` is inheritable so descendants (`.name`, `.edge-type`,
       `.npc-tag`, ...) all stay single-line too without per-element rules.
       `width: max-content` makes the flex item as wide as its content so
       the parent `.tree-node` / `.panel-body` see the overflow and
       horizontal scroll kicks in. */
    white-space: nowrap;
    width: max-content;
    min-width: 100%;
}
.tree-label:hover { background: var(--border); }
.tree-label.active { background: var(--border); color: var(--accent-color); }
.tree-label:focus-visible {
    outline: 2px solid var(--accent-color);
    outline-offset: -2px;
}
.tree-label .toggle {
    /* Generous click target so the chevron isn't a pixel-precision
       target. The arrow stays centered horizontally via `text-align`
       and vertically via the parent's `align-items: center`. Bumping
       padding here only is fine because the `min-width` reservation
       keeps the toggle column a consistent width even on leaf rows,
       which render an empty toggle (no chevron). */
    min-width: 24px;
    padding: 2px 4px;
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-xs);
    flex-shrink: 0;
}
.tree-label .edge-type {
    font-size: var(--fs-2xs);
    padding: 1px 4px;
    border-radius: 2px;
    flex-shrink: 0;
}
.edge-all { background: #1b4332; color: #95d5b2; }
.edge-any { background: #3d2b1f; color: #f4a261; }
.edge-false { background: #4a1520; color: #f07167; }
/* `.tree-label .name` previously had `word-break: break-all` to force
   long textline names to wrap mid-word. That was dropped: rows stay on a
   single line (`.tree-label` is `white-space: nowrap`) and the
   `.panel-body` scrolls horizontally so deep chains push content right. */
.tree-label .npc-tag { font-size: var(--fs-2xs); color: var(--text-dim); margin-left: auto; flex-shrink: 0; }

/* Narrative-priority badge. Same compact pill style as the
   other inline tree badges (`.edge-type`, `.tree-blocked-badge`); three
   tone variants for super- / plain-priority / low-priority so each
   cascade tier reads at a glance.
   Tree placement: positioned in the right-aligned cluster just before
   `.npc-tag`. `margin-left: auto` on the badge consumes the free space
   so badges line up at the right edge across rows; the sibling rule
   below then zeros out the npc-tag's own auto margin (which only kicks
   in when no badge is present). */
.priority-badge {
    font-size: var(--fs-2xs);
    padding: 1px 5px;
    border-radius: 2px;
    flex-shrink: 0;
    font-weight: 500;
}
.tree-label .priority-badge { margin-left: auto; }
.tree-label .priority-badge ~ .npc-tag { margin-left: 8px; }
.priority-super { background: #3b2a0a; color: #ffd166; border: 1px solid #6b4e1f; }
.priority-priority { background: #2a2438; color: #b8a5ff; border: 1px solid #4a3a6b; }
.priority-normal { background: #1c2530; color: #8fb3d1; border: 1px solid #2c4258; }
.priority-low { background: #20242a; color: #8a98a8; border: 1px solid #3a4350; }
/* H2 ordinal-rank badge ("#N/M"). Neutral monospace styling - the
   ordinal/size pair is purely positional information, so a tinted
   colour would suggest a qualitative tier that the H2 data does not
   express. The badge sits in the same slot as the H1 tier badge so
   they share placement rules. */
.priority-ordinal {
    background: #1d242c;
    color: #c7d2dc;
    border: 1px solid #303a47;
    font-family: 'Consolas', monospace;
    font-variant-numeric: tabular-nums;
}

/* Set-level priority badge (SP / P). Surfaces the per-textline-set
   `Priority` / `SuperPriority` boolean as a small standalone pill to
   the right of the tier badge; the two signals are independent (the
   set-level boolean only biases random selection within a section)
   so they get their own marker rather than collapsing together. */
.set-priority-badge {
    font-size: var(--fs-2xs);
    padding: 1px 5px;
    border-radius: 2px;
    flex-shrink: 0;
    font-weight: 600;
    letter-spacing: 0.5px;
}
.set-priority-super { background: #3b2a0a; color: #ffd166; border: 1px solid #6b4e1f; }
.set-priority-priority { background: #2a2438; color: #b8a5ff; border: 1px solid #4a3a6b; }
/* Details-panel header: the priority, save and PlayOnce/Repeatable
   badges sit in one right-aligned cluster. Pinning the whole cluster
   (rather than putting `margin-left: auto` on the priority badge)
   keeps the always-present repeatability badge at the right edge even
   when the priority badge is absent - e.g. an H2 textline with no
   narrative rank renders no ordinal badge, which used to leave the
   repeatability badge with nothing to push it right, so it jumped to
   the left. The Repeatable/PlayOnce badge is the cluster's last child
   so it stays pinned to the edge regardless of which siblings render. */
.textline-info h3 .h3-indicators {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
}
.textline-info h3 .h3-indicators > * {
    margin-left: 0;
    flex-shrink: 0;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    height: 20px;
    padding: 0 7px;
    font-size: var(--fs-sm);
    line-height: 1;
    border-radius: 3px;
}
.tree-children { display: none; }
.tree-children.expanded { display: block; }
.cycle-marker { color: var(--accent-color); font-size: var(--fs-xs); font-style: italic; }

/* GameData group boxes wrap contiguous tree children that all came from
   the same `GameData.X` list expansion. Collapsible via header click. */
.gamedata-group {
    margin: 4px 0 4px 16px;
    border: 1px dashed #4a3a6b;
    border-radius: 4px;
    background: rgba(108, 91, 123, 0.08);
    padding: 2px 4px 4px 4px;
}
.gamedata-group-header {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 6px;
    font-size: var(--fs-xs);
    cursor: pointer;
    color: #c8b6ff;
    border-radius: 3px;
}
.gamedata-group-header:hover { background: rgba(108, 91, 123, 0.18); }
.gamedata-group-header .toggle {
    width: 12px;
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-2xs);
    flex-shrink: 0;
}
.gamedata-group-label {
    font-family: 'Consolas', monospace;
    font-weight: 600;
    color: #b8a5ff;
}
.gamedata-group-count {
    font-size: var(--fs-2xs);
    color: var(--text-muted);
    margin-left: auto;
}
.gamedata-group-children { display: none; }
.gamedata-group-children.expanded { display: block; }
/* Children inside a group lose their own left border + indent so the
   group box itself frames them visually. */
.gamedata-group-children .tree-node {
    margin-left: 8px;
    border-left: 1px dotted #4a3a6b;
    padding-left: 8px;
}

/* Requirement-type group boxes wrap contiguous tree children that all
   share the same requirement type (RequiredTextLines, RequiredAnyTextLines,
   etc.). Mirrors the per-type coloured boxes in the info panel — the
   actual border-left colour and tinted background come from the global
   `.req-type-*` classes defined in panels.css, so both views stay in
   sync. Collapsible via header click. */
.req-type-group {
    margin: 4px 0 4px 16px;
    border-left: 3px solid #555;
    border-radius: 3px;
    background: rgba(255, 255, 255, 0.025);
    padding: 2px 4px 4px 4px;
}
.req-type-group-header {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 3px 6px;
    font-size: var(--fs-xs);
    cursor: pointer;
    border-radius: 3px;
}
/* Group-satisfaction verdict dot (upstream tree, shown only when a save
   is loaded). Green = the group's requirement condition is met by the
   played set; grey = not met; periwinkle = can't be determined from a
   save (per-run / queued / run-count scoping). Mirrors the per-row
   ``.save-badge`` dot so the verdicts read as a column down the left edge
   of the tree. */
.group-status {
    display: inline-block;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    flex-shrink: 0;
}
.group-status-met { background: #4caf50; }
.group-status-unmet { background: #78909c; }
.group-status-unknown { background: #8b95d6; }
.group-status-unobtainable { background: var(--danger); }
.req-type-group-header:hover { background: rgba(255, 255, 255, 0.04); }
.req-type-group-header .toggle {
    width: 12px;
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-2xs);
    flex-shrink: 0;
}
.req-type-group-header .edge-type {
    font-size: var(--fs-2xs);
    padding: 1px 4px;
    border-radius: 2px;
    flex-shrink: 0;
}
/* Help-cursor affordance when an edge-chip carries a tooltip. Covers
   both the per-row edge chip in tree rows (.tree-label .edge-type) and
   the group-header chip (.req-type-group-header .edge-type). No
   underline on chips - the chip background already frames the
   text - so a cursor change is the only hover hint needed. */
.edge-type[data-tooltip] {
    cursor: help;
}
.req-type-group-label {
    font-family: 'Consolas', monospace;
    font-weight: 600;
    color: #c0c0c0;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: var(--fs-2xs);
}
.req-type-group-count {
    font-size: var(--fs-2xs);
    color: var(--text-muted);
    margin-left: auto;
}
.req-type-group-children { display: none; padding-top: 2px; }
.req-type-group-children.expanded { display: block; }
/* Direct child tree-nodes inside a req-type-group lose their own left
   border + indent so the box itself frames them visually. */
.req-type-group-children > .tree-node {
    margin-left: 8px;
    border-left: 1px dotted rgba(255, 255, 255, 0.08);
    padding-left: 8px;
}
/* Nested gamedata-group boxes already have their own margin; reduce it
   so they don't sit awkwardly far right inside the req-type box. */
.req-type-group-children > .gamedata-group {
    margin-left: 8px;
}


/* OR-alternative grouping for H2 `OrRequirements`. The outer
   `.or-group-box` wraps one `.or-branch-box` per alternative;
   each branch contains its own `.req-type-group` children sharing
   the same structure as the AND base block. Amber accent across the
   whole stack so the user can tell at a glance "any one of these"
   from the surrounding "all of these". */
.or-group-box {
    margin: 6px 0 4px 16px;
    border-left: 3px solid #b07a3a;
    border-radius: 3px;
    background: rgba(176, 122, 58, 0.04);
    padding: 2px 4px 4px 4px;
}
.or-group-header {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 3px 6px;
    font-size: var(--fs-xs);
    cursor: pointer;
    border-radius: 3px;
}
.or-group-header:hover { background: rgba(176, 122, 58, 0.08); }
.or-group-header .toggle {
    width: 12px;
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-2xs);
    flex-shrink: 0;
}
.or-group-label {
    font-family: 'Consolas', monospace;
    font-weight: 600;
    color: #f4a261;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: var(--fs-2xs);
    cursor: help;
}
.or-group-children { display: none; padding-top: 2px; }
.or-group-children.expanded { display: block; }

/* One alternative inside an OR group. Dashed left accent to
   visually distinguish "this is one of several branches" from the
   surrounding solid-accented OR wrapper. */
.or-branch-box {
    margin: 4px 0 4px 8px;
    border-left: 2px dashed rgba(176, 122, 58, 0.55);
    border-radius: 2px;
    background: rgba(176, 122, 58, 0.02);
    padding: 2px 4px 4px 4px;
}
.or-branch-box-header {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 3px 6px;
    font-size: var(--fs-xs);
    cursor: pointer;
    border-radius: 3px;
}
.or-branch-box-header:hover { background: rgba(176, 122, 58, 0.06); }
.or-branch-box-header .toggle {
    width: 12px;
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-2xs);
    flex-shrink: 0;
}
.or-branch-box-label {
    font-family: 'Consolas', monospace;
    font-weight: 600;
    color: #e9c46a;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: var(--fs-2xs);
}
.or-branch-box-count {
    font-size: var(--fs-2xs);
    color: var(--text-muted);
    margin-left: auto;
}
.or-branch-box-children { display: none; padding-top: 2px; }
.or-branch-box-children.expanded { display: block; }
/* Direct child req-type-groups inside a branch lose extra indent
   so the branch box itself frames them. */
.or-branch-box-children > .req-type-group {
    margin-left: 8px;
}

/* Placeholder row for an OR branch whose gates are non-textline only.
   Renders as a single header (no children container) so it visually
   reads as "this slot exists, here's the alt index, full content
   lives elsewhere" without taking up the vertical space of a real
   expandable branch. The italic hint + tooltip carry the routing
   pointer to the details panel. */
.or-branch-box-placeholder {
    padding-bottom: 2px;
}
.or-branch-box-header-placeholder {
    cursor: help;
}
.or-branch-box-header-placeholder:hover {
    background: transparent;
}
.or-branch-placeholder-note {
    margin-left: auto;
    font-size: var(--fs-2xs);
    font-style: italic;
    color: #999;
}

/* Leaf-root note shown when a textline has no prerequisites / dependents,
   so an empty tree reads as intentional rather than a failed render. */
.tree-empty-note {
    margin: 6px 0 2px 1.6rem;
    font-size: var(--fs-sm);
    font-style: italic;
    color: var(--text-muted);
}

/* Downstream OR-routed dependents section. Mirrors the upstream
   ``.or-group-box`` amber accent for visual consistency, but its
   inner structure is simpler: a single header + a flat list of
   req-type groups (no per-branch sub-boxes -- each dependent has
   its own branch count which surfaces via the per-row badge). */
.or-downstream-section {
    margin: 6px 0 4px 16px;
    border-left: 3px solid #b07a3a;
    border-radius: 3px;
    background: rgba(176, 122, 58, 0.04);
    padding: 2px 4px 4px 4px;
}
.or-downstream-header {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 3px 6px;
    font-size: var(--fs-xs);
    cursor: pointer;
    border-radius: 3px;
}
.or-downstream-header:hover { background: rgba(176, 122, 58, 0.08); }
.or-downstream-header .toggle {
    width: 12px;
    text-align: center;
    color: var(--text-muted);
    font-size: var(--fs-2xs);
    flex-shrink: 0;
}
.or-downstream-label {
    font-family: 'Consolas', monospace;
    font-weight: 600;
    color: #f4a261;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-size: var(--fs-2xs);
    cursor: help;
}
.or-downstream-count {
    font-size: var(--fs-2xs);
    color: var(--text-muted);
    margin-left: auto;
}
.or-downstream-children { display: none; padding-top: 2px; }
.or-downstream-children.expanded { display: block; }
/* Direct child req-type-groups inside the downstream OR section lose
   extra indent so the section box itself frames them. */
.or-downstream-children > .req-type-group {
    margin-left: 8px;
}

/* Per-row badge for OR-routed downstream dependents. Indicates which
   option of the dependent's OR group routes via the rooted textline.
   Compact monospace amber chip, matching the section accent. */
.or-alt-badge {
    font-size: var(--fs-2xs);
    padding: 1px 5px;
    border-radius: 2px;
    flex-shrink: 0;
    background: rgba(176, 122, 58, 0.15);
    color: #f4a261;
    border: 1px solid rgba(176, 122, 58, 0.4);
    font-family: 'Consolas', monospace;
    font-variant-numeric: tabular-nums;
}
.or-alt-badge[data-tooltip] { cursor: help; }

/* Alternates group in tree */
.alternates-group {
    margin: 2px 0;
    border: 1px solid #3a4a50;
    border-radius: 4px;
    background: #1a2228;
}

.alternates-group.alternates-self {
    border-color: #5a7a5a;
    background: #1a261a;
}

.alternates-group.alternates-other {
    border-color: #4a4a5a;
    background: #1e1e28;
}

.alternates-group-header {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.25rem 0.5rem;
    cursor: pointer;
    font-size: var(--fs-base);
    color: #c4a56e;
}

.alternates-group-header:hover {
    background: #243038;
}

.alternates-group-chevron {
    font-size: 0.6em;
    transition: transform 0.15s;
    display: inline-block;
}

.alternates-group.expanded > .alternates-group-header .alternates-group-chevron {
    transform: rotate(90deg);
}

.alternates-group:not(.expanded) > .alternates-group-children {
    display: none;
}

.alternates-group-children {
    padding-left: 0.5rem;
}

.alternates-group-count {
    font-size: var(--fs-sm);
    opacity: 0.6;
}

.alternates-group-label {
    font-weight: 500;
}

/* --- responsive.css --- */

/* Responsive overrides.
 *
 * PINNED LAST by build_css() so these @media rules win the cascade over the
 * desktop base in every other file (media queries add no specificity, so
 * load order breaks equal-specificity ties).
 *
 * The desktop layout is locked: every rule here is an ADDITIVE mobile/tablet
 * override inside a max-width query. Desktop (> tablet breakpoint) is never
 * touched. Sections below are organised by view: foundation shell, header,
 * dialogue detail, trees, eligibility tracer, speaker, duplicates, search.
 *
 * Breakpoints (single source of truth; CSS @media widths can't read custom
 * properties, so the values are documented here and used literally below):
 *   phone   : max-width  599px
 *   tablet  : max-width 1024px   (covers phone + tablet portrait)
 *   desktop : min-width 1025px   (untouched - locked)
 */

:root {
    /* Minimum comfortable touch target (WCAG 2.5.5 / platform guidance).
       Used by mobile overrides to size chevrons, chips, rows, etc. */
    --tap-min: 44px;
}

/* === Foundation: tablet and below ======================================
 * The desktop shell is a fixed-height (100dvh), overflow-hidden flex column
 * whose panels scroll internally. On small screens that clips content and
 * fights the mobile browser chrome. Here the shell becomes an ordinary
 * vertically-scrolling document: the panels stack and the page scrolls.
 * Wide content (deep trees) is kept from widening the page by the tree
 * section below, which gives those panels their own horizontal scroller.
 * ====================================================================== */
@media (max-width: 1024px) {
    body {
        height: auto;
        min-height: 100vh;
        min-height: 100svh;
        overflow: visible;
        /* Notch / rounded-corner / home-indicator clearance. With
           viewport-fit=cover the page renders edge-to-edge, so these insets
           keep the header and content out of the unsafe zones (and let the
           tour's full-screen fixed dim cover the whole display). Resolves to 0
           on devices without insets, so tablets and desktop are unaffected.
           Not observable under headless emulation - verify on a real device. */
        padding-top: env(safe-area-inset-top);
        padding-left: env(safe-area-inset-left);
        padding-right: env(safe-area-inset-right);
        padding-bottom: env(safe-area-inset-bottom);
    }
    main {
        flex-direction: column;
        overflow: visible;
    }
    /* Panels stack vertically and flow to their natural height (the page
       scrolls, not each panel). The right divider becomes a bottom one. */
    .panel {
        flex: initial;
        overflow: visible;
        border-right: none;
        border-bottom: 1px solid var(--border);
    }
    .panel:last-child { border-bottom: none; }
    .panel-body {
        flex: initial;
        overflow: visible;
    }
}

/* === Header & global controls =====================================
 * The desktop header is a single nowrap flex row (title, game toggle, a
 * 500px search, nav links, save upload) that overflows narrow screens.
 * Let it wrap instead of overflowing, size the controls for touch, and on
 * phones give the search its own full-width row. No JS menu needed - the
 * header isn't sticky on mobile, so a wrapped header just scrolls away.
 * Desktop (> tablet) keeps the original single row.
 * ====================================================================== */
@media (max-width: 1024px) {
    header {
        flex-wrap: wrap;
        row-gap: 8px;
    }
    /* Search drops to its own full-width row so it stays prominent and
       easily tappable rather than being squeezed thin between the other
       controls. (DOM order puts it after the title + toggle, so it lands
       on the second row.) */
    .search-container {
        flex-basis: 100%;
        max-width: none;
    }
    /* When the row wraps, let the save controls flow inline instead of
       being shoved to the far edge by their desktop margin-left:auto. */
    .header-right {
        margin-left: 0;
        flex-wrap: wrap;
        row-gap: 8px;
    }
    /* Touch-sized header controls. */
    #search,
    .header-nav-link,
    .save-upload-btn,
    .game-toggle-btn {
        min-height: var(--tap-min);
    }
    /* Match the status pill's height to the Load-save button so the save
       row reads as one consistent strip; centre its text too. */
    .save-status {
        min-height: var(--tap-min);
        display: inline-flex;
        align-items: center;
    }
    /* min-height alone top-aligns the label inside the link/button content
       box; flex-centre it so the text sits in the middle of the 44px row. */
    .header-nav-link,
    .game-toggle-btn {
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
    /* The display overrides above (needed for sizing / glyph alignment)
       beat the UA ``[hidden] { display: none }`` rule, which would leave a
       cleared save status visible on mobile. Re-assert it. */
    .save-status[hidden] {
        display: none;
    }
}
@media (max-width: 599px) {
    /* Phone: regroup the header controls into two predictable rows rather
       than letting them wrap purely by width. The desktop single-row DOM
       order is locked, so this is pure CSS ``order`` (no DOM change):
         row A: Cross-game duplicates + Load save
         row B: save status   (only present once a save is loaded)
       Both wrapper boxes are flattened with display:contents so their
       children become direct header flex items and can be ordered apart
       from their desktop grouping (header-right held the save group;
       save-upload held the Load-save button and the status pill). */
    .header-right,
    .save-upload {
        display: contents;
    }
    #nav-duplicates { order: 1; }
    .save-upload-btn { order: 2; }
    #save-status { order: 5; }
    /* Forced full-width break between the two rows, gated on a loaded save
       (the status pill becomes visible) via :has so the no-save header carries
       no trailing empty line. */
    header:has(#save-status:not([hidden]))::after {
        content: "";
        order: 3;
        flex-basis: 100%;
        height: 0;
    }
    /* The status pill can be long (e.g. "Hades II with Zagreus' Journey: 1234
       dialogues"); let its text wrap rather than overflow its row. */
    #save-status {
        min-width: 0;
        white-space: normal;
        max-width: 100%;
    }
}

/* === Dialogue detail =============================================
 * The three detail panels already stack vertically (via the shared
 * foundation rules above), and
 * prose (dialogue lines, requirement boxes) wraps. The one thing that
 * still overflows on a phone is a long, unbreakable monospace string - the
 * GameState / CurrentRun paths in the "Other requirements" block - because
 * the panel no longer scrolls horizontally on mobile. Let those break so
 * they wrap instead of pushing the page wider. (overflow-wrap is inherited,
 * so the ``.other-req-path`` code pills inside break too. Shared with the
 * eligibility tracer's Other-requirements section.)
 * ==================================================================== */
@media (max-width: 1024px) {
    .other-req-item {
        overflow-wrap: anywhere;
    }
    /* Touch targets in the info panel: each requirement row taps through to
       that dialogue, and the coloured section-header pill toggles its group.
       Both are dense desktop rows - keep a comfortable tap height but trim it
       (and the section gaps/padding) so the list isn't needlessly tall. */
    .req-section {
        margin-top: 6px;
        padding: 4px 8px 3px;
    }
    .req-item {
        min-height: 38px;
        margin: 1px 0;
    }
    .req-section h4 {
        min-height: 36px;
        margin-bottom: 4px;
        display: inline-flex;
        align-items: center;
    }
}
@media (max-width: 599px) {
    /* The badge cluster (played / priority / play-once / #N/M) sits inline
       at the right of the dialogue name on desktop via margin-left:auto.
       On a phone the name wraps and the right-aligned cluster wraps
       awkwardly, so drop it onto its own row, left-aligned under the name.
       The row spacing comes from the h3's row-gap (shared with the desktop
       wrap case), so no margin-top is needed here. */
    .textline-info h3 .h3-indicators {
        flex-basis: 100%;
        margin-left: 0;
    }
}

/* === Dependency / eligibility trees ==================================
 * The shared upstream / downstream tree rows are single-line (white-space:
 * nowrap) and indent per level, so deep chains and long names grow the tree
 * wider than a phone. On desktop the panel-body absorbs that with a
 * horizontal scrollbar; the mobile foundation made panel-body
 * overflow:visible (so panels stack into the page scroll), which let the
 * wide tree push the whole PAGE sideways instead. Give just the two tree
 * panels their own horizontal scroller back, trim the per-level indent, and
 * grow the row / chevron tap targets for touch. The eligibility tracer
 * reuses these same tree rows, so it inherits the fix.
 * ==================================================================== */
@media (max-width: 1024px) {
    /* Contain the wide tree within its panel instead of widening the page.
       overflow-x:auto promotes overflow-y to auto, but the panel-body height
       is unconstrained on mobile (it grows to fit content), so nothing is
       clipped vertically - only the x-axis scrolls. */
    #upstream-content,
    #downstream-content {
        overflow-x: auto;
    }
    /* Trim the recursive, depth-compounding indent (desktop is 16 + 12) so
       deep chains run off the right far more slowly on a narrow screen. The
       one-off group-box offsets are left as-is. */
    .tree-node {
        margin-left: 4px;
        padding-left: 6px;
    }
    /* The eligibility tracer's own dialogue rows wrap rather than scroll, but
       still need a full touch height. */
    .tree-node-row {
        min-height: var(--tap-min);
    }
    /* Collapsible group / section headers (e.g. "MUST NOT HAVE PLAYED (ALL)",
       "Alternates"): a 44px header centred its short label and left a wide gap
       between that label and the dialogue rows beneath it. Trim it - still a
       comfortable tap target, but the rows now sit close under their header. */
    .req-type-group-header,
    .or-group-header,
    .or-branch-box-header,
    .or-downstream-header,
    .gamedata-group-header,
    .alternates-group-header {
        min-height: 32px;
    }
    /* The upstream / downstream tree rows themselves are denser: a shorter
       row and tighter vertical margin (still a comfortable tap height), and
       trimmed horizontal padding to claw back the left gutter. */
    .tree-label {
        min-height: 38px;
        margin: 1px 0;
        padding: 2px 6px;
    }
    /* The chevron column was a full 44px wide, leaving a large dead gutter on
       the left of every row (leaf rows have no chevron at all). Narrow it -
       it stays full row height, so it's still an easy target. */
    .tree-label .toggle {
        min-width: 30px;
        align-self: stretch;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    /* The taller touch rows leave the desktop-dense type looking sparse, so
       bump the tree's whole font scale up a step on small screens. Overriding
       the size tokens on the tree containers cascades to every row, badge and
       header (they all size from these vars, or from em relative to them), so
       the hierarchy is preserved with one lever instead of per-element rules. */
    #upstream-content,
    #downstream-content,
    .eligibility-tree-container {
        --fs-2xs: 0.75rem;
        --fs-xs: 0.8125rem;
        --fs-sm: 0.875rem;
        --fs-base: 0.9375rem;
    }
}

/* === Site disclaimer placement ======================================
 * Desktop pins the disclaimer to the foot of the middle (prerequisites)
 * panel via ``#upstream-content::after``. Once the panels stack on mobile
 * that lands it mid-page, between prerequisites and dependents. Move it to
 * the foot of the last panel (dependents) so it reads as a page footer. The
 * single-panel views keep their ``#info-content`` disclaimer, already last
 * on mobile. (The string is repeated from base.css rather than touching the
 * locked desktop rule; it is also the README's wording.)
 * ==================================================================== */
@media (max-width: 1024px) {
    #upstream-content::after {
        display: none;
    }
    #downstream-content::after {
        content: 'Built with the assistance of generative AI. All game content belongs to Supergiant Games.';
        display: block;
        padding: 12px 20px;
        font-size: var(--fs-2xs);
        color: #444;
        text-align: center;
        border-top: 1px solid #0f346040;
    }
}

/* === Eligibility tracer ==============================================
 * The tracer is a single panel of wrapping flex rows, so it reflows through
 * the shared foundation, and its tree is handled in the tree section above.
 * Touch gaps: the "Trace eligibility" entry button in the dialogue detail
 * panel, and the clickable "play any of" option rows in the unplayed list.
 * (The inline ``eligibility-ref`` links inside the "needed by" prose are
 * left at text size - per the WCAG inline-link exception, enlarging them
 * would break the sentences.)
 * ==================================================================== */
@media (max-width: 1024px) {
    .trace-eligibility-btn {
        min-height: var(--tap-min);
        display: inline-flex;
        align-items: center;
    }
    .eligibility-group-option {
        min-height: var(--tap-min);
    }
}

/* === Speaker overview ===============================================
 * The speaker view (single full-width panel) packs a 3-/4-column summary
 * grid, a two-column upstream/downstream adjacency block and chip rows, all
 * sized for desktop width. On a phone the columns get unreadably narrow and
 * the adjacency speaker names truncate mid-word. Collapse the grids and stack
 * the adjacency on phones; size the chips and clickable rows for touch on any
 * small screen. Desktop (> tablet) keeps the multi-column layout.
 * ==================================================================== */
@media (max-width: 599px) {
    /* Header: stack the title / description above the "other versions" aside
       rather than squeezing the aside into a third of the width. Flex column
       supersedes the children's desktop grid-column placement. */
    .speaker-overview-header {
        display: flex;
        flex-direction: column;
    }
    /* Summary cards: drop the 3 / 4 desktop columns to two so the big stat
       values stay legible. The two single-number cells (owned / as-guest)
       sit side by side; the two multi-line list cells (sections, save
       progress) each take a full-width row of their own so they're readable
       and the save-progress list is clearly separate from the sections. */
    .speaker-summary-row,
    .speaker-summary-row-4 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
    .speaker-summary-sections,
    .speaker-summary-eligibility {
        grid-column: 1 / -1;
    }
    /* Adjacency: stack the two columns so each gets full width and the
       speaker names stop truncating. Drop the desktop fixed name-column
       width (a JS-set ch value that aligns count chips across rows) and let
       the name cell flex to fill instead. Stack the internal id under the
       friendly name (two lines) to save horizontal space, and bump both a
       step for readability. */
    .speaker-adjacency {
        flex-direction: column;
    }
    .speaker-adjacency-row {
        align-items: center;
    }
    .speaker-adjacency-nameid {
        width: auto;
        flex: 1;
        display: flex;
        flex-direction: column;
        white-space: normal;
        /* Extra gap from the chevron so the name/id sit clear of it (the row
           gap alone left them cramped against the chevron). */
        margin-left: 8px;
    }
    .speaker-adjacency-nameid .speaker-adjacency-name {
        font-size: var(--fs-lg);
    }
    .speaker-adjacency-nameid .speaker-id-inline {
        font-size: var(--fs-sm);
    }
    /* Put the "self" badge to the left of the count chip (DOM order is
       count then self). Explicit orders after the order-0 chevron + name. */
    .speaker-adjacency-self {
        order: 1;
    }
    .speaker-adjacency-count {
        order: 2;
    }
    /* Textline list rows: drop the trailing badges (#N/M, Play-once) onto a
       second line under the dialogue name - the same treatment as the
       adjacency id - so the name gets the full row width instead of being
       squeezed and truncated by the badges. The name keeps its single-line
       ellipsis (these are unbreakable identifier strings, so wrapping them
       would overflow). */
    .speaker-textline-row {
        flex-wrap: wrap;
    }
    .speaker-textline-badges {
        flex-basis: 100%;
    }
}
@media (max-width: 1024px) {
    /* Touch-size the interactive speaker controls: the filter / eligibility
       chips, the "other versions" pills, and the clickable group / adjacency
       / textline rows. */
    .priority-chip,
    .speaker-similar-pill {
        min-height: var(--tap-min);
        display: inline-flex;
        align-items: center;
    }
    .speaker-textline-row,
    .speaker-textline-group-header,
    .speaker-adjacency-row {
        min-height: var(--tap-min);
    }
}

/* === Cross-game duplicates =========================================
 * Desktop master-detail: a fixed speaker sidebar (160-220px) beside a
 * detail pane whose entries tile at min 340px. That fixed pairing overruns
 * a phone (the grid measured ~520px). On phones, stack it: the speaker
 * master list becomes a wrapping chip row above a full-width, single-column
 * detail list, and each duplicate entry becomes a small card - the dialogue
 * name on its own line above the two game buttons, which split the row as
 * equal touch targets. The 2-column master-detail still fits a tablet, so
 * the stack is phone-only; the touch sizing covers any small screen.
 * ================================================================== */
@media (max-width: 599px) {
    .duplicates-md {
        grid-template-columns: 1fr;
    }
    .duplicates-speakers {
        flex-direction: row;
        flex-wrap: wrap;
        gap: 6px;
    }
    .duplicates-speaker-item {
        width: auto;
    }
    .duplicates-detail-list {
        grid-template-columns: 1fr;
        /* More separation between one entry card (name + buttons) and the
           next than the dense desktop row gap. */
        row-gap: 0.9rem;
    }
    /* Card entry. Drops the desktop fixed monospace name-column width, which
       (plus the two buttons) was what tipped the row over the phone width. */
    .duplicates-entry {
        display: flex;
        flex-wrap: wrap;
        align-items: stretch;
        column-gap: 0.5rem;
        row-gap: 0.35rem;
    }
    .duplicates-name {
        flex-basis: 100%;
        width: auto;
        font-size: var(--fs-lg);
    }
    .duplicates-entry > .duplicates-game-link {
        flex: 1;
    }
    .duplicates-entry > .duplicates-game-link:first-of-type {
        margin-left: 0;
    }
}
@media (max-width: 1024px) {
    /* Touch-size the filter input, the game-open buttons and the speaker
       master items. */
    .duplicates-search,
    .duplicates-speaker-item {
        min-height: var(--tap-min);
    }
    .duplicates-game-link {
        min-height: var(--tap-min);
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
}

/* === Search results dropdown =======================================
 * The dropdown already spans the full-width search row on a phone; it just
 * needs touch-sized rows. Its open height is sized to the space above the
 * on-screen keyboard by search-ui.js (the VisualViewport API), so the list
 * stays clear of the keyboard instead of running behind it; the desktop CSS
 * max-height stays as the fallback.
 * ================================================================== */
@media (max-width: 1024px) {
    .search-item {
        padding-top: 13px;
        padding-bottom: 13px;
    }
    .search-section-header {
        padding-top: 8px;
        padding-bottom: 8px;
    }
}