/* App-wide theme, shared by every page (index.php, roster.php, summary.php,
   units.php, admin/*, and the core/ rulebook browser) -- Bootstrap 5.3
   CSS-variable overrides, no Sass recompile needed. Two real palettes now,
   not just a dark starting point:
     Light ("Parchment"): black text on parchment cream, burgundy for
     headers/links/accents -- matches the printed rulebook page itself.
     Dark ("Warhammer Dark"): the OTHER half of the book's own palette --
     dark burgundy surface, gold text/accents (not just a brightened
     light palette), matching the ornamental banner colours. Contrast-
     checked: black-on-parchment ~13.7:1, gold-on-burgundy ~7.9:1, both
     clear of WCAG AAA (7:1) for normal text.
   Toggled via [data-bs-theme] on <html>, set by js/theme.js (localStorage,
   falling back to prefers-color-scheme) -- see that file for the toggle
   button wiring every page shares. */

:root[data-bs-theme="light"] {
    --bs-body-bg: #EDE3CC;
    --bs-body-color: #211A10;
    --bs-emphasis-color: #211A10;
    --bs-secondary-color: #5A4A32;
    --bs-secondary-bg: #E4D8B8;
    --bs-tertiary-bg: #E4D8B8;
    --bs-border-color: #B8A47E;

    --bs-primary: #6B1F2A;
    --bs-primary-rgb: 107, 31, 42;
    --bs-link-color: #6B1F2A;
    --bs-link-color-rgb: 107, 31, 42;
    --bs-link-hover-color: #4A1520;
    --bs-link-hover-color-rgb: 74, 21, 32;

    --bs-accordion-bg: transparent;
    --bs-accordion-active-bg: #E4D8B8;
    --bs-accordion-btn-color: #211A10;
    --bs-accordion-active-color: #6B1F2A;
    --bs-accordion-border-color: #B8A47E;
    --bs-accordion-btn-focus-border-color: #6B1F2A;
    --bs-accordion-btn-focus-box-shadow: 0 0 0 .25rem rgba(107, 31, 42, .25);

    --bs-popover-bg: #E4D8B8;
    --bs-popover-border-color: #B8A47E;
    --bs-popover-header-bg: #E4D8B8;
    --bs-popover-header-color: #6B1F2A;
    --bs-popover-body-color: #211A10;

    --wfb-active-bg: #6B1F2A;
    --wfb-active-color: #EDE3CC;
}

:root:not([data-bs-theme="light"]) {
    --bs-body-bg: #3B0F16;
    --bs-body-color: #D4AF37;
    --bs-emphasis-color: #E8C766;
    --bs-secondary-color: #C79A4B;
    --bs-secondary-bg: #2A0A10;
    --bs-tertiary-bg: #2A0A10;
    --bs-border-color: #6B2331;

    --bs-primary: #D4AF37;
    --bs-primary-rgb: 212, 175, 55;
    --bs-link-color: #E8C766;
    --bs-link-color-rgb: 232, 199, 102;
    --bs-link-hover-color: #F5DA8C;
    --bs-link-hover-color-rgb: 245, 218, 140;

    --bs-accordion-bg: transparent;
    --bs-accordion-active-bg: #2A0A10;
    --bs-accordion-btn-color: #D4AF37;
    --bs-accordion-active-color: #E8C766;
    --bs-accordion-border-color: #6B2331;
    --bs-accordion-btn-focus-border-color: #E8C766;
    --bs-accordion-btn-focus-box-shadow: 0 0 0 .25rem rgba(232, 199, 102, .25);

    --bs-popover-bg: #2A0A10;
    --bs-popover-border-color: #6B2331;
    --bs-popover-header-bg: #2A0A10;
    --bs-popover-header-color: #E8C766;
    --bs-popover-body-color: #D4AF37;

    --wfb-active-bg: #D4AF37;
    --wfb-active-color: #2A0A10;

    /* Bootstrap's own stock danger/info/success (a generic neutral-gray
       dark mode) were never re-tuned for THIS app's specific burgundy/gold
       palette -- confirmed live 2026-08-24 (the new Composition Report
       modal's table-info/table-danger/badge rows "look great in light
       mode, not so much in dark mode"). Stock cyan info/plain red danger
       read as a cold, clashing accent against warm burgundy; these are a
       first pass at warmer equivalents in the same family as the existing
       gold primary/accent colours, not independently contrast-verified
       against a real render -- flagged to the user as a starting point to
       react to, not a finished, checked palette the way the base light/
       dark palette above was.
       --bs-{color}/-rgb drive solid badges (.text-bg-*); the -text-emphasis/
       -bg-subtle/-border-subtle trio drive subtle backgrounds (.table-*,
       most of .alert-*) -- both need overriding for a color to look right
       in every context Bootstrap uses it. */
    --bs-danger: #D9534F;
    --bs-danger-rgb: 217, 83, 79;
    --bs-danger-text-emphasis: #F2A9A0;
    --bs-danger-bg-subtle: #4A1414;
    --bs-danger-border-subtle: #7A2424;

    --bs-info: #C9832E;
    --bs-info-rgb: 201, 131, 46;
    --bs-info-text-emphasis: #E8B366;
    --bs-info-bg-subtle: #4A2E12;
    --bs-info-border-subtle: #6B4A1F;

    --bs-success: #7A9A4A;
    --bs-success-rgb: 122, 154, 74;
    --bs-success-text-emphasis: #C3D69B;
    --bs-success-bg-subtle: #2E3A1A;
    --bs-success-border-subtle: #4A5A28;
}

