/* incursions.css: Tactical Dice Incursions (preflight, run board, dice tray, debrief); then the XP / progression panel and the Vistas Log Book. */
/* ===== INCURSIONS (Tactical Dice engine) ===== */
/* Small generic helpers used by the incursions templates. */
.incursions-scope { /* placeholder to keep this block greppable */ }
.hint { color: var(--text-muted); font-size: 0.85rem; margin: 4px 0 0; }
.inline-form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.field { display: flex; flex-direction: column; gap: 6px; }
.field-label { font-size: 0.8rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.04em; }
.input, .input-sm {
    background: var(--bg-subtle); color: var(--text-primary);
    border: 1px solid var(--border-color); border-radius: var(--radius-sm);
    padding: 8px 10px; font-family: var(--font-body); font-size: 0.95rem;
}
.input-sm { padding: 4px 8px; font-size: 0.85rem; }
.btn-xs { padding: 3px 9px; min-height: 26px; font-size: 0.78rem; }
.badge { display: inline-block; padding: 1px 8px; border-radius: var(--radius-pill); font-size: 0.72rem; font-weight: 700; }
.badge-active { background: var(--color-success-bg); color: var(--color-success-text); border: 1px solid var(--color-success-border); }
.badge-danger { background: var(--color-error-bg); color: var(--color-error-text); border: 1px solid var(--color-error-border); }

/* Staff catalog: every currently-active way an item can be obtained, as a wrapping chip list. */
.catalog-acquisition-tags { display: flex; flex-wrap: wrap; gap: 4px; margin: 0; padding: 0; list-style: none; }
.catalog-acquisition-tags li { white-space: nowrap; }

/* Global construction goal bar */
.goal-panel { margin-bottom: 18px; }
.goal-bar { height: 14px; background: var(--bg-subtle); border-radius: var(--radius-pill); overflow: hidden; border: 1px solid var(--border-color); margin: 10px 0 6px; }
.goal-bar__fill { height: 100%; background: linear-gradient(90deg, var(--color-primary), var(--color-violet)); border-radius: var(--radius-pill); transition: width 0.4s ease; }
.goal-meta { color: var(--text-secondary); font-size: 0.88rem; margin: 0; }

/* Dice chips + dice faces */
.dice-chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 9px; border-radius: var(--radius-pill); font-size: 0.82rem; font-weight: 600; background: var(--bg-subtle); border: 1px solid var(--border-subtle); }
.dice-total { color: var(--text-muted); font-size: 0.82rem; }
.die {
    position: relative;
    display: inline-flex; align-items: center; justify-content: center;
    width: 36px; height: 36px; border-radius: var(--radius-sm);
    font-weight: 800; font-size: 1.05rem; color: #10121a;
    box-shadow: inset 0 -2px 0 rgba(0,0,0,0.25);
}
/* Color-initial corner tag so dice read without relying on color alone (WCAG 1.4.1). */
.die-tag { position: absolute; top: 1px; left: 3px; font-size: 0.5rem; font-weight: 800; line-height: 1; opacity: 0.75; pointer-events: none; }
.stacked-die .die-tag { top: 0; left: 2px; }
.die-red   { background: var(--dice-red); }
.die-green { background: var(--dice-green); }
.die-blue  { background: var(--dice-blue); }
.die-gold  { background: var(--dice-gold); }
.die-glass { background: var(--dice-glass); }
.die-star  { color: #10121a; box-shadow: inset 0 0 0 2px rgba(255,255,255,0.55); }

/* Squad manager */
/* 280px was plenty for the old single-line roster row, but the .squad-pet-pick cards
   (thumbnail + name + Lvl/trait/status pills) need more room than that: at 280px the
   card's own padding + the roster box's padding + the row's own padding + a 40px
   thumbnail leaves well under half the card for the pills, so a trait or status chip
   routinely poked out past the row's rounded border instead of wrapping cleanly. */
.squad-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(380px, 100%), 1fr)); gap: 16px; }
.squad-card { background: var(--bg-surface); border: 1px solid var(--border-color); border-radius: var(--radius-lg); padding: 16px; display: flex; flex-direction: column; gap: 10px; }
.squad-card__head { display: flex; align-items: center; gap: 8px; }
.drag-handle { cursor: grab; color: var(--text-muted); font-size: 1.1rem; user-select: none; }

/* Keyboard/touch reorder controls: the accessible equivalent of .drag-handle.
   Shared by the squad manager and the barracks roster (see reorderList). */
.reorder-nudge { display: inline-flex; gap: 2px; align-items: center; }
.reorder-nudge__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    font-size: 0.8rem;
    line-height: 1;
    color: var(--text-muted);
    background: var(--bg-subtle);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    cursor: pointer;
}
@media (hover: hover) { .reorder-nudge__btn:hover { color: var(--text-primary); border-color: var(--color-primary); } }
/* On a pet card the controls float over the artwork; the card itself is a link
   target, so they sit above it rather than inside the flow. */
.reorder-nudge--card {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: 2;
    flex-direction: column;
    gap: 3px;
    padding: 3px 4px;
    background: var(--bg-surface);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
}
/* The card's control cluster is two rows (drag/up/down, then to-top/to-bottom)
   stacked inside one pill-shaped container: see reorder-nudge--card above. */