/* Bootstrap's compiled .table-{color} rules bake a LITERAL hex into their
   own --bs-table-bg/--bs-table-border-color right inside the rule itself
   (not a var(--bs-{color}-bg-subtle) reference) -- so overriding the
   --bs-{color}-bg-subtle variables above (which DID work for the badges,
   since .text-bg-{color} references --bs-{color}/-rgb directly) has no
   effect on a .table-{color} row's own background. Found live 2026-08-24
   (Composition Report modal): the badge picked up the new palette, the row
   background stayed Bootstrap's stock pale cyan. Re-declaring the same
   --bs-table-bg/--bs-table-border-color/color values directly on the
   .table-{color} selectors themselves is the only way to actually change
   them. Same "first pass, not independently verified" caveat as the
   variable overrides above -- see [[wfb_dark_mode_contextual_colors]]. */
:root:not([data-bs-theme="light"]) .table-danger {
    --bs-table-bg: #4A1414;
    --bs-table-border-color: #7A2424;
    color: #F2A9A0;
}
:root:not([data-bs-theme="light"]) .table-info {
    --bs-table-bg: #4A2E12;
    --bs-table-border-color: #6B4A1F;
    color: #E8B366;
}
:root:not([data-bs-theme="light"]) .table-success {
    --bs-table-bg: #2E3A1A;
    --bs-table-border-color: #4A5A28;
    color: #C3D69B;
}

/* Accordion chevron -- Bootstrap bakes the arrow into an SVG background-
   image (currentColor doesn't apply to it), so both states need their own
   explicit re-coloured SVG. Used by the rulebook browser's nav tree. */
:root[data-bs-theme="light"] .accordion-button::after {
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236B1F2A'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
}
:root:not([data-bs-theme="light"]) .accordion-button::after {
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23D4AF37'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
}

/* Replaces the old "Overview" link inside an expanded accordion item --
   an (i) button sits beside the accordion-button itself (so it works even
   collapsed) and pops the parent node's own intro text up in place instead
   of navigating to a separate page. See rb_render_nav_tree() in
   rules-browser.php; popover init lives in js/rules-browser.js since
   Bootstrap doesn't auto-wire popovers from data attributes the way
   collapse/accordion do. */
.rulebook-header-with-info {
    display: flex;
    align-items: stretch;
}
/* Static, pinned full-height sidebar at md+ (matches the col-md-3/col-md-9
   breakpoint the layout already switches to side-by-side at) -- restores
   the same "stays in view while the main content scrolls" behavior the
   plain vh-100/overflow-auto nav had before it became an offcanvas-md
   drawer below that breakpoint, since offcanvas-md itself only handles the
   slide-in/backdrop behavior, not sizing. Below md, the offcanvas markup's
   own default (fixed, full viewport height, own internal scroll while
   open) already does the right thing unassisted -- no override needed
   there, so this rule is scoped to md+ only.

   Found 2026-08-17: this used to target plain .rulebook-sidebar/
   .rulebook-sidebar-body classes, which silently lost to Bootstrap's own
   .offcanvas-md .offcanvas-body reset at the exact same 768px breakpoint
   (bootstrap.css: flex-grow: 0; padding: 0; overflow-y: visible;
   background-color: transparent !important;) -- that compound selector
   outranks a single class regardless of load order, so nothing in this
   block was ever actually taking effect: no scroll clipping (hence no
   scrollbar and the sidebar just flowing away with the page instead of
   staying pinned), no padding (hence mb-5/pb-5 both doing nothing -- there
   was no box to pad), and the background silently forced transparent.
   Rewritten against #rulebookSidebar (an id beats any number of classes)
   with !important only where needed to beat Bootstrap's own !important.

   Second bug found same day, after the above alone still didn't produce a
   scrollbar: checked the FULL bootstrap.css block (not just the fragment
   read the first time) and .offcanvas-md never sets display:flex on the
   OUTER element at desktop width at all -- that only happens inside the
   mobile (max-width) drawer-mode block, which doesn't apply here. Without
   an explicit flex container, flex-grow on .offcanvas-body is a no-op (flex
   properties do nothing on a child of a non-flex parent), so it was never
   actually being sized to fill the 100vh parent -- it just sat at its own
   natural content height with nothing to overflow, hence no scrollbar even
   though the box was otherwise styled correctly (padding/background). Fixed
   by explicitly making #rulebookSidebar a flex column itself AND giving the
   body a flex-independent height:100% as a non-flex-reliant fallback. */
@media (min-width: 768px) {
    #rulebookSidebar {
        display: flex;
        flex-direction: column;
        height: 100vh;
        background-color: var(--bs-tertiary-bg) !important;
    }
    #rulebookSidebar .offcanvas-body {
        display: flex;
        flex-direction: column;
        flex-grow: 1;
        height: 100%;
        overflow-y: auto;
        padding: 1rem 0 3rem;
        background-color: var(--bs-tertiary-bg) !important;
    }
}

/* Same fix, same reason, for roster.php's own offcanvas-md sidebar (the
   "Add to Roster" nav) -- see #rulebookSidebar's own comment just above for
   why a plain class-based rule silently loses to Bootstrap's own
   .offcanvas-md .offcanvas-body reset at this exact breakpoint. Replaces
   the old .sidebar class's vh-100/sticky/overflow-auto rule (css/units.css),
   which was never gated to md+ and was what actually caused the mobile
   scroll-overlap bug this whole offcanvas conversion fixes -- see roster.php.
   .sidebar itself is left alone since admin/index.php and units.php still
   use it unmodified, without an offcanvas wrapper. */
@media (min-width: 768px) {
    #rosterSidebar {
        display: flex;
        flex-direction: column;
        height: 100vh;
        background-color: var(--bs-tertiary-bg) !important;
    }
    #rosterSidebar .offcanvas-body {
        display: flex;
        flex-direction: column;
        flex-grow: 1;
        height: 100%;
        overflow-y: auto;
        padding: 1rem 0 3rem;
        background-color: var(--bs-tertiary-bg) !important;
    }
}

/* Same fix, same reason, for armybooks-browser.php's own offcanvas-md army
   picker -- see #rulebookSidebar's own comment above for the full
   explanation of why a plain class-based rule loses to Bootstrap's own
   .offcanvas-md .offcanvas-body reset at this exact breakpoint. */