.reorder-nudge__row { display: inline-flex; gap: 2px; align-items: center; justify-content: center; }
.squad-card__name { margin: 0; font-size: 1.05rem; display: flex; align-items: center; gap: 8px; flex: 1; }
.squad-dice { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.squad-stats-line { color: var(--text-secondary); font-size: 0.84rem; margin: 0; }
/* Progress toward the next die of each colour. Dice capacity is floor division, so most
   single stat allocations move nothing; this is the only thing on either screen that
   shows an allocation accumulating. Shared by the squad card and the incursion preflight,
   so the two cannot style the same fact differently. */
.squad-next-die,
.preflight-next-die { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 6px 0 0; }
.next-die-chip {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 1px 7px; border-radius: 999px;
    border: 1px solid var(--border-subtle);
    font-family: var(--font-mono); font-size: 0.74rem; color: var(--text-secondary);
}
.squad-pet-count { color: var(--text-muted); font-size: 0.78rem; font-family: var(--font-mono); white-space: nowrap; }
.squad-members { display: flex; flex-wrap: wrap; gap: 6px; }
.member-chip { display: inline-flex; align-items: center; gap: 6px; background: var(--bg-hover); border-radius: var(--radius-pill); padding: 2px 10px 2px 2px; font-size: 0.82rem; }
.member-chip__thumb { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; flex: 0 0 auto; background: var(--bg-subtle); }
.member-chip__thumb--pending { display: flex; align-items: center; justify-content: center; font-size: 0.72rem; }
/* Collapsed by default (see squads.html's comment on this toggle); the button itself
   needs no more than the shared .btn-xs styling, already touch-floored inside
   .squad-card by the @media (pointer: coarse) block below. */
.squad-details { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
.squad-card__actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.rename-wrap { display: inline-flex; gap: 6px; align-items: center; }
.rename-form { display: inline-flex; gap: 6px; }
.squad-roster-wrap { border-top: 1px solid var(--border-subtle); padding-top: 10px; }
.squad-roster { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; max-height: min(420px, 60dvh); overflow-y: auto; border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 6px; }
.roster-row-form { display: contents; }
.pet-group-checkbox-form { display: contents; }
.roster-celestial { color: var(--color-warning); }
.roster-full-hint { color: var(--color-warning-text); margin-bottom: 4px; }
.roster-busy { color: var(--color-warning-text); }
.roster-member-tag { color: var(--text-muted); }
/* Wraps the toggle-membership pick card and (when the Commander holds a usable item,
   e.g. Void Fog Essence) a small "feed it to this pet" form beneath it. Was a bare
   .roster-row-form { display: contents } before the feed form existed, so .squad-pet-pick
   sat directly as .squad-roster's own flex item; this div is now that flex item instead,
   deliberately not display:contents, since it needs to actually stack the two forms.
   Always stacked (never beside the pick card): a side-by-side layout above some width
   threshold used to squeeze the feed <select> down to a couple of unreadable characters
   the moment its column got narrower than that threshold actually allowed for (a squad
   card sitting at .squad-grid's own 380px minimum, or the incursion launch screen's
   narrower bench rail, both did this) — the full-width row underneath the pick card
   never has that failure mode, so it is the only layout now. */
.squad-pet-row { display: flex; flex-direction: column; gap: 4px; }
.squad-pet-feed {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    padding: 4px 10px;
    background: var(--bg-subtle);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
}
.squad-pet-feed select { flex: 1 1 auto; min-width: 0; text-overflow: ellipsis; }
.squad-pet-feed .btn { flex: 0 0 auto; white-space: nowrap; }

/* Squad add/remove + squad-creation pet picker: a big, clearly-selectable row instead of
   a bare native checkbox, via the same visually-hidden-input + :has(:checked) technique as
   .mw-swatch/.species-pick-card. Also the incursion launch screen's own "Adjust party" bench
   (incursions/home.html, partyPicker): the two rosters are deliberately styled as one shared
   design rather than diverging surfaces, so a pick card looks and behaves the same wherever
   a Commander is choosing which pets go somewhere. */
.squad-pet-pick {
    position: relative;
    display: flex;
    flex-direction: row;
    flex-shrink: 0;
    align-items: center;
    gap: 10px;
    padding: 8px 40px 8px 10px;
    min-height: 44px;
    border: 2px solid var(--border-color);
    border-radius: var(--radius-md);
    background: var(--bg-surface);
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease;
}
.squad-pet-pick .pet-picker-thumb,
.squad-pet-pick .pet-picker-thumb-pending { width: 40px; height: 40px; }
.squad-pet-pick input[type="checkbox"] { position: absolute; opacity: 0; width: 1px; height: 1px; }
@media (hover: hover) { .squad-pet-pick:hover { border-color: var(--color-primary); } }
.squad-pet-pick:has(input:checked) { border-color: var(--color-primary); background: var(--bg-hover); }
.squad-pet-pick:has(input:focus-visible) { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.squad-pet-pick:has(input:disabled) { opacity: 0.45; cursor: not-allowed; }
/* A slightly bigger thumbnail reads as an icon rather than a decorative sliver: scoped
   here rather than resizing the shared .pet-picker-thumb every other picker uses. */
.squad-pet-pick .pet-picker-thumb,
.squad-pet-pick .pet-picker-thumb-pending { width: 40px; height: 40px; font-size: 1rem; }
/* One line: name, then Lvl/trait/status pills, wrapping onto a second line only if a
   narrow card genuinely has no room. Previously name sat on its own line above the
   pills, which read as taller and choppier than it needed to be now that the card has
   room (see .squad-grid's 380px minimum) to fit all of it side by side. Species and level
   stay grouped in the first pill so the name remains the strongest scan target; species
   also feeds the search box via each row's data-search. */
.squad-pet-pick__body { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; flex: 1; min-width: 0; }
.squad-pet-pick__name {
    flex: 1 1 auto;
    min-width: 40px;
    font-weight: 600;
    color: var(--text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* A non-shrinking group so its pills wrap together onto their own line rather than
   interleaving with a truncating name mid-sentence. */
.squad-pet-pick__meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 0.78rem; flex: 0 1 auto; }
.squad-pet-pick__level {
    font-family: var(--font-mono);
    font-size: 0.72rem;
    color: var(--text-muted);
    background: var(--bg-subtle);
    border: 1px solid var(--border-subtle);
    border-radius: 999px;
    padding: 1px 7px;
    white-space: nowrap;
}
/* Selection is not signalled by border colour alone: a checkmark medallion carries it,
   same convention as .mw-swatch__check. */
.squad-pet-pick__check {
    position: absolute;
    top: 50%;
    right: 10px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--gradient-primary);
    color: #fff;
    font-size: 0.75em;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transform: translateY(-50%) scale(0.6);
    transition: opacity 0.12s ease, transform 0.12s ease;
}
.squad-pet-pick:has(input:checked) .squad-pet-pick__check { opacity: 1; transform: translateY(-50%) scale(1); }

/* The squad component rules intentionally keep the desktop controls compact. Keep the
   touch-sized overrides after them so the 44px floor survives the cascade. */
@media (pointer: coarse) {
    .squad-card .btn-xs,
    .squad-card .roster-toggle,
    .squad-pet-feed .input,
    .squad-create-form .input,
    .squad-create-form .btn,
    .squad-search__clear,
    .squad-selected-member__remove { min-height: 44px; }
    .squad-card .reorder-nudge__btn {
        width: 44px;
        height: 44px;
        min-width: 44px;
        min-height: 44px;
    }
    .squad-search__clear,
    .squad-selected-member__remove { width: 44px; min-width: 44px; }
}

/* ── Incursion preflight (launch screen) ─────────────────────────────────────
   The DOM order is the phone order (party -> final dice -> mode -> launch -> world
   context) and the grid only rearranges it from 768px up, so the primary action stays
   near the top of a phone without a second markup path. The whole form is deliberately
   NOT width-capped: it was `max-width: 480px`, a compact single-action rule applied to a
   screen that carries party composition plus mission intelligence, which left half of a
   1440px page empty. */
/* The compact switch in the run board's own header (partials/_board_channel_switch.html).
   The note sits below the button in the header's own note row, alongside the Abandon
   consequence, rather than in a title= that touch and keyboard cannot reach. */
.board-channel-switch { display: contents; }
.board-channel-switch__note { flex-basis: 100%; margin: 0; }

/* The board channel switch on the launch screen (partials/_board_channel_toggle.html).

   Two peer options rather than a checkbox: "on/off" cannot say which board is on, and the
   current channel has to be legible without reading the control's state. The in-use option
   is `disabled` and carries `aria-current`, so the state is in words, in the pressed
   semantics and in the border, never in colour alone. */
.board-channel {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: 8px 12px; margin-bottom: 12px; padding: 8px 12px;
    background: var(--bg-surface); border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
}
.board-channel__lead { flex: 1 1 240px; min-width: 0; }
.board-channel__title {
    margin: 0; font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.09em;
    font-weight: 700; color: var(--text-muted);
}
.board-channel__note { margin: 0; font-size: 0.78rem; color: var(--text-muted); line-height: 1.4; max-width: 60ch; }
.board-channel__options { display: flex; flex-wrap: wrap; gap: 8px; }
.board-channel__options form { margin: 0; }
.board-channel__option {
    display: flex; flex-direction: column; gap: 2px; align-items: flex-start;
    padding: 8px 12px; min-height: 44px; cursor: pointer;
    background: var(--bg-panel); color: var(--text-primary);
    border: 1px solid var(--border-color); border-radius: var(--radius-sm);
    font: inherit; text-align: left;
}
.board-channel__option-name { font-size: 0.84rem; font-weight: 700; }
.board-channel__option-state { font-size: 0.72rem; color: var(--text-muted); }
.board-channel__option.is-current {
    border-color: var(--color-primary); background: var(--bg-elevated); cursor: default;
    opacity: 1;   /* a disabled button here means "already in use", not "unavailable" */
}
.board-channel__option.is-current .board-channel__option-state { color: var(--color-primary); }
@media (hover: hover) {
    .board-channel__option:not(.is-current):hover { border-color: var(--color-primary); }
}

.preflight { display: flex; flex-direction: column; gap: 14px; }
.preflight__grid { display: grid; grid-template-columns: 1fr; gap: 14px; align-items: start; }
.preflight-stats { color: var(--text-secondary); font-size: 0.86rem; margin: 10px 0 6px; }
.preflight-stats strong { color: var(--text-primary); }

/* The authoritative loadout fragment. It is swapped on every party edit, so it carries a
   pending treatment of its own: without one, a slow request leaves the previous total on
   screen looking current. */
.preflight-loadout { display: flex; flex-direction: column; gap: 10px; }
.preflight-loadout.htmx-request { opacity: 0.55; transition: opacity 0.12s ease; }
.preflight-loadout__total { margin: 0; font-size: 1.05rem; color: var(--text-secondary); }
.preflight-loadout__total strong { font-size: 1.5rem; color: var(--text-primary); }
.preflight-loadout__party-size { font-size: 0.85rem; color: var(--text-muted); }
.preflight-dice { display: flex; flex-wrap: wrap; gap: 6px; }
.preflight-sources { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
.preflight-sources__row { font-size: 0.8rem; color: var(--text-secondary); line-height: 1.4; }
.preflight-traits { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding-top: 8px; border-top: 1px solid var(--border-subtle); }
.preflight-traits .eyebrow { flex-basis: 100%; }
.preflight-traits__effect { flex-basis: 100%; margin: 0; font-size: 0.76rem; color: var(--text-secondary); line-height: 1.4; }
.preflight-warn { margin-top: 4px; font-size: 0.78rem; }
.preflight-blockers { padding: 8px 10px; border: 1px solid var(--color-error-border); background: var(--color-error-bg); border-radius: var(--radius-sm); }
.preflight-blockers__list { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; font-size: 0.82rem; color: var(--color-error-text); line-height: 1.4; }

.preflight__commit { display: flex; flex-direction: column; gap: 12px; }
.mode-choice__note { margin: 2px 0 0; }
.btn-launch { align-self: flex-start; min-height: 44px; padding-inline: 28px; font-size: 1rem; }

/* World context: a one-line summary that expands. Native <details> rather than an Alpine
   toggle, so it works with JS absent and is keyboard/screen-reader navigable by default. */
.preflight__world { display: flex; flex-direction: column; gap: 8px; }
.preflight-world-card { background: var(--bg-surface); border: 1px solid var(--border-color); border-radius: var(--radius-md); }
.preflight-world-card > summary { display: flex; align-items: center; gap: 10px; padding: 10px 12px; cursor: pointer; min-height: 44px; }
.preflight-world-card__icon { font-size: 1.3rem; line-height: 1; }
.preflight-world-card__lead { display: flex; flex-direction: column; gap: 1px; }
.preflight-world-card__name { font-weight: 600; font-size: 0.9rem; }
.preflight-world-card__body { margin: 0; padding: 0 12px 12px; font-size: 0.82rem; color: var(--text-secondary); line-height: 1.45; }

@media (min-width: 768px) {
    /* Party gets the wider column: it holds a roster, the conditions column holds figures. */
    .preflight__grid { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }
    .preflight__commit,
    .preflight__world { grid-column: 1 / -1; }

    /* A WRAPPING row in which each fieldset owns a full line, rather than one nowrap row
       shared by every child. This block was written when .preflight__commit had exactly two
       children (the dice-rules fieldset and the launch button), so a single row was the whole
       layout. The Resonance Trials "Run track" fieldset was later added as a THIRD child;
       being a flex item its automatic minimum size is min-content, and its two-column
       .track-choice__options grid makes that ~737px of a 1009px row. That left the
       dice-rules chooser (flex: 1 1 0) with 101px, rendering Pathfinder and Blind as two
       101px-wide, 500px-tall slivers of wrapped text for every account on the allowlist.
       Ordinary accounts never saw it: without the track fieldset the original two-child row
       still resolved correctly, which is why it survived.

       Giving each fieldset its own line keeps its own option cards side by side (~494px
       each) in both states, and no future sibling added here can starve one of the others. */
    .preflight__commit { flex-flow: row wrap; align-items: flex-end; gap: 16px 20px; }
    .preflight__commit > .track-choice { flex: 1 1 100%; }
    .preflight__commit .mode-choice { flex: 1 1 100%; flex-direction: row; flex-wrap: wrap; }
    .preflight__commit .mode-option { flex: 1 1 280px; }
    /* Keeps the launch button on the right of its own final line, as it sat on the right of
       the shared row before. */
    .preflight__commit .btn-launch { margin-left: auto; }
    .mode-choice__note { flex-basis: 100%; }
    .preflight__world { flex-direction: row; }
    .preflight-world-card { flex: 1 1 0; }
}

.preview-members { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.preview-member { display: flex; flex-direction: column; gap: 4px; font-size: 0.82rem; }
.preview-member__row { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.preview-member__name { color: var(--text-primary); }
.preview-member__stats { color: var(--text-muted); white-space: nowrap; }
.preview-member__gold { color: var(--color-warning); }
.preview-member__xp { height: 4px; }

/* Inline help affordance (e.g. Void Stress explainer) */
.help-glyph { cursor: help; opacity: 0.65; font-size: 0.85em; }

/* Marketplace bulk lister: a fixed-height scrollable pet picker (thumbnail + name +
   species) so a Commander with a large roster isn't scrolling a full-page grid, plus the
   per-pet pricing table it feeds. Forms are unconstrained width by default (see
   ".form-compact" under "FORMS") — this one genuinely needs the room for a multi-column
   picker/pricing table, so it's left at the default rather than opting into the compact
   cap the way the small single-action forms elsewhere on this page do. */
.pet-picker-box { display: flex; flex-direction: column; gap: 3px; max-height: 340px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 6px; margin-top: 8px; }
.pet-picker-row { display: flex; align-items: center; gap: 10px; width: 100%; padding: 5px 8px; border: 1px solid transparent; border-radius: var(--radius-sm); background: none; cursor: pointer; text-align: left; font: inherit; color: inherit; }
@media (hover: hover) { .pet-picker-row:hover { background: var(--bg-hover); } }
.pet-picker-row.is-selected { border-color: var(--color-primary); background: var(--bg-subtle); }
/* .pet-picker-thumb / .pet-picker-thumb-pending are also reused by the squad roster
   (_squad_list.html) and the Star Map party bench (_map_nodes_inner.html): the same
   small-thumbnail row treatment, on top of each page's own existing checkbox markup
   rather than a duplicate set of thumbnail rules per page. */
.pet-picker-thumb, .pet-picker-thumb-pending { width: 32px; height: 32px; border-radius: var(--radius-sm); flex-shrink: 0; object-fit: cover; }
.pet-picker-thumb-pending { display: flex; align-items: center; justify-content: center; background: var(--bg-subtle); font-size: 0.85rem; }
.pet-picker-name { font-weight: 500; }
.pet-picker-species { color: var(--text-muted); font-size: 0.82rem; }
.pet-picker-check { margin-left: auto; color: var(--color-primary); }

.bulk-fill-row { display: flex; flex-wrap: wrap; gap: 16px; margin: 12px 0; }
.bulk-fill-row .form-group { margin: 0; }
.bulk-fill-row .form-group > div { display: flex; gap: 6px; }
.bulk-fill-row input[type="number"] { width: 100px; }

/* The pricing table itself is a plain <table> — the global `table` rule (see "TABLES"
   above) already gives it the display:block self-scroll technique, borders, and cell
   padding; this only adds the one thing that's specific to it, the input width. Setting
   width:100% here would fight that global rule's width:max-content and force the table
   narrower than its content needs, which is what caused the pricing table to clip with a
   spurious horizontal scrollbar even on a wide page. */
.bulk-price-table input[type="number"] { width: 90px; }

/* ── Run board: the command table ────────────────────────────────────────────
   One markup path, three compositions. Phone: a single column with the dice in the inline
   encounter hand. Tablet (768-1023): still one column, because a 260px reference rail beside
   a multi-slot room card left roughly 560px for the board itself. Desktop (1024+): a sticky
   command rail beside the encounter, placed by grid-template-areas so the DOM can stay in
   phone order (HUD -> encounter -> route -> notes) regardless of which side the rail is on. */
.board-shell { display: flex; flex-direction: column; gap: 14px; }
.board-layout { display: grid; grid-template-columns: 1fr; gap: 16px; align-items: start; }
.board-main { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
.board-rail { min-width: 0; }

/* Persistent run HUD. Six facts, no explanations: the explanations live in the field notes
   and the help guide, and repeating them here is what made the old tray 953px tall. */
.run-hud {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 2px;
    background: var(--border-subtle);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    overflow: hidden;
}
.run-hud__cell { display: flex; flex-direction: column; gap: 1px; padding: 8px 12px; background: var(--bg-surface); min-width: 0; }
.run-hud__label { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); }
.run-hud__value { font-size: 1.15rem; font-weight: 700; color: var(--text-primary); overflow-wrap: anywhere; }
.run-hud__value--haul { font-size: 0.95rem; color: var(--color-success-text); }
.run-hud__value--loss { color: var(--color-error-text); }
.run-hud__sub { font-size: 0.7rem; color: var(--text-muted); line-height: 1.35; }
.run-hud__hand-summary { display: flex; flex-wrap: wrap; gap: 0 4px; }
.run-hud__hand-label { color: var(--text-secondary); }
.run-hud__next { display: flex; flex-direction: column; gap: 1px; margin-top: 4px; padding-top: 4px; border-top: 1px solid var(--border-subtle); }
.run-hud__next-label { font-size: 0.64rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.run-hud__next-value { font-size: 0.82rem; font-weight: 700; color: var(--color-success-text); white-space: nowrap; }
.run-hud__next-note { font-size: 0.66rem; color: var(--text-muted); line-height: 1.3; }
.run-hud__cell--stress:not([data-stress="0"]) { background: var(--color-error-bg); }
.run-hud__cell--stress:not([data-stress="0"]) .run-hud__value { color: var(--color-error-text); }

/* Reachable-room switcher, beside the encounter it changes. */
.room-switcher { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.room-switcher__label { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); }
.room-switcher__btn {
    min-height: 36px; padding: 6px 14px;
    background: var(--bg-subtle); color: var(--text-secondary);
    border: 1px solid var(--border-color); border-radius: var(--radius-pill);
    font: inherit; font-size: 0.85rem; cursor: pointer;
}
@media (hover: hover) { .room-switcher__btn:hover { border-color: var(--color-primary); } }
.room-switcher__btn.is-selected { border-color: var(--color-primary); background: var(--bg-surface); color: var(--text-primary); font-weight: 600; }

/* Route choice: the next-room decision is deliberately above the full encounter. The old
   pill switcher was easy to miss on phones and repeated the route map without exposing the
   stakes that make one branch preferable to another. */
.room-chooser { display: flex; flex-direction: column; gap: 10px; }
.room-chooser__head { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: 8px 18px; }
.room-chooser__title { margin: 2px 0 0; font-size: 1rem; }
.room-chooser__note { margin: 0; max-width: 58ch; font-size: 0.78rem; color: var(--text-muted); line-height: 1.4; }
.room-chooser__options { display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 8px; }
.room-chooser__option {
    display: flex; flex-direction: column; align-items: flex-start; gap: 4px;
    min-height: 104px; padding: 10px 12px; text-align: left; font: inherit; color: inherit;
    background: var(--bg-surface); border: 1px solid var(--border-color);
    border-radius: var(--radius-md); cursor: pointer;
}
@media (hover: hover) { .room-chooser__option:hover { border-color: var(--color-primary); background: var(--bg-hover); } }
.room-chooser__option.is-selected { border-color: var(--color-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 35%, transparent); background: var(--bg-subtle); }
.room-chooser__option-head { display: flex; align-items: center; gap: 6px; width: 100%; }
.room-chooser__icon { font-size: 1.05rem; }
.room-chooser__name { font-weight: 700; }
.room-chooser__selected { margin-left: auto; color: var(--color-primary); font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
.room-chooser__type { color: var(--text-muted); font-size: 0.72rem; }
.room-chooser__reward { color: var(--color-success-text); font-size: 0.78rem; line-height: 1.3; }
.room-chooser__risk { color: var(--color-warning-text); font-size: 0.74rem; line-height: 1.3; }
.room-chooser__staged { color: var(--color-primary); font-size: 0.72rem; }
.room-chooser__playability { font-size: 0.72rem; line-height: 1.3; }
.room-chooser__playability--clean { color: var(--color-success-text); }
.room-chooser__playability--forced { color: var(--color-warning-text); }
.room-chooser__playability--blind { color: var(--color-info-text); }
.room-chooser__playability--blocked { color: var(--color-warning-text); }

.route-node__playability { display: block; color: var(--color-warning-text); font-size: 0.72rem; line-height: 1.3; }

/* The active encounter: the largest, clearest object on the board. */
.encounter { background: var(--bg-surface); border: 1px solid var(--color-primary); border-radius: var(--radius-lg); overflow: hidden; box-shadow: 0 2px 14px rgba(0,0,0,0.16); scroll-margin-top: calc(var(--header-height) + 14px); }
.encounter__body { display: flex; flex-direction: column; gap: 12px; padding: 14px 16px; }
.encounter__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }
.encounter__title { margin: 0; font-size: 1.15rem; }
.encounter__step { margin: 0 0 2px; color: var(--text-muted); font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em; }
.encounter__step span { color: var(--color-primary); font-weight: 700; }
.encounter__instruction { margin: -2px 0 0; color: var(--text-secondary); font-size: 0.86rem; line-height: 1.35; }
.encounter__stakes { display: flex; flex-direction: column; gap: 6px; margin: 0; }
.encounter__stake { display: flex; flex-direction: column; gap: 4px; min-width: 0; font-size: 0.86rem; }
.encounter__stake dt { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.encounter__stake dd { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 8px; margin: 0; min-width: 0; }
.encounter__stake--reward dd { color: var(--color-success-text); }
.encounter__stake--penalty dd { color: var(--color-warning-text); }
.encounter__stake-value { font-weight: 700; }
.encounter__stake-condition { color: var(--text-secondary); }
.encounter__stake-free { color: var(--text-muted); }
.encounter__unlocks { margin: 0; font-size: 0.8rem; color: var(--text-muted); }
.encounter-empty { text-align: center; }

/* The card's action hierarchy: a quick deterministic placement, an inline hand on touch
   surfaces, then the lock itself. The desktop rail remains the spacious reference surface. */
.encounter-quick-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.encounter-quick-actions__hint { color: var(--text-muted); font-size: 0.74rem; line-height: 1.35; }
.encounter-hand { display: none; flex-direction: column; gap: 6px; padding: 10px 0 2px; border-top: 1px solid var(--border-subtle); }
.encounter-hand__head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.encounter-hand__head h3 { margin: 0; font-size: 0.84rem; }
.encounter-hand__head > span { color: var(--text-muted); font-size: 0.76rem; }
.encounter-hand__head strong { color: var(--color-primary); }
.encounter-hand__hint { margin: 0; color: var(--text-muted); font-size: 0.74rem; line-height: 1.35; }
.encounter-hand__dice { min-height: 40px; }
.room-lock__blocked { margin: 10px 0 0; padding: 9px 11px; border-left: 3px solid var(--color-warning); background: var(--bg-subtle); color: var(--color-warning-text); font-size: 0.82rem; line-height: 1.4; }
.room-fail-form { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; margin-top: 12px; padding: 10px 12px; border: 1px solid var(--color-warning); border-radius: var(--radius-md); background: var(--bg-subtle); }
.room-fail-form__note { flex: 1 1 100%; margin: 0; color: var(--color-warning-text); font-size: 0.8rem; line-height: 1.4; }
.encounter-retreat { display: none; align-items: center; flex-wrap: wrap; gap: 8px 12px; padding: 10px 0 0; }
.encounter-retreat .hint { margin: 0; font-size: 0.74rem; }

/* Art slot: a designed placeholder that reserves the final aspect ratio, so dropping real
   art in later shifts no layout. Never an empty frame labelled "art coming soon". */
.incursion-room-art,
.incursion-boss-art {
    position: relative;
    display: flex; align-items: center; justify-content: center;
    height: 72px;
    aspect-ratio: auto;
    max-height: none;
    background:
        radial-gradient(farthest-side at 50% 120%, color-mix(in srgb, var(--color-primary) 24%, transparent), transparent),
        linear-gradient(160deg, var(--bg-subtle), var(--bg-hover));
    border-bottom: 1px solid var(--border-subtle);
}
.incursion-boss-art { height: 92px; }
.incursion-room-art__glyph,
.incursion-boss-art__glyph { font-size: 2rem; opacity: 0.5; }
.incursion-boss-art__glyph { font-size: 2.5rem; }

/* A named floor modifier, stated where the floor is named. */
.floor-modifier { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 8px 0 0; }
.floor-modifier__blurb { font-size: 0.82rem; color: var(--text-secondary); line-height: 1.4; }

/* ── Room resolution: the primary feedback moment ──────────────────────────── */
.room-result {
    border: 1px solid var(--border-color);
    border-left: 4px solid var(--border-color);
    border-radius: var(--radius-md);
    background: var(--bg-surface);
    padding: 14px 16px;
    display: flex; flex-direction: column; gap: 10px;
}
/* Programmatic focus lands here after every resolve, so the ring hangs off plain :focus:
   a browser will not reliably treat programmatic focus as :focus-visible. */
.room-result:focus { outline: 2px solid var(--color-primary); outline-offset: 3px; }
/* Outcome is carried by the heading's own words and glyph; these only tint what already
   says the thing. */
.room-result--clean { border-left-color: var(--color-success); }
.room-result--forced { border-left-color: var(--color-warning); }
.room-result--fail,
.room-result--collapse { border-left-color: var(--color-error); }
/* The whole causal story collapses behind this native <details>, closed by default: the
   verdict alone is what matters at a glance, and the dice/reward detail is one click away.
   No JS required, same convention as _field_notes.html. */
.room-result__disclosure { display: flex; flex-direction: column; }
.room-result__head {
    display: flex; align-items: center; justify-content: space-between; gap: 10px;
    cursor: pointer; list-style: none; min-height: 44px;
}
.room-result__head::-webkit-details-marker { display: none; }
.room-result__head:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.room-result__chevron { color: var(--text-muted); transition: transform 0.15s ease; }
.room-result__disclosure[open] .room-result__chevron { transform: rotate(180deg); }
.room-result__verdict { font-size: 1.1rem; }
.room-result--clean .room-result__verdict { color: var(--color-success-text); }
.room-result--forced .room-result__verdict { color: var(--color-warning-text); }
.room-result--fail .room-result__verdict,
.room-result--collapse .room-result__verdict { color: var(--color-error-text); }
.room-result__body { display: flex; flex-direction: column; gap: 10px; padding-top: 10px; }

.result-slots { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.result-slot { display: flex; flex-direction: column; gap: 4px; padding: 8px 10px; background: var(--bg-subtle); border-radius: var(--radius-sm); }
.result-slot__dice { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.result-slot__arrow { color: var(--text-muted); }
.result-slot__outcome { margin: 0; font-size: 0.84rem; color: var(--text-secondary); }
.result-slot--open .result-slot__outcome { color: var(--color-success-text); }
.result-slot--forced .result-slot__outcome { color: var(--color-warning-text); }
.result-slot--shut .result-slot__outcome { color: var(--color-error-text); }
/* The die that opened the slot, marked with a ring rather than by colour alone: its
   accessible name says "opened the lock" too. */
.die-winner { box-shadow: inset 0 0 0 2px var(--text-primary), 0 0 0 2px var(--color-success); }

.room-result__spent { margin: 0; font-size: 0.82rem; color: var(--text-secondary); }
.room-result__spent strong { color: var(--text-primary); }
.result-changes { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
.result-change { font-size: 0.86rem; }
.result-change--good { color: var(--color-success-text); }
.result-change--bad { color: var(--color-error-text); }
.result-change--route { color: var(--color-primary); }
.result-change--neutral { color: var(--text-muted); }
.result-change__running { color: var(--text-muted); font-size: 0.8rem; }
.room-result__boss { margin: 0; font-size: 0.86rem; color: var(--text-secondary); }
.room-result__xp { list-style: none; margin: 4px 0 6px; padding: 0; }
.room-result__xp li {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 10px;
    align-items: baseline;
    font-size: 0.84rem;
}
.room-result__xp-name { flex: 1 1 auto; }
.room-result__xp .rested-credit,
.room-result__xp .xp-capped-note { flex: 1 0 100%; }

/* Entrance only. Every value is already in the DOM and readable; this staggers the slots so
   the causal story reads in order. body.reduced-motion clamps all of it globally. */
@keyframes result-slot-in {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: none; }
}
.room-result.is-revealing .result-slot {
    animation: result-slot-in 0.32s ease both;
    animation-delay: calc(var(--reveal-index, 0) * 0.18s);
}
.room-result.is-revealing .result-changes { animation: result-slot-in 0.3s ease 0.5s both; }

/* Route: compact nodes. A locked node used to repeat its full reward sentence, its penalty
   sentence, "a clean clear costs nothing" and the word Locked twice; seven of those filled a
   phone screen above the one room that could be played. */
.route { display: flex; flex-direction: column; gap: 10px; }
.route__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; }
.route__title { margin: 0; font-size: 1rem; }
.route__note { margin: 0; font-size: 0.78rem; color: var(--text-muted); }
.route-node {
    position: relative; z-index: 1;
    flex: 1 1 170px; max-width: 240px; min-width: 0;
    background: var(--bg-surface); border: 1px solid var(--border-color);
    border-radius: var(--radius-md); padding: 8px 10px;
}
/* Focused programmatically after a resolve swap (restoreFocus), so the ring has to come
   from :focus; a browser will not always treat programmatic focus as :focus-visible, and
   without it a keyboard player has no idea where they landed. */
.route-node:focus { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.route-node--cleared { opacity: 0.72; border-color: var(--color-success-border); }
.route-node--failed { opacity: 0.6; border-color: var(--color-error-border); }
.route-node--skipped { opacity: 0.52; border-color: var(--border-subtle); border-style: dashed; }
.route-node--locked { opacity: 0.75; }
.route-node--available { border-color: var(--color-primary); }
.route-node.is-selected { box-shadow: 0 0 0 2px var(--color-primary); }
.route-node__select {
    display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
    width: 100%; min-height: 44px; padding: 0;
    background: none; border: 0; font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.route-node__head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.route-node__icon { font-size: 1rem; }
.route-node__name { font-size: 0.86rem; font-weight: 600; }
.route-node__staged { flex-basis: 100%; font-size: 0.72rem; color: var(--color-primary); }
.route-node__detail { margin-top: 6px; font-size: 0.76rem; }
.route-node__detail > summary { cursor: pointer; color: var(--text-muted); min-height: 24px; }
.route-node__detail .room-reward,
.route-node__detail .room-penalty { margin: 4px 0 0; font-size: 0.76rem; }

/* Field notes: everything that explains the run rather than being part of the next decision.
   Native <details>, so it works with JS absent and is keyboard-navigable by default. */
.field-notes { display: flex; flex-direction: column; gap: 6px; }
.field-notes__heading { margin: 0 0 2px; font-size: 0.9rem; }
.field-note { background: var(--bg-surface); border: 1px solid var(--border-color); border-radius: var(--radius-md); }
.field-note > summary { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 12px; min-height: 44px; cursor: pointer; }
.field-note__label { font-size: 0.84rem; font-weight: 600; }
.field-note__value { font-size: 0.84rem; color: var(--text-secondary); font-family: var(--font-mono); }
.field-note__body { padding: 0 12px 12px; font-size: 0.82rem; color: var(--text-secondary); line-height: 1.5; }
.field-note__body p { margin: 0 0 6px; }
.field-note[data-stress]:not([data-stress="0"]) { border-color: var(--color-error-border); }
.field-note--warn { border-color: var(--color-warning-border); padding: 8px 12px; }
.field-note--warn .hint { margin: 0; }
.board-squad__trait-effect { font-size: 0.74rem; color: var(--text-muted); padding-left: 2px; }

/* ── Dice tray: sticky rail at 1024+; touch layouts use the inline hand ──────── */
.dice-tray { display: flex; flex-direction: column; gap: 10px; background: var(--bg-surface); border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 12px; }
.dice-tray__head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.dice-tray__title { margin: 0; font-size: 0.85rem; font-weight: 600; }
.dice-tray__count { font-size: 1.3rem; font-weight: 800; color: var(--color-primary); }
.dice-tray__body { display: flex; flex-direction: column; gap: 10px; }
.dice-tray__hint { margin: 0; font-size: 0.76rem; color: var(--text-muted); line-height: 1.35; }
.dice-tray__mode-note { margin: 0; font-size: 0.74rem; }
.dice-pool { display: flex; flex-wrap: wrap; gap: 8px; }
/* A die is a real button: HTML5 drag events never fire on touch, so tap-to-place is the
   only interaction that works on the device this board is mostly played on. */
.die-btn { border: 0; cursor: pointer; font: inherit; }
@media (hover: hover) { .die-btn:hover { transform: translateY(-1px); } }
.lock-slot-v2.is-armed { border-style: solid; border-color: var(--color-primary); background: var(--bg-subtle); }
.slot-controls { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.slot-arm { flex: 0 0 auto; min-height: 32px; }
.slot-controls .slot-pick { flex: 1 1 140px; width: auto; }
.meter-note { font-size: 0.72rem; color: var(--text-muted); line-height: 1.35; }
.btn-attempt { min-height: 44px; }
/* Pathfinder dice-match assist: ring the pool dice that open the hovered/focused slot. */
.die.die-hint-match { outline: 2px solid var(--color-success); outline-offset: 2px; }
.die.die-hint-sacrifice { outline: 2px dashed var(--color-warning); outline-offset: 2px; }
/* Void Sickness forewarning: shown before the Fail that would trigger a cooldown. */
.hint-warn {
    color: var(--color-warning-text);
    background: var(--color-warning-bg);
    border: 1px solid var(--color-warning-border);
    border-radius: var(--radius-sm);
    padding: 6px 8px;
    line-height: 1.4;
}
.void-sickness-warn { margin-top: 10px; font-size: 0.76rem; }

/* The Void Flux this run was launched under (frozen at start_run). */
/* "Frozen at launch" was a title=, i.e. unreadable on the phones this board is played on. */

/* Void Resonance: what this floor's boss pays, and what the next one would. */

.tray-actions { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border-subtle); display: flex; flex-direction: column; gap: 6px; }
.tray-actions__hint { margin: 0; font-size: 0.74rem; color: var(--text-muted); line-height: 1.35; }

/* The minimap is a visual route thumbnail, not a second room-card surface. The graph is
   laid out server-side from the same RoomNode rows/edges as the full map; only reachable
   nodes are buttons, so the click target remains an existing room-selection action. */
.incursion-minimap { display: flex; flex-direction: column; gap: 8px; padding: 10px; background: var(--bg-surface); border: 1px solid var(--border-color); border-radius: var(--radius-md); }
.incursion-minimap__head { display: flex; align-items: end; justify-content: space-between; gap: 8px; }
.incursion-minimap__title { margin: 2px 0 0; font-size: 0.9rem; }
.incursion-minimap__progress { color: var(--text-muted); font-size: 0.72rem; font-family: var(--font-mono); }
.incursion-minimap__plot { position: relative; min-height: 224px; overflow: hidden; border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); background: var(--bg-subtle); }
.incursion-minimap__plot::before { content: ""; position: absolute; inset: 0; opacity: 0.28; background-image: linear-gradient(to bottom, transparent 49.5%, var(--border-subtle) 50%, transparent 50.5%), linear-gradient(to right, transparent 49.5%, var(--border-subtle) 50%, transparent 50.5%); pointer-events: none; }
.incursion-minimap__edges { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.incursion-minimap__edge { fill: none; stroke: var(--border-color); stroke-width: 1.3; vector-effect: non-scaling-stroke; opacity: 0.78; }
.incursion-minimap__edge.edge-cleared { stroke: var(--color-success); opacity: 0.95; }
.incursion-minimap__edge.edge-dead { stroke: var(--color-error); stroke-dasharray: 3 3; opacity: 0.7; }
.incursion-minimap__nodes { position: absolute; inset: 0; }
.incursion-minimap__node { position: absolute; z-index: 1; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; transform: translate(-50%, -50%); border: 1px solid var(--border-color); border-radius: 50%; background: var(--bg-surface); color: var(--text-secondary); font: inherit; font-size: 0.88rem; line-height: 1; text-align: center; }
.incursion-minimap__node[title] { cursor: help; }
button.incursion-minimap__node { cursor: pointer; }
.incursion-minimap__node--cleared { border-color: var(--color-success-border); background: var(--color-success-bg); color: var(--color-success-text); }
.incursion-minimap__node--failed { border-color: var(--color-error-border); background: var(--color-error-bg); color: var(--color-error-text); opacity: 0.72; }
.incursion-minimap__node--skipped { border-style: dashed; color: var(--text-muted); opacity: 0.58; }
.incursion-minimap__node--locked { border-style: dashed; opacity: 0.58; }
.incursion-minimap__node--available { border-color: var(--color-primary); background: var(--bg-surface); color: var(--text-primary); box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 22%, transparent); }
.incursion-minimap__node.is-selected { border-color: var(--color-primary); box-shadow: 0 0 0 3px var(--color-primary), 0 0 14px color-mix(in srgb, var(--color-primary) 45%, transparent); }
.incursion-minimap__node.node-boss { border-color: var(--color-reward); }
.incursion-minimap__node.node-boss.is-selected { box-shadow: 0 0 0 3px var(--color-reward), 0 0 14px color-mix(in srgb, var(--color-reward) 45%, transparent); }
button.incursion-minimap__node:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 3px; }
.incursion-minimap__legend { display: flex; flex-wrap: wrap; gap: 5px 8px; color: var(--text-muted); font-size: 0.66rem; line-height: 1.2; }
.incursion-minimap__legend > span { display: inline-flex; align-items: center; gap: 3px; }
.incursion-minimap__legend-swatch { display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px; border: 1px solid var(--border-color); border-radius: 50%; font-style: normal; font-size: 0.58rem; }
.incursion-minimap__legend-swatch--cleared { border-color: var(--color-success-border); color: var(--color-success-text); }
.incursion-minimap__legend-swatch--reachable { border-color: var(--color-primary); color: var(--color-primary); }
.incursion-minimap__legend-swatch--skipped { border-style: dashed; color: var(--text-muted); }
.incursion-minimap__legend-swatch--locked { border-style: dashed; }
.incursion-minimap__help { margin: 0; color: var(--text-muted); font-size: 0.7rem; line-height: 1.3; }

/* Pending-haul HUD (the Extract-vs-Descend stake) */
/* The Extract-keeps-all / Fail-forfeits-N% terms, previously title= only. */
.run-summary__haul { color: var(--text-secondary); font-size: 0.95rem; margin: 6px 0 14px; }
.floor-map-wrap { position: relative; }
.floor-edges-layer { position: absolute; inset: 0; pointer-events: none; z-index: 0; }
.floor-edges { width: 100%; height: 100%; overflow: visible; }
.floor-edges .edge { stroke-width: 2; fill: none; }
.floor-edges .edge-open { stroke: var(--text-muted); opacity: 0.45; }
.floor-edges .edge-cleared { stroke: var(--color-success); opacity: 0.85; }
.floor-edges .edge-dead { stroke: var(--color-error); opacity: 0.5; stroke-dasharray: 4 4; }
.floor-map { display: flex; flex-direction: column; gap: 14px; }
.floor-row { display: flex; flex-wrap: wrap; gap: 14px; justify-content: center; }
/* The card is focused programmatically after a resolve swap (restoreFocus), so the ring
   has to come from :focus; a browser won't always treat programmatic focus as
   :focus-visible, and without it a keyboard player has no idea where they landed. */
.node-boss { border-color: var(--color-reward); }
.node-faction_supply { border-color: var(--color-info-border); }
.room-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; }
.room-type { font-weight: 700; font-size: 0.9rem; }
.room-status { font-size: 0.72rem; color: var(--text-muted); text-transform: uppercase; }
.room-lock { display: flex; flex-direction: column; gap: 8px; }
/* Slot requirement chip. The die-face palette (#e5573c/#4bc46a/#4a9df0) is tuned for dark
   backgrounds and lands at 1.8–3.0:1 on the light themes' --bg-subtle, well under the 4.5:1
   WCAG 1.4.3 minimum for this 0.8rem bold text, and this chip is the most-read element on
   the board. Tokenised so each theme can supply a shade that actually passes; the R/G/B
   letter still carries the meaning independently of colour (WCAG 1.4.1). */
.slot-req { display: inline-flex; align-items: center; justify-content: center; min-width: 46px; padding: 2px 6px; border-radius: var(--radius-sm); font-weight: 700; font-size: 0.8rem; background: var(--bg-subtle); }
.slot-req.slot-red { color: var(--dice-red-text, #e5573c); }
.slot-req.slot-green { color: var(--dice-green-text, #4bc46a); }
.slot-req.slot-blue { color: var(--dice-blue-text, #4a9df0); }
.slot-req.slot-any { color: var(--text-secondary); }
/* The floor decision, presented immediately after the boss result. Two columns of facts,
   neither styled as the recommended one: the panel explains consequences, it does not
   advise. */
.boss-choice-modal { position: relative; margin-top: 14px; }
.boss-choice-modal__backdrop { display: none; }
.boss-choice-modal__dialog { width: 100%; }
.boss-choice-modal--open {
    position: fixed;
    inset: 0;
    z-index: 9990;
    display: grid;
    place-items: center;
    padding: 20px;
    overflow-y: auto;
    overscroll-behavior: contain;
}
.boss-choice-modal--open .boss-choice-modal__backdrop {
    display: block;
    position: absolute;
    inset: 0;
    background: var(--modal-overlay);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
}
.boss-choice-modal--open .boss-choice-modal__dialog {
    position: relative;
    z-index: 1;
    width: min(780px, 100%);
    max-height: calc(100vh - 40px);
    max-height: calc(100dvh - 40px);
    overflow-y: auto;
    overscroll-behavior: contain;
}
.boss-choice { border-color: var(--color-reward); }
.boss-choice__title { text-align: center; }
.boss-choice__lede { margin: 6px auto 12px; max-width: 58ch; color: var(--text-muted); text-align: center; }
.boss-choice__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; margin-top: 10px; }
.boss-choice__option { display: flex; flex-direction: column; gap: 10px; padding: 12px 14px; background: var(--bg-subtle); border: 1px solid var(--border-color); border-radius: var(--radius-md); }
.boss-choice__option-title { margin: 0; font-size: 1rem; }
/* A scroll box of its own, same convention as .squad-roster: this is what keeps the modal
   itself short even though each option still states every fact. max-height caps it; a short
   dl (e.g. the terminal floor's "Not available" option) simply never reaches it. */
.boss-choice__facts {
    display: flex; flex-direction: column; gap: 8px; margin: 0; flex: 1;
    max-height: min(230px, 32dvh); overflow-y: auto; overscroll-behavior: contain;
    border: 1px solid var(--border-color); border-radius: var(--radius-md);
    padding: 8px; background: var(--bg-surface);
}
.boss-choice__facts > div { display: flex; flex-direction: column; gap: 1px; }
.boss-choice__facts dt { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.boss-choice__facts dd { margin: 0; font-size: 0.86rem; color: var(--text-primary); }
.boss-choice__sub { display: block; font-size: 0.74rem; color: var(--text-muted); }
.boss-choice__option .btn { min-height: 44px; }
.run-summary { text-align: center; }
.run-summary__actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 12px; }

/* Launch screen: Pathfinder / Blind mode chooser */
.mode-choice { display: flex; flex-direction: column; gap: 8px; border: 0; padding: 0; margin: 0; }
.mode-choice legend { padding: 0; margin-bottom: 2px; }
.mode-option { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-md); cursor: pointer; transition: border-color 0.15s ease, background 0.15s ease; }
@media (hover: hover) { .mode-option:hover { background: var(--bg-hover); } }
.mode-option:has(input:checked) { border-color: var(--color-primary); background: var(--bg-subtle); }
.mode-option input { margin-top: 3px; }
.mode-option__body { display: flex; flex-direction: column; gap: 2px; }
.mode-option__body strong { font-size: 0.95rem; }
.mode-option__body small { color: var(--text-secondary); font-size: 0.82rem; line-height: 1.35; }
.mode-option:has(input:disabled) { opacity: 0.55; cursor: not-allowed; }

/* Blind mode: face-down dice */
.die-blind { color: #10121a; opacity: 0.9; box-shadow: inset 0 0 0 2px rgba(0,0,0,0.28), inset 0 -2px 0 rgba(0,0,0,0.2); }

/* Room lock: chip-based dice slots (both modes, _room_lock.html) */
.lock-slot-v2 { display: flex; flex-direction: column; gap: 6px; padding: 8px; border: 1px dashed var(--border-color); border-radius: var(--radius-sm); }
.lock-slot-v2__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.slot-odds { font-size: 0.8rem; font-weight: 700; color: var(--text-secondary); }
.slot-stack { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; min-height: 34px; }
.slot-empty { color: var(--text-muted); font-size: 0.8rem; font-style: italic; }
.stacked-die { position: relative; display: inline-flex; align-items: center; gap: 1px; padding: 0; width: 34px; height: 34px; justify-content: center; border: none; border-radius: var(--radius-sm); font-weight: 800; font-size: 0.95rem; color: #10121a; cursor: pointer; box-shadow: inset 0 -2px 0 rgba(0,0,0,0.25); }
.stacked-die__x { font-size: 0.7rem; opacity: 0; transition: opacity 0.12s ease; }
@media (hover: hover) { .stacked-die:hover .stacked-die__x { opacity: 0.85; } }
.stacked-die.die-sacrifice { box-shadow: inset 0 0 0 2px var(--color-warning), inset 0 -2px 0 rgba(0,0,0,0.25); }
.slot-pick { width: 100%; }
.sacrifice-warn { margin: 2px 0 0; padding: 5px 8px; font-size: 0.78rem; line-height: 1.3; color: var(--color-warning-text); background: var(--color-warning-bg); border: 1px solid var(--color-warning-border); border-radius: var(--radius-sm); }
.sacrifice-tax { margin: 4px 0 0; font-size: 0.76rem; line-height: 1.3; font-weight: 600; color: var(--color-error-text); }

/* Room reward/penalty reveal (#5) + reachable emphasis + unlocks hint (#7) */
.room-reward { margin: 0 0 4px; font-size: 0.82rem; color: var(--color-success-text); }
.room-penalty { margin: 0 0 8px; font-size: 0.8rem; color: var(--color-warning-text); }
/* "A clean clear costs nothing" was in the penalty's title=. It is the reassuring half of
   the sentence, so it renders in the room's own muted tone rather than the warning colour. */
.room-penalty__free { color: var(--text-muted); }
.room-unlocks { margin: 8px 0 0; font-size: 0.78rem; color: var(--text-muted); letter-spacing: 0.02em; }
.room-unlocks__child + .room-unlocks__child { margin-left: 3px; }

/* Board node-type legend (#7) */
.node-legend { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border-subtle); display: flex; flex-wrap: wrap; gap: 6px 10px; }
.node-legend__title { flex-basis: 100%; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.node-legend__item { font-size: 0.78rem; color: var(--text-secondary); white-space: nowrap; }

/* Roster search (#10) */
.roster-search { width: 100%; margin-bottom: 4px; }

.boss-choice__hint { color: var(--text-muted); font-size: 0.82rem; margin: 10px auto 0; max-width: 46ch; }

@media (width < 600px) {
    .boss-choice-modal--open { align-items: end; padding: 10px; }
    .boss-choice-modal--open .boss-choice-modal__dialog { max-height: calc(100dvh - 20px); }
    .boss-choice-modal--open .boss-choice { padding: 14px; }
}

/* Authored room identity + the Merchant's actual choice. */
.encounter__flavour { margin: 0; font-size: 0.86rem; color: var(--text-secondary); line-height: 1.45; }
.encounter__type { color: var(--text-muted); }
.encounter__moment { margin: 0; font-size: 0.82rem; color: var(--color-primary); font-style: italic; }
.reward-choice { display: flex; flex-wrap: wrap; gap: 8px; border: 0; padding: 0; margin: 0; }
.reward-choice__legend { padding: 0; margin-bottom: 4px; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.reward-choice__context { flex-basis: 100%; margin: 0 0 2px; font-size: 0.76rem; color: var(--text-secondary); line-height: 1.35; }
.reward-choice__option { flex: 1 1 160px; display: flex; align-items: flex-start; gap: 8px; padding: 8px 10px; min-height: 44px; border: 1px solid var(--border-color); border-radius: var(--radius-md); cursor: pointer; }
@media (hover: hover) { .reward-choice__option:hover { background: var(--bg-hover); } }
.reward-choice__option:has(input:checked) { border-color: var(--color-primary); background: var(--bg-subtle); }
.reward-choice__body { display: flex; flex-direction: column; gap: 1px; }
.reward-choice__body small { color: var(--text-muted); font-size: 0.74rem; line-height: 1.3; }

/* Bounded room decisions: readable before the dice tray on a phone, and two-up on wider
   screens so the choice feels like part of the encounter rather than a hidden form field. */
.room-interaction { display: flex; flex-wrap: wrap; gap: 8px; border: 0; padding: 0; margin: 0; }
.room-interaction__legend { flex-basis: 100%; padding: 0; margin-bottom: 2px; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.room-interaction__option { flex: 1 1 190px; display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px; min-height: 52px; border: 1px solid var(--border-color); border-radius: var(--radius-md); cursor: pointer; background: var(--bg-subtle); }
@media (hover: hover) { .room-interaction__option:hover { background: var(--bg-hover); } }
.room-interaction__option:has(input:checked) { border-color: var(--color-primary); box-shadow: 0 0 0 1px var(--color-primary); }
.room-interaction__body { display: flex; flex-direction: column; gap: 3px; }
.room-interaction__body small { color: var(--text-secondary); font-size: 0.78rem; line-height: 1.35; }
/* Each option's own concrete payout, so two choices whose blurbs both promise "more" can't
   render identically: see incursions/views.py::_room_interaction_options. A COLUMN, not a
   wrapping chip row: these lines are the room contract in a fixed order (lock, cost now,
   clean clear, forced or failed, changes later), and a player learns that order by reading
   it down the card. Wrapped side by side, the same six lines re-flow differently on every
   room and the order stops being legible at all. */
.room-interaction__figures { display: flex; flex-direction: column; gap: 2px; font-size: 0.78rem; font-weight: 700; margin-top: 2px; }
.room-interaction__figure--lock { color: var(--text-secondary); }
.room-interaction__figure--reward { color: var(--color-success-text); }
.room-interaction__figure--penalty { color: var(--color-warning-text); }
.room-interaction__figure--cost { color: var(--color-warning-text); }
.room-interaction__figure--effect { color: var(--text-primary); }
/* An option that cannot do anything right now is dimmed and says why in real markup, never
   a title=. It stays on the card rather than disappearing: an option that vanishes reads as
   a missing feature, while a stated reason teaches the rule. */
.room-interaction__option--unavailable { opacity: 0.72; cursor: not-allowed; border-style: dashed; }
.room-interaction__option--unavailable strong { color: var(--text-muted); }
.room-interaction__blocked { display: flex; align-items: flex-start; gap: 6px; margin-top: 2px; font-size: 0.76rem; font-weight: 700; color: var(--color-warning-text); line-height: 1.35; }
.camp-colour__note { flex-basis: 100%; color: var(--text-muted); font-size: 0.74rem; line-height: 1.3; }
.room-interaction__cost { flex-basis: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 10px; border: 1px solid var(--color-warning); border-radius: var(--radius-md); background: var(--bg-subtle); font-size: 0.8rem; }
.room-interaction__cost > span { display: flex; flex-direction: column; gap: 2px; }
.room-interaction__cost small { color: var(--text-muted); font-size: 0.74rem; line-height: 1.3; }
.room-interaction__cost select { min-height: 44px; min-width: 180px; }
/* Nested inside a chosen option's own .room-interaction__body (a column flex container),
   not a sibling of .room-interaction__option: the colour picker only ever makes sense
   attached to the option that requires it, so it lives in that option's own card. */
.camp-colour { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 2px; font-size: 0.8rem; color: var(--text-muted); }
.camp-colour select { min-height: 44px; min-width: 110px; }
.route-node__promise { flex-basis: 100%; color: var(--color-primary); font-size: 0.72rem; }
.node-camp { border-color: var(--color-primary); }
.node-camp .route-node__name { color: var(--color-primary); }
@media (max-width: 600px) {
    .room-interaction__option, .room-interaction__cost { flex-basis: 100%; }
    .room-interaction__cost { align-items: stretch; flex-direction: column; }
    .room-interaction__cost select { width: 100%; }
}

/* First-descent onboarding: taught on the real board, inferred from run history, and plain
   markup that the page reads fine without. */
.first-run-tips { border: 1px solid var(--color-primary); border-radius: var(--radius-md); background: var(--bg-subtle); }
.first-run-tips__summary { padding: 10px 14px; min-height: 44px; display: flex; align-items: center; gap: 8px; cursor: pointer; font-weight: 600; }
.first-run-tips__body { padding: 0 14px 12px; }
.first-run-tips__list { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 6px; font-size: 0.86rem; color: var(--text-secondary); line-height: 1.45; }
.first-run-tips__list strong { color: var(--text-primary); }
.first-run-tips__more { margin: 10px 0 0; font-size: 0.8rem; color: var(--text-muted); }

/* ── Debrief: the canonical field report ───────────────────────────────────── */
.debrief { display: flex; flex-direction: column; gap: 16px; }
.debrief__head { padding: 14px 16px; border: 1px solid var(--border-color); border-left: 4px solid var(--border-color); border-radius: var(--radius-md); background: var(--bg-surface); }
.debrief__head--extracted { border-left-color: var(--color-success); }
.debrief__head--failed { border-left-color: var(--color-error); }
.debrief__head--abandoned { border-left-color: var(--text-muted); }
.debrief__verdict { margin: 0; font-size: 1.3rem; font-weight: 700; }
.debrief__reason { margin: 2px 0 10px; font-size: 0.9rem; color: var(--text-secondary); }
.debrief__meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin: 0; }
.debrief__meta dt { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.debrief__meta dd { margin: 0; font-size: 0.9rem; }
.debrief__gone { color: var(--text-muted); font-style: italic; }
.debrief__section { padding: 14px 16px; border: 1px solid var(--border-color); border-radius: var(--radius-md); background: var(--bg-surface); }
.debrief__boon-card + .debrief__boon-card { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border-subtle); }
.debrief__section-title { margin: 0 0 10px; font-size: 1rem; }
.debrief__ledger { display: flex; flex-direction: column; gap: 4px; }
.debrief__ledger-row { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px; padding: 6px 0; border-bottom: 1px solid var(--border-subtle); font-size: 0.9rem; }
.debrief__ledger-row:last-child { border-bottom: 0; }
.debrief__ledger-label { color: var(--text-muted); }
.debrief__ledger-value { font-weight: 600; overflow-wrap: anywhere; }
.debrief__ledger-row--secured .debrief__ledger-value { color: var(--color-success-text); }
.debrief__ledger-row--lost .debrief__ledger-value { color: var(--color-error-text); }
.debrief__steel { margin: 10px 0 0; font-size: 0.86rem; color: var(--text-secondary); }
.debrief__sickness-change { margin-top: 10px; }
.debrief__sickness-change .hint { margin-bottom: 4px; }
.debrief__sickness-list { margin: 0; padding-left: 20px; color: var(--text-secondary); font-size: 0.82rem; line-height: 1.4; }
.debrief__stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin: 0; }
.debrief__stats dt { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.debrief__stats dd { margin: 0; font-size: 1.1rem; font-weight: 700; }
.debrief__route { margin-top: 12px; font-size: 0.86rem; }
.debrief__route > summary { cursor: pointer; min-height: 44px; display: flex; align-items: center; color: var(--text-secondary); }
.debrief__route-list { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 4px; counter-reset: none; }
.debrief__route-entry { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; padding: 6px 8px; background: var(--bg-subtle); border-radius: var(--radius-sm); font-size: 0.82rem; }
.debrief__route-floor { font-family: var(--font-mono); color: var(--text-muted); }
.debrief__route-room { font-weight: 600; }
.debrief__route-entry--clean .debrief__route-outcome { color: var(--color-success-text); }
.debrief__route-entry--forced .debrief__route-outcome { color: var(--color-warning-text); }
.debrief__route-entry--failed .debrief__route-outcome { color: var(--color-error-text); }
.debrief__route-dice { margin-left: auto; color: var(--text-muted); }
.debrief__pets { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.debrief__pet { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 0.88rem; }
.debrief__pet-name { font-weight: 600; }
.debrief__pet-xp { color: var(--color-reward); font-family: var(--font-mono); font-size: 0.82rem; }
.debrief__pet-bonus { color: var(--text-muted); }
.debrief__pet .xp-capped-note { flex-basis: 100%; }
.debrief__actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 18px; }
.debrief__actions .btn { min-height: 44px; }
.debrief__rerun-note { flex-basis: 100%; margin: 0; }

/* Run history page */
.history-reason { margin: 6px 0 0; font-size: 0.82rem; color: var(--text-secondary); }
.history-lost { color: var(--color-error-text); }
.history-row__foot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 8px; }
.history-open { margin-left: auto; font-size: 0.84rem; }
.history-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 16px; }
@media (max-width: 480px) { .history-stats { grid-template-columns: 1fr; } }
.history-stats .stat-sub { font-size: 0.74rem; color: var(--text-muted); }
.history-list { display: flex; flex-direction: column; gap: 12px; }
.history-row { padding: 14px 16px; }
.history-row.history-failed { border-color: var(--color-error-border); }
.history-row.history-extracted { border-color: var(--color-success-border); }
.history-row.history-retreat { border-color: var(--color-warning-border); }
.history-row__head { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.history-badge { padding: 2px 10px; border-radius: var(--radius-pill); font-size: 0.78rem; font-weight: 700; }
.history-badge.badge-extracted { background: var(--color-success-bg); color: var(--color-success-text); border: 1px solid var(--color-success-border); }
.history-badge.badge-failed { background: var(--color-error-bg); color: var(--color-error-text); border: 1px solid var(--color-error-border); }
.history-badge.badge-abandoned { background: var(--bg-subtle); color: var(--text-muted); border: 1px solid var(--border-subtle); }
.history-badge.badge-retreat { background: var(--color-warning-bg); color: var(--color-warning-text); border: 1px solid var(--color-warning-border); }
.history-meta { font-weight: 600; }
.history-date { margin-left: auto; color: var(--text-muted); font-size: 0.82rem; }
.history-rewards { margin: 8px 0 0; color: var(--text-secondary); font-size: 0.9rem; }
.history-stress { margin: 4px 0 0; color: var(--color-error-text); font-size: 0.82rem; }
.history-squad { color: var(--text-secondary); font-size: 0.82rem; }
.history-count { margin: 0 0 12px; color: var(--text-muted); font-size: 0.82rem; }
/* Focus lands here after a page turn (historyPager). Programmatic focus is not reliably
   :focus-visible, so the ring has to hang off plain :focus or the player cannot see where
   they were put: same reason the board's #board-heading carries one. */
.history-count:focus { outline: 2px solid var(--color-primary); outline-offset: 3px; border-radius: var(--radius-sm); }

/* Per-squad retention strip. auto-fit rather than a fixed column count: the number of
   squads is player-driven and uncapped, so a breakpoint per width would be guesswork. */
.squad-stats { margin-bottom: 20px; }
.squad-stats__heading { font-size: 0.9rem; margin: 0 0 8px; }
.squad-stats__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
.squad-stats__note { margin: 8px 0 0; color: var(--text-muted); font-size: 0.78rem; }
.squad-stat-card { padding: 12px 14px; }
.squad-stat-card__name { display: block; font-weight: 700; margin-bottom: 8px; }
.squad-stat-card__figures { display: flex; flex-wrap: wrap; gap: 4px 20px; margin: 0; }
.squad-stat-card__figures dt { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.squad-stat-card__figures dd { margin: 0; font-size: 1.1rem; font-weight: 700; }
.squad-stat-card__sub { display: block; font-size: 0.72rem; font-weight: 400; color: var(--text-muted); }

/* Board: frozen squad roster (who is actually descending) */
.board-squad { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border-subtle); }
.board-squad__title { display: block; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); margin-bottom: 6px; }
.board-squad__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }
.board-squad__pet { display: flex; justify-content: space-between; gap: 10px; font-size: 0.8rem; }
.board-squad__stats { color: var(--text-muted); white-space: nowrap; }
.board-squad__gold { color: var(--color-warning); }
.board-squad__hint { margin: 6px 0 0; font-size: 0.72rem; color: var(--text-muted); }

/* Board: the shared construction goal this run's Star-Steel feeds */
.board-goal { margin-top: 18px; }
.board-goal__pending { margin: 8px 0 0; font-size: 0.85rem; color: var(--text-secondary); }

/* Pathfinder: determined outcome of the staged attempt (see incursionBoard.roomVerdict) */
.attempt-verdict { margin: 4px 0 0; font-size: 0.78rem; line-height: 1.35; font-weight: 600; }
.attempt-verdict.verdict-clear { color: var(--color-success-text); }
.attempt-verdict.verdict-sacrifice { color: var(--color-warning-text); }
.attempt-verdict.verdict-fail { color: var(--color-error-text); }

/* Room lock: live success meter */
.blind-meter { display: flex; flex-direction: column; gap: 4px; }
.meter-bar { height: 8px; background: var(--bg-subtle); border: 1px solid var(--border-subtle); border-radius: var(--radius-pill); overflow: hidden; }
.meter-fill { height: 100%; border-radius: var(--radius-pill); transition: width 0.2s ease, background 0.2s ease; }
.meter-fill.meter-high { background: var(--color-success); }
.meter-fill.meter-mid { background: var(--color-warning); }
.meter-fill.meter-low { background: var(--color-error); }
.meter-label { font-size: 0.78rem; color: var(--text-secondary); }
.meter-clean { color: var(--text-muted); }

/* Desktop: a compact command rail beside the encounter. grid-template-areas rather than
   column order, so the DOM can stay in phone order (HUD -> encounter -> route -> notes)
   while the rail renders on the left. */
@media (min-width: 1024px) {
    .board-layout { grid-template-columns: 260px minmax(0, 1fr); grid-template-areas: "tray main"; }
    .board-main { grid-area: main; }
    .board-rail {
        grid-area: tray;
        position: sticky;
        /* Follows the real header height. It was hardcoded to 76px, the DESKTOP header, so
           on a phone the tray parked a gap below the bar it was supposed to sit under. */
        top: calc(var(--header-height) + 14px);
        max-height: calc(100vh - var(--header-height) - 28px);   /* fallback */
        max-height: calc(100dvh - var(--header-height) - 28px);
        overflow-y: auto;
        display: flex;
        flex-direction: column;
        gap: 12px;
        scrollbar-gutter: stable;
    }
    .encounter__body { padding: 18px 20px; }
}

/* Coarse-pointer floor for the Incursion pillar's own controls.

   This block sits at the END of the incursion styles, not up with the other
   `@media (pointer: coarse)` rules, and that placement is the whole point: most of these
   controls also carry `.btn-xs` / `.btn-sm`, whose `min-height` is declared further down
   the file at the SAME specificity, so a coarse rule written earlier loses the cascade and
   silently does nothing. Browser-measured at 390px: every one of these came back 24-36px
   with the rule in the wrong place. */
@media (pointer: coarse) {
    .slot-arm, .slot-pick, .room-switcher__btn,
    .roster-toggle, .board-header .btn, .preview-member .btn,
    .route-node__detail > summary, .room-interaction__option,
    .camp-colour select { min-height: 44px; }
    .slot-arm, .room-switcher__btn,
    .roster-toggle, .board-header .btn, .preview-member .btn {
        display: inline-flex; align-items: center; justify-content: center;
    }
    .route-node__detail > summary { display: flex; align-items: center; }
}

/* Touch surfaces get the hand beside the lock (.encounter-hand) instead of a fixed drawer.
   The drawer looked useful in a screenshot but covered the room, the route controls and the
   Attempt button the moment a player scrolled, so it was retired here. Desktop keeps the
   sticky rail above.

   This is the ONLY board rule set below 1024px. For two releases a second, earlier
   `@media (width < 1024px)` block built the whole drawer (fixed positioning, a toggle, an
   `is-open` body capped at 30dvh, reserved `.board-shell` padding and matching
   `scroll-margin-bottom` compensation) and this block then set `display: none` over all of
   it, so roughly sixty lines of browser-measured reasoning were dead at every width while
   CLAUDE.md, `_dice_tray.html` and `docs/ux-rules.md` all still described the drawer as the
   phone interaction. Keep phone board rules here; do not open a second block.

   The scroll offset is the one thing that outlived the drawer, re-aimed at what is actually
   fixed on a phone: the bottom tab bar. scroll-into-view knows nothing about a fixed
   overlay, so without this a browser will happily centre a slot behind the bar. */
@media (width < 1024px) {
    .dice-tray { display: none; }
    .incursion-minimap { display: none; }
    .encounter-hand { display: flex; }
    .encounter-retreat { display: flex; }
    .incursion-room-art { aspect-ratio: 16 / 5; max-height: 130px; }
    .encounter,
    .lock-slot-v2,
    .btn-attempt { scroll-margin-bottom: calc(var(--tabbar-height) + 18px); }
}

@media (max-width: 600px) {
    .room-chooser__options { grid-template-columns: 1fr; }
    .room-chooser__option { min-height: 88px; }
    .encounter__body { padding: 12px; }
    .incursion-room-art { max-height: 104px; }
}

/* Desktop command view: keep the decision surface in the first viewport. The original
   desktop composition let the long haul sentence stretch every HUD cell to 170px, then
   wrapped the header actions below the title before a 190px art banner and the lock even
   began. This is a command board, not a gallery: preserve the information, but give the
   room and its next action the visual priority. */
@media (min-width: 1024px) {
    .board-header {
        align-items: flex-start;
        flex-wrap: nowrap;
    }
    .board-header .page-header__titles { flex: 1 1 auto; }
    .board-header .page-header__actions {
        flex: 0 0 min(300px, 31%);
        align-content: flex-start;
    }

    .run-hud { grid-template-columns: repeat(5, minmax(0, 1fr)); }
    .run-hud__cell { min-height: 78px; padding: 7px 10px; }
    .run-hud__value { font-size: 1.05rem; }
    .run-hud__value--haul {
        display: -webkit-box;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
        overflow: hidden;
        font-size: 0.82rem;
        line-height: 1.3;
    }
    .run-hud__sub {
        display: -webkit-box;
        -webkit-box-orient: vertical;
        -webkit-line-clamp: 2;
        overflow: hidden;
    }

    .room-chooser { gap: 7px; }
    .room-chooser__head { gap: 4px 14px; }
    .room-chooser__title { font-size: 0.92rem; }
    .room-chooser__note { font-size: 0.74rem; }
    .room-chooser__options { gap: 6px; }
    .room-chooser__option { min-height: 78px; padding: 7px 10px; gap: 2px; }
    .room-chooser__type,
    .room-chooser__reward,
    .room-chooser__risk,
    .room-chooser__staged { font-size: 0.7rem; }

    .encounter { box-shadow: none; }
    .incursion-room-art { aspect-ratio: 14 / 3; max-height: 112px; }
    .incursion-room-art__glyph { font-size: 2rem; }
    .encounter__body { gap: 8px; padding: 12px 14px; }
    .encounter__title { font-size: 1.02rem; }
    .encounter__instruction { font-size: 0.78rem; }
    .encounter__flavour { font-size: 0.8rem; line-height: 1.35; }
    .encounter__moment { font-size: 0.76rem; }
    .encounter__stakes { flex-direction: row; flex-wrap: wrap; gap: 6px; }
    .encounter__stake {
        flex: 1 1 220px;
        padding: 6px 8px;
        background: var(--bg-subtle);
        border-radius: var(--radius-sm);
    }
    .encounter__stake dt { font-size: 0.64rem; }
    .encounter__stake dd { font-size: 0.78rem; }
    .room-interaction { gap: 6px; }
    .room-interaction__option { padding: 7px 9px; min-height: 44px; }
    .room-interaction__body small { font-size: 0.72rem; }
    .room-lock { gap: 6px; }
    .lock-slot-v2 { gap: 4px; padding: 6px; }
    .slot-stack { min-height: 28px; }
    .btn-attempt { min-height: 40px; }
}

/* ── Incursion cockpit composition ──────────────────────────────────────────
   The board is a sequence, not a stack of equal-weight cards: orient the player, present
   the current room, then let them solve it. The compact route compass preserves the past and
   the destination without making the full graph compete with the active lock. */
.route-compass { display: flex; flex-direction: column; gap: 8px; }
.route-compass__head { display: flex; align-items: end; justify-content: space-between; gap: 12px; }
.route-compass__title { margin: 2px 0 0; font-size: 0.98rem; }
.route-compass__progress { color: var(--text-muted); font-size: 0.76rem; }
.route-compass__track { display: grid; grid-template-columns: minmax(0, 1fr) minmax(190px, 1.2fr) minmax(0, 1fr); gap: 8px; }
.route-compass__zone { display: flex; flex-direction: column; justify-content: center; gap: 4px; min-height: 78px; padding: 8px 10px; background: var(--bg-surface); border: 1px solid var(--border-color); border-radius: var(--radius-md); }
.route-compass__zone--behind { border-left: 3px solid var(--color-success); }
.route-compass__zone--current { border-color: var(--color-primary); background: var(--bg-subtle); text-align: center; }
.route-compass__zone--ahead { border-right: 3px solid var(--color-reward); }
.route-compass__label { color: var(--text-muted); font-size: 0.66rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; }
.route-compass__zone--current .route-compass__label { color: var(--color-primary); }
.route-compass__zone strong { font-size: 0.82rem; }
.route-compass__hint,
.route-compass__empty,
.route-compass__more { color: var(--text-muted); font-size: 0.72rem; }
.route-compass__nodes { display: flex; gap: 4px; min-width: 0; overflow: hidden; }
.route-compass__node { display: inline-flex; align-items: center; gap: 3px; min-width: 0; color: var(--text-secondary); font-size: 0.7rem; white-space: nowrap; }
.route-compass__node span:last-child { overflow: hidden; text-overflow: ellipsis; }
.route-compass__node--failed { color: var(--color-error-ink); text-decoration: line-through; }

/* Route promises: what the next row of rooms is FOR, so a branch choice can be made before
   dice are committed. Lives on the compass rather than the minimap, which deliberately
   carries topology and state rather than room copy. A wrapping row on wide screens, one
   column on a phone, and each promise is plain text: nothing here is carried by colour. */
.route-promises { display: flex; flex-direction: column; gap: 6px; }
.route-promises__label { color: var(--text-muted); font-size: 0.66rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; }
.route-promises__list { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 0; list-style: none; }
.route-promise { flex: 1 1 220px; display: flex; flex-direction: column; gap: 3px; min-width: 0; padding: 8px 10px; background: var(--bg-surface); border: 1px solid var(--border-color); border-left: 3px solid var(--color-primary); border-radius: var(--radius-md); }
.route-promise__head { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
.route-promise__name { font-size: 0.82rem; font-weight: 700; }
.route-promise__kind { padding: 1px 7px; border: 1px solid var(--border-color); border-radius: 999px; background: var(--bg-subtle); color: var(--text-secondary); font-size: 0.66rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; }
.route-promise__text { color: var(--text-secondary); font-size: 0.76rem; line-height: 1.35; }
@media (max-width: 600px) {
    .route-promise { flex-basis: 100%; }
}

.route-details { border: 1px solid var(--border-color); border-radius: var(--radius-md); background: var(--bg-surface); }
.route-details__summary { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 52px; padding: 8px 12px; cursor: pointer; list-style: none; }
.route-details__summary::-webkit-details-marker { display: none; }
.route-details__summary:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
.route-details__summary > span:first-child { display: flex; flex-direction: column; gap: 2px; }
.route-details__summary strong { font-size: 0.86rem; }
.route-details__summary small { color: var(--text-muted); font-size: 0.72rem; }
.route-details__chevron { color: var(--text-muted); font-size: 1.1rem; line-height: 1; transition: transform 0.15s ease; }
.route-details[open] .route-details__chevron { transform: rotate(180deg); }
.route-details > .route { padding: 0 12px 12px; }

.encounter__body { gap: 12px; }
.encounter__brief,
.encounter__solve { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.encounter__solve-head { display: flex; flex-direction: column; gap: 2px; padding-bottom: 8px; border-bottom: 1px solid var(--border-subtle); }
.encounter__solve-head strong { font-size: 0.88rem; }

@media (min-width: 1024px) {
    .route-compass { gap: 4px; }
    .route-compass__head { gap: 8px; }
    .route-compass__title { font-size: 0.9rem; }
    .route-compass__progress { font-size: 0.7rem; }
    .route-compass__zone { min-height: 60px; padding: 6px 8px; }
    .room-chooser { gap: 5px; }
    .room-chooser__head { gap: 2px 12px; }
    .room-chooser__title { font-size: 0.9rem; }
    .room-chooser__option { min-height: 68px; padding: 6px 8px; }

    .board-layout { grid-template-columns: minmax(0, 1fr) 260px; grid-template-areas: "main tray"; }
    .encounter__body { display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(300px, 1.1fr); align-items: start; }
    .encounter__solve { padding-left: 14px; border-left: 1px solid var(--border-subtle); }
    .incursion-room-art { width: 100%; height: 112px; aspect-ratio: auto; }
}

@media (width < 1024px) {
    .encounter__body { display: flex; }
    .encounter__solve { padding-top: 4px; }
    .run-hud { grid-template-columns: repeat(4, minmax(0, 1fr)); }
    .run-hud__cell--haul { grid-column: span 3; }
    .run-hud__value--haul { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
    .run-hud__sub { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
    .incursion-room-art { width: 100%; height: 130px; aspect-ratio: auto; }
}

@media (max-width: 600px) {
    /* Two compact rows keep the route intelligible without forcing the player through a
       three-panel tour before the room. The current position is always first; behind/ahead
       remain visible as labeled context rather than disappearing into a hidden map. */
    .run-hud { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .run-hud__cell--haul { grid-column: span 2; }
    .run-hud__cell { padding: 7px 8px; }
    .run-hud__value--haul { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; overflow: hidden; font-size: 0.78rem; line-height: 1.3; }
    .run-hud__sub { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; overflow: hidden; }

    .route-compass { gap: 6px; }
    .route-compass__head { align-items: baseline; }
    .route-compass__title { font-size: 0.9rem; }
    .route-compass__progress { font-size: 0.7rem; white-space: nowrap; }
    .route-compass__track { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
    .route-compass__zone { min-height: 62px; padding: 7px 8px; }
    .route-compass__zone--current { grid-column: 1 / -1; order: -1; min-height: 58px; }
    .route-compass__zone strong { font-size: 0.78rem; }

    .room-chooser__options { grid-auto-flow: column; grid-auto-columns: minmax(220px, 78%); grid-template-columns: none; overflow-x: auto; padding-bottom: 4px; scroll-snap-type: x proximity; }
    .room-chooser__option { min-height: 118px; scroll-snap-align: start; }
    .route-details__summary { min-height: 48px; }
    .incursion-room-art { height: 104px; }
}

/* Until authored room art exists, the large icon banner is visual noise rather than useful
   information. The room type remains beside the title, so removing this reserved block lets
   the player reach the actual lock/solve controls without changing room identity. */
.incursion-room-art { display: none; }

/* XP / PROGRESSION (pet-detail progression block + Commander panel) */
.progression-xp-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 4px; }
.progression-xp-label { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-muted); }
.progression-xp-value { font-family: var(--font-mono); font-size: 0.82rem; color: var(--text-secondary); }
.ceiling-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-top: 12px; }
.trait-card { display: flex; flex-direction: column; gap: 4px; padding: 12px; border-radius: var(--radius-md); border: 1px solid var(--border-subtle); margin-top: 12px; }
.trait-card__name { font-weight: 700; }
.trait-card__summary { margin: 0; font-family: var(--font-mono); font-size: 0.78rem; color: var(--color-reward); }
.trait-card__link { font-size: 0.78rem; margin-top: 2px; }
.trait-card--inert { border-style: dashed; }
/* Reuses the trait-card chrome: same "one fact about this pet, with its consequence
   spelled out" shape, just tinted since it is a live restriction rather than a passive. */
.void-sickness-card { border-color: var(--color-warning); }
.void-sickness-card .trait-card__name { color: var(--color-warning-text); }
/* Compact trait marker for rosters, pet cards, party pickers and the incursion board.
   The label is always spelled out and the tooltip carries the concrete effect: an
   inert trait is distinguished by the dashed border and the muted color, never by hue
   alone, so it does not depend on color perception to read as "no effect yet". */
.trait-chip {
    display: inline-flex; align-items: center; gap: 3px;
    padding: 1px 7px; border-radius: 999px; white-space: nowrap;
    font-family: var(--font-mono); font-size: 0.72rem; line-height: 1.6;
    color: var(--color-reward);
    border: 1px solid color-mix(in srgb, var(--color-reward) 40%, var(--border-color));
}
.trait-chip--inert {
    color: var(--text-muted);
    border-color: var(--border-subtle);
    border-style: dashed;
}
.squad-traits-line { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 6px 0 0; }
/* .pet-card is ~200px and .trait-chip is nowrap: let a long trait label wrap inside
   the chip there rather than spill past the card border (same reason .state-chip is
   handled specially for that card, see the rule near the top of this file). */
.pet-card > .trait-chip { max-width: 100%; white-space: normal; }
.infusion-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.infusion-actions form { max-width: none; margin: 0; }
.infusion-actions select { margin-right: 6px; }
/* Pet page Progression panel (.prog-*, partials/_pet_progression.html). One column: the
   level strip, the Build block (the panel's one decision), Recalibrate, the trait. From
   720px of panel width (a container query, so the Classic interface's narrower column
   stays single) Build sits left with the trait and Recalibrate stacked right. */
.prog { container-type: inline-size; }
.prog-sick { margin: 0 0 14px; }
.prog-layout { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; }
.prog-note { margin: 4px 0 0; font-size: 0.82rem; line-height: 1.45; color: var(--text-muted); }
.prog-warn { margin: 0; font-size: 0.8rem; }
.prog-level__name { font-family: var(--font-heading); font-size: 1rem; font-weight: 700; color: var(--text-primary); }
.prog-level__cap { margin: 0; font-size: 0.9rem; color: var(--text-secondary); }
.prog-level__cap strong { font-family: var(--font-heading); color: var(--text-primary); }
.prog-xp-bar { display: flex; height: 10px; }
.prog-xp-bar__fill { background: var(--color-primary); }
/* Rested: the stretch of the bar that will fill at the Rested multiplier, drawn ahead of
   the fill. Decoration only; the line under the bar is the explanation. */
.prog-xp-bar__rested {
    height: 100%;
    background: repeating-linear-gradient(135deg,
        color-mix(in srgb, var(--color-primary) 55%, transparent) 0 4px, transparent 4px 7px);
}
.prog-rested { color: var(--text-secondary); }
.prog-incursion { margin-top: 8px; }
.prog-incursion__label { font-size: 0.86rem; }
.prog-build { display: flex; flex-direction: column; gap: 8px; padding-top: 14px; border-top: 1px solid var(--border-subtle); }
.prog-build__headline { margin: 0; font-size: 0.95rem; color: var(--text-primary); }
.prog-build__headline strong { font-family: var(--font-heading); font-size: 1.3rem; color: var(--color-reward); }
.prog-build__ledger { margin: 0; font-size: 0.82rem; color: var(--text-secondary); }
.prog-build .progression-fold { margin-top: 0; }
.prog-stats { list-style: none; margin: 0; padding: 0; }
.prog-stat { display: flex; flex-direction: column; gap: 6px; padding: 10px 0; border-top: 1px solid var(--border-subtle); }
.prog-stat:first-child { border-top: 0; padding-top: 2px; }
.prog-stat__head { display: flex; align-items: center; gap: 10px; min-height: 32px; }
.prog-stat__name { flex: 1 1 auto; font-weight: 700; color: var(--text-primary); }
.prog-stat__value { font-family: var(--font-mono); font-size: 1rem; font-weight: 700; white-space: nowrap; }
.prog-stat__value small { font-weight: 400; color: var(--text-muted); }
.prog-stat__value--capped { color: var(--color-reward); }
.prog-stat__perm { margin-left: 4px; font-size: 0.76rem; font-weight: 400; color: var(--text-secondary); white-space: nowrap; }
.prog-stat__form { max-width: none; margin: 0; }
.prog-stat__add { min-width: 56px; }
/* What a point buys: reference text read once per decision, so small and quiet. */
.prog-stat__effects { margin: 0; font-size: 0.78rem; line-height: 1.4; color: var(--text-muted); }
/* One pip per point up to the ceiling. Permanent points are a diamond, not just a colour. */
.prog-pips { display: flex; gap: 3px; }
.prog-pip { flex: 0 1 12px; min-width: 3px; height: 10px; border-radius: 2px; border: 1px solid transparent; }
.prog-pip--born { background: color-mix(in srgb, var(--text-muted) 60%, transparent); }
.prog-pip--spent { background: var(--color-primary); }
.prog-pip--permanent { background: var(--color-reward); border-radius: 0; clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.prog-pip--room { border-color: var(--border-color); }
.prog-legend { margin: 0; font-size: 0.74rem; color: var(--text-muted); }
.prog-legend__item { display: inline-block; margin-right: 12px; white-space: nowrap; }
.prog-legend .prog-pip { display: inline-block; width: 12px; vertical-align: -1px; margin-right: 4px; }
.prog-recal { display: flex; flex-direction: column; gap: 6px; padding-top: 14px; border-top: 1px dashed var(--border-subtle); }
.prog-recal__title { font-weight: 700; color: var(--text-primary); }
.prog-recal form { max-width: none; margin: 0; }
.prog-recal .btn { align-self: flex-start; }
.prog-recal .progression-fold { margin-top: 0; }
.prog-trait { margin-top: 0; }
.prog-trait__summary { margin: 0; font-size: 0.88rem; font-weight: 600; color: var(--text-primary); }
.prog-trait__form { max-width: none; margin: 6px 0 0; gap: 4px; align-items: flex-start; }
@container (min-width: 720px) {
    .prog-layout {
        grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
        grid-template-areas: "level level" "build trait" "build recal";
        grid-template-rows: auto auto 1fr;
        column-gap: 28px;
    }
    .prog-level {
        grid-area: level; display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
        column-gap: 28px; padding-bottom: 14px; border-bottom: 1px solid var(--border-subtle);
    }
    .prog-level > * { grid-column: 1; }
    .prog-level > .prog-incursion { grid-column: 2; grid-row: 1 / span 3; align-self: start; margin-top: 0; }
    .prog-build { grid-area: build; padding-top: 0; border-top: 0; }
    .prog-trait { grid-area: trait; align-self: start; }
    .prog-recal {
        grid-area: recal; align-self: start; padding: 12px;
        border: 1px solid var(--border-subtle); border-radius: var(--radius-md);
    }
}
@media (pointer: coarse) {
    .prog .btn { min-height: 44px; }
}
/* progressionPanel puts focus back on this heading after an allocate/infuse/recalibrate
   swap. A browser will not reliably treat programmatic focus as :focus-visible, so
   without an explicit ring a keyboard player has no idea where they landed. */
.panel-title:focus { outline: 2px solid var(--color-primary); outline-offset: 3px; }
.progression-danger-zone { margin-top: 16px; padding-top: 16px; border-top: 1px dashed var(--border-subtle); }
/* Last-pet release block: mirrors pets.services.release_pet's guard. */
.progression-warn { margin: 0 0 8px; padding: 8px 10px; font-size: 0.85rem; line-height: 1.4; color: var(--color-warning-text); background: var(--color-warning-bg); border: 1px solid var(--color-warning-border); border-radius: var(--radius-sm); }

.commander-hero { display: flex; flex-wrap: wrap; gap: 20px; align-items: center; justify-content: space-between; }
.commander-unlock-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-top: 14px; }
.commander-unlock-card { display: flex; flex-direction: column; gap: 8px; padding: 14px; border-radius: var(--radius-md); border: 1px solid var(--border-subtle); }
.commander-unlock-card__head { display: flex; align-items: center; gap: 8px; font-weight: 700; }
.commander-unlock-card form { max-width: none; margin: 0; }

/* ===== VISTAS (Log Book) ===== */
.vista-equipped__preview { display: flex; gap: 14px; align-items: center; }
.vista-equipped__preview img { border-radius: var(--radius-md); object-fit: cover; }
.vista-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin-top: 14px; }
.vista-card {
    display: flex; flex-direction: column; gap: 8px; padding: 14px;
    border-radius: var(--radius-md); border: 1px solid var(--border-subtle); text-align: center;
}
.vista-card form { max-width: none; margin: 0; }
.vista-card__art { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--radius-sm); }
.vista-card__name { margin: 0; font-family: var(--font-display); }
.vista-card__flavor { color: var(--text-secondary); font-size: 0.85rem; margin: 0; }
.vista-card--mystery { opacity: 0.6; border-style: dashed; }
.vista-card__silhouette {
    width: 100%; aspect-ratio: 4 / 3; display: flex; align-items: center; justify-content: center;
    font-size: 2.4rem; border-radius: var(--radius-sm); background: color-mix(in srgb, var(--text-secondary) 10%, transparent);
}