@media (min-width: 768px) {
    #armybooksSidebar {
        display: flex;
        flex-direction: column;
        height: 100vh;
        background-color: var(--bs-tertiary-bg) !important;
    }
    #armybooksSidebar .offcanvas-body {
        display: flex;
        flex-direction: column;
        flex-grow: 1;
        height: 100%;
        overflow-y: auto;
        padding: 1rem 0 3rem;
        background-color: var(--bs-tertiary-bg) !important;
    }
}

/* render_unit_stat_table()/render_armybook_summary_table()'s shared
   .stat-block table class -- vertical-center each cell against the row's
   tallest content (a wrapped unit name, a multi-line rule badge, etc.).
   Already defined in css/units.css for units.php's own standalone page;
   duplicated here since index.php (the Armybooks tab) loads theme.css
   instead, not that file. */
table.stat-block th,
table.stat-block td {
    vertical-align: middle;
}

/* armybooks-browser.php's own summary-grid table -- a lightly shaded
   background on the header row and the Name column (both are <th>
   elements already -- the Name column is a row header, scope="row") so
   both read at a glance as labels rather than data cells. Same
   bs-secondary-bg token css/units.css already uses for
   .list-group-item-action's own "make it visibly distinct from the page"
   look, so this reuses the app's existing "shaded" vocabulary rather than
   inventing a new tone. Requested by the user 2026-08-22. */
table.armybook-summary-table thead th,
table.armybook-summary-table tbody th {
    background-color: var(--bs-secondary-bg);
}

/* RaceHandler::armybookNotesHtml()'s own bordered/collapsible note block
   (e.g. Dwarfs' "Runes" -- class/Dwarfs.php) -- <details>/<summary> is the
   native, no-JS collapse mechanism (same one campaigns-browser.php's own
   "Campaign Rules" section already uses), styled here to read as a
   bordered fieldset with its <summary> acting as the clickable legend, per
   the user's own request 2026-08-22. */
.armybook-race-notes {
    border: 1px solid var(--bs-border-color);
    border-radius: var(--bs-border-radius);
    padding: 0.75rem 1rem;
}

.armybook-race-notes > summary {
    font-weight: 600;
    cursor: pointer;
    list-style: revert;
}

.armybook-race-notes:not([open]) > summary {
    margin-bottom: 0;
}

.armybook-race-notes[open] > summary {
    margin-bottom: 0.5rem;
}

.armybook-race-notes-body :last-child {
    margin-bottom: 0;
}

.rulebook-header-with-info .accordion-button {
    flex: 1 1 auto;
    min-width: 0;
}
.rulebook-info-trigger {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    border: none;
    border-left: 1px solid var(--bs-accordion-border-color);
    background-color: transparent;
    padding: 0 .9rem;
    font-size: 1.0rem;
    color: var(--bs-secondary-color);
}
.rulebook-info-trigger:hover, .rulebook-info-trigger:focus {
    color: var(--bs-primary);
    outline: none;
}
.popover {
    font-family: 'Lora', Georgia, serif;
    max-width: 320px;
}
.popover-body p:last-child { margin-bottom: 0; }

/* Sidebar/breadcrumb "you are here" indicator in the rulebook browser --
   a dedicated class instead of Bootstrap's bg-info-subtle (tied to the
   "info" role used elsewhere for unrelated default component semantics). */
.wfb-active {
    background-color: var(--wfb-active-bg) !important;
    color: var(--wfb-active-color) !important;
}

body, p, .accordion-body, .form-control {
    font-family: 'Lora', Georgia, serif;
}
h1, h2, h3, h4, h5, h6,
.accordion-button, .navbar-brand {
    font-family: 'Cinzel', Georgia, serif;
    letter-spacing: .02em;
}

/* Navbar brand mark -- plain <span>s rather than the <h1>+<div> the markup
   used to nest inside the <a>: a page's own content already has its real
   <h1> (e.g. "Rulebook"), so a second one decorating the nav brand was a
   duplicate top-level heading, not just a styling wart. Cinzel is a
   unicase-leaning display face (lowercase renders as smaller caps, not a
   true lowercase form), which is why fw-italic on it read poorly before --
   letter-spacing/color/size carry the distinction instead. */
.wfb-brand-title {
    font-family: 'Cinzel', Georgia, serif;
    font-weight: 700;
    letter-spacing: .04em;
    color: var(--bs-primary);
}
.wfb-brand-subtitle {
    font-family: 'Cinzel', Georgia, serif;
    font-size: .68rem;
    font-weight: 500;
    letter-spacing: .14em;
    color: var(--bs-secondary-color);
    margin-top: .2rem;
}
.wfb-brand-disclaimer {
    font-family: system-ui, -apple-system, sans-serif;
    font-size: .6rem;
    font-weight: 400;
    letter-spacing: normal;
    color: var(--bs-secondary-color);
    opacity: .7;
    margin-top: .15rem;
}

/* Custom navbar-expand breakpoint (2026-09-01, day-before-launch mobile
   fix) -- partials/nav-header.php's own six-tab nav-underline list doesn't
   fit on one line until ~1050px; Bootstrap's own navbar-expand-md (768px)
   left it wrapping onto two rows from 768px up to that point, but Bootstrap
   ships no utility class for a custom pixel breakpoint, so this replicates
   the small subset of its own .navbar-expand-{breakpoint} media query that
   actually matters here. Scoped to .wfb-navbar-expand (not .navbar-expand-md
   itself) so nothing else on the site using a real Bootstrap breakpoint is
   affected.
   The tab list itself is a plain Bootstrap .nav (nav-underline), not the
   .navbar-nav Bootstrap's own expand mixin targets -- .navbar-nav defaults
   to a vertical column and only Bootstrap's OWN expand breakpoint flips it
   to a row, but .nav defaults to a horizontal, wrapping row always, at
   every width. Below 1050px that meant tapping the hamburger revealed the
   tabs laid out sideways (too wide for a phone, wrapping badly) instead of
   stacking as a dropdown list -- found live 2026-09-01, the SAME day as
   the breakpoint fix above (this is the other half of it: the media query
   controlled whether the tabs show at all, but nothing controlled their
   OWN direction once shown). */
#wfb-nav-links .nav {
    flex-direction: column;
}
@media (min-width: 1050px) {
    .wfb-navbar-expand {
        flex-wrap: nowrap;
    }
    #wfb-nav-links .nav {
        flex-direction: row;
    }
    .wfb-navbar-expand .navbar-collapse {
        display: flex !important;
        flex-basis: auto;
        /* Bootstrap's own .navbar-collapse base rule (unconditional, not
           gated behind any breakpoint) also sets flex-grow: 1 -- fine on
           its own, but combined with the order swap just below it meant
           this box greedily claimed ALL the row's free space before
           .wfb-nav-buttons's own margin-left: auto ever got a chance to
           claim any of it, leaving nothing to actually push the buttons
           over to the right edge (found live 2026-09-01: the buttons sat
           immediately after the tabs instead of at the end of the row).
           Zeroed out so this box is only ever as wide as the tabs
           themselves -- the buttons' own auto margin below is what
           creates the "everything else on the left, this flush right"
           split instead. */
        flex-grow: 0;
        /* #wfb-nav-links's own mb-2 (nav-header.php) is only there to space
           the tabs away from the button row when the two are STACKED below
           this breakpoint -- inline in the same row up here, it would just
           nudge the tabs off-center against .align-items-center's own
           vertical centering, so it's zeroed out again for this range. */
        margin-bottom: 0 !important;
    }
    .wfb-navbar-expand .navbar-toggler {
        display: none;
    }
    /* DOM order is buttons-then-tabs (partials/nav-header.php's own
       .wfb-nav-buttons cluster comes before #wfb-nav-links -- see that
       file's own comment on why, a mobile-only concern), but the desktop
       row should still read brand -> tabs -> buttons. `order` flips the
       PAINT sequence back (doesn't touch DOM/tab order, so the mobile fix
       above is untouched); margin-left: auto on the buttons cluster is
       what actually pins it to the row's right edge, same standard
       "everything left, one thing flush right" flexbox pattern used
       everywhere else pinning something to a row's far end. */
    #wfb-nav-links {
        order: 1;
    }
    .wfb-nav-buttons {
        order: 2;
        margin-left: auto;
    }
}

.rulebook-figure {
    margin: 1.25rem 0;
    padding: .75rem;
    border: 1px solid var(--bs-border-color);
    background-color: var(--bs-tertiary-bg);
    text-align: center;
}
/* 40%, not 100% -- these diagrams are simple formation/movement squares
   (bold shapes, one large numeral, no fine detail), and native sizes run
   ~620-1475px wide, so full width dominates the reading column for no real
   benefit. Same reasoning/value as print/rulebook.php's own copy of this
   rule (2026-08-28) -- that page can't share this stylesheet (no shared
   chrome), so it carries its own equivalent. */
.rulebook-figure img { max-width: 40%; height: auto; }
.rulebook-figure figcaption {
    margin-top: .5rem;
    font-size: .9rem;
    color: var(--bs-secondary-color);
    font-style: italic;
}

/* Boxes off one merged sub-topic on a rulebook page that combines several
   short entries into one page (e.g. Dice, Characteristics) -- Bootstrap's
   Reboot turns <legend> into a full-width block by default (form-label
   styling), so float/width are reset here to get the classic "caption cut
   into the border" look instead. */
.rulebook-content fieldset {
    margin: 1.5rem 0;
    padding: .25rem 1rem 1rem;
    border: 1px solid var(--bs-border-color);
    border-radius: .25rem;
}
.rulebook-content fieldset:first-of-type { margin-top: 1rem; }
.rulebook-content legend {
    float: none;
    width: auto;
    margin-bottom: 0;
    padding: 0 .5rem;
    font-family: 'Cinzel', Georgia, serif;
    font-size: 1.1rem;
    letter-spacing: .02em;
    color: var(--bs-primary);
}
.rulebook-content fieldset > *:last-child { margin-bottom: 0; }

/* Campaigns quest page's scenario badge (campaigns-browser.php) -- bg is
   plain .text-bg-primary, which hardcodes white text at compile time and
   doesn't know --bs-primary has been swapped to a light gold in dark theme
   (Bootstrap's own compiled utility, not something a CSS-variable-only
   theme override can reach). Both palettes are mirror images of each other
   though (see this file's header comment), so each theme's own body
   background is already the correctly-contrasting color to use as this
   badge's text: cream-on-maroon in light theme, dark-burgundy-on-gold in
   dark theme -- one rule, no separate per-theme blocks needed. */
.campaign-scenario-badge { color: var(--bs-body-bg) !important; }

/* Hand-aligned pipe tables in campaign quest text (campaign_render_text()'s
   own <pre class="campaign-table">) -- boxed like .rulebook-content's own
   fieldsets, overflow-x scrolls instead of forcing the page itself wide
   when a table's too wide for a narrow viewport. */
.campaign-table {
    overflow-x: auto;
    margin: 1rem 0;
    padding: .75rem 1rem;
    border: 1px solid var(--bs-border-color);
    border-radius: .25rem;
    background: var(--bs-tertiary-bg);
}

/* Full-page block shown while an Add/Remove roster request is in flight
   (js/roster.js) -- covers the whole viewport so a spam-click on "Add to
   Roster" (or anything else) can't fire a second request before the first
   one's response has replaced #roster-panel. */
.roster-busy-overlay {
    position: fixed;
    inset: 0;
    z-index: 2000; /* above Bootstrap's own modal backdrop (1050) and modals (1055) */
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(23, 16, 11, 0.6);
}

/* Draws the eye to the Army Size input while it's empty -- composition
   checks (Core/Special/Rare/Lord/Hero %) are meaningless with no declared
   point limit, so Roster::validate() shows one explanatory violation
   instead of misleading per-category ones (see class/Roster.php). This is
   the visual half of that: :placeholder-shown is true exactly while the
   field is empty, so the glow appears/disappears on its own the moment a
   value is typed or cleared, no JS needed. */
#roster-points-limit:placeholder-shown {
    border-color: var(--bs-danger, #dc3545);
    box-shadow: 0 0 6px 1px rgba(220, 53, 69, 0.7);
    animation: roster-points-limit-glow 1.6s ease-in-out infinite;
}
@keyframes roster-points-limit-glow {
    0%, 100% { box-shadow: 0 0 5px 0px rgba(220, 53, 69, 0.55); }
    50%      { box-shadow: 0 0 10px 3px rgba(220, 53, 69, 0.9); }
}

/* Rosters dashboard armybook grouping (index.php) -- only shown once a
   player has rosters in 2+ different armybooks (index.php's own
   $groupByArmy). One tile per armybook opens that army's own modal
   (#army-rosters-modal-<id>) instead of expanding inline -- a modal never
   affects the tile grid's own row height, so N tiles per row stays trivial
   regardless of how long any one army's roster list is (the inline-
   collapsible-card layout this replaced would have warped its own grid
   row every time one card opened). */
.army-tile {
    background-color: var(--bs-tertiary-bg);
    border: 1px solid var(--bs-border-color);
    color: var(--bs-body-color);
    padding: .75rem 1rem;
    transition: transform .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.army-tile:hover, .army-tile:focus {
    transform: translateY(-2px);
    box-shadow: 0 .35rem .75rem rgba(0, 0, 0, .25);
    border-color: var(--bs-primary);
}

/* Roster rows -- both the flat dashboard list and each armybook modal's own
   list share this (render_roster_row() in index.php). A softer, animated
   hover than Bootstrap's flat .list-group-item-action background snap, plus
   the Copy/Delete icons only reaching full opacity on hover so a row reads
   clean at rest instead of every row showing two icons all the time. */
.roster-list-item {
    transition: background-color .15s ease, padding-left .15s ease;
}
.roster-list-item:hover {
    /* Bootstrap's own .bg-body-tertiary utility (already on this row, its
       resting-state background) sets background-color with !important --
       plain specificity can't win against that, so this needs it too. */
    background-color: var(--bs-secondary-bg) !important;
    padding-left: 1.25rem;
}
.roster-row-action {
    opacity: .35;
    transition: opacity .15s ease;
}
.roster-list-item:hover .roster-row-action,
.roster-row-action:focus {
    opacity: 1;
}

/* Staggered fade/slide-in for a just-opened armybook modal's own roster
   list -- js/index.js's shown.bs.modal handler toggles .stagger-in off then
   back on (a class left in place from a previous open only plays the CSS
   animation once). Delay is keyed by nth-child rather than JS-computed
   inline style, capped at 6 rows -- past that the stagger would just read
   as lag, not polish. */
.roster-modal-list.stagger-in .roster-list-item {
    animation: roster-row-in .25s ease both;
}
.roster-modal-list .roster-list-item:nth-child(1) { animation-delay: .02s; }
.roster-modal-list .roster-list-item:nth-child(2) { animation-delay: .06s; }
.roster-modal-list .roster-list-item:nth-child(3) { animation-delay: .10s; }
.roster-modal-list .roster-list-item:nth-child(4) { animation-delay: .14s; }
.roster-modal-list .roster-list-item:nth-child(5) { animation-delay: .18s; }
.roster-modal-list .roster-list-item:nth-child(n+6) { animation-delay: .22s; }
@keyframes roster-row-in {
    from { opacity: 0; transform: translateY(-4px); }
    to   { opacity: 1; transform: translateY(0); }
}

/* partials/footer.php's own icon-credit rotator (2026-08-28) -- every
   credited icon is stacked in the same box via absolute positioning, only
   .active is opaque; the JS alongside it just toggles which one carries
   that class every 2s. */
.wfb-credits-rotator {
    position: relative;
    display: inline-block;
    width: 1.5rem;
    height: 1.5rem;
    vertical-align: middle;
}
.wfb-credits-rotator-icon {
    position: absolute;
    inset: 0;
    opacity: 0;
    transition: opacity .4s ease;
}
.wfb-credits-rotator-icon.active {
    opacity: 1;
}
