/* =====================================================================
   global.css — site-wide shared styles (gp- prefixed)
   ---------------------------------------------------------------------
   Part of the incremental split of the site.css monolith.
   Holds global styles used across multiple pages. By convention these
   are the gp- prefixed classes/styles.

   Loaded after site.css but before the theme files, mirroring the
   cascade slot these rules held while inside site.css.

   Populated incrementally: when working on a page, move that page's
   gp- prefixed global styles out of site.css into here. A non-gp style
   used on more than one page must be raised with the user before moving.
   ===================================================================== */

/* ---------------------------------------------------------------------
   Modal overlay + dialog — shared, reusable (Session 162).
   Modelled on the existing #force-logout-overlay/#force-logout-dialog,
   which could later be migrated onto these classes.
   --------------------------------------------------------------------- */
.gp-modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.65);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 9999;
    padding: 1rem;
}

.gp-modal {
    background: var(--bg-3);
    border: 1px solid var(--line);
    border-radius: 0.375rem;
    padding: 1.5rem 1.75rem;
    max-width: 28rem;
    width: 100%;
}

.gp-modal h3 {
    margin: 0 0 0.75rem;
    font-size: 1.1rem;
}

.gp-modal-sub {
    color: var(--ink-3);
    margin: 0 0 1rem;
    font-size: 0.9rem;
}

.gp-modal textarea {
    width: 100%;
    box-sizing: border-box;
    resize: vertical;
    padding: 0.5em 0.625em;
    border: 1px solid var(--line);
    border-radius: 0.375rem;
    background: var(--bg-2);
    color: var(--ink);
    font: inherit;
}

.gp-modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
    margin-top: 1.25rem;
}

/* ---------------------------------------------------------------------
   The guarded field — a pill that enables the control beneath it
   (Session 222; first used by Shared/_RemoveMemberModal.cshtml).

   LAYOUT ONLY, BY INSTRUCTION: the field must not look different from
   the site's general styles. Every element keeps its standard form — the
   small checkbox pill, .role-detail-label, the global select and
   textarea rules including their :disabled dimming. These rules only
   place them.

   SCOPED TO .gp-modal-field ON PURPOSE. A bare ".gp-modal select" rule
   would widen the select in every other modal on the site as well.
   --------------------------------------------------------------------- */
.gp-modal-field {
    margin-top: 1rem;
}

.gp-modal-field-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.375rem;
}

/* A flex item: without this the pill stretches across the row (UI/TogglePill.md). */
.gp-modal-field-head .toggle-pill-check {
    flex: 0 0 auto;
}

/* .role-detail-label carries a top margin for its usual place above a
   field; beside the pill it would push the label below the pill's centre. */
.gp-modal-field-head .role-detail-label {
    margin-top: 0;
}

.gp-modal-field select {
    width: 100%;
}

/* =====================================================================
   .gp-select-tree — hierarchical selection tree, a .data-table variant.
   A checkbox column, an indented label, and a right-aligned count per row,
   with group heading rows that select a whole branch.
   Depth is a class on the row, not nested markup: the tree is at most four
   levels and nesting <table>s or <ul>s would buy nothing while losing every
   behaviour .data-table already provides.
   Indent steps are rem — page-level spacing, not component-internal (UI/Units.md).
   ===================================================================== */
.gp-select-tree .gp-tree-count {
    width: 1px;
    white-space: nowrap;
    text-align: right;
    padding-right: 1rem;
    color: var(--ink-3);
}

/* Group heading row. Same typographic recipe as other table group rows
   (uppercase, muted, on --bg-3) rather than a second look for the same job. */
.gp-select-tree .gp-tree-group td {
    font-size: 0.688rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.031rem;
    color: var(--ink-3);
    background: var(--bg-3);
}
.gp-select-tree .gp-tree-group:hover {
    background-image: none;
}

/* Depth indent — applied to the label cell, which is always the second column. */
/*.gp-select-tree .gp-tree-d1 td:nth-child(2) { padding-left: 1rem; }
.gp-select-tree .gp-tree-d2 td:nth-child(2) { padding-left: 2rem; }
.gp-select-tree .gp-tree-d3 td:nth-child(2) { padding-left: 3rem; }
.gp-select-tree .gp-tree-d4 td:nth-child(2) { padding-left: 4rem; }*/

/* Depth indent and depth dimming, both on the label cell (always the second column).
   Each tier steps once down the ink scale, so nesting reads as depth even when the indent
   alone is ambiguous. Tier 1 is left alone so it keeps the table's own text colour; tier 3
   lands on --ink-3, which is exactly what .text-muted resolves to.

   The named scale is what makes this theme-safe: every theme sets its own direction for the
   four inks (dark #e7e9ee -> #4a5063, light #1a1a1a -> #909090), so "one step dimmer" stays
   dimmer against either background. An opacity or a fixed colour would invert on half of them. */
.gp-select-tree .gp-tree-d1 td:nth-child(2) { padding-left: 1rem; }
.gp-select-tree .gp-tree-d2 td:nth-child(2) { padding-left: 2rem; color: var(--ink-2); }
.gp-select-tree .gp-tree-d3 td:nth-child(2) { padding-left: 3rem; color: var(--ink-3); }
.gp-select-tree .gp-tree-d4 td:nth-child(2) { padding-left: 4rem; color: var(--ink-4); }

/*  Superseded: warnings used to be injected as their own row beneath the offending item, which
    made the table grow and shrink as the selection changed. They now share the note column.
.gp-select-tree .gp-tree-warning td {
    font-size: 0.75rem;
    color: var(--warn);
    padding-top: 0;
    border-top: 0;
}
.gp-select-tree .gp-tree-warning:hover {
    background-image: none;
}
*/

/* Name column shrinks to its content. That leaves the note column as the only one carrying no
   width hint, so it absorbs the remaining width — the filler-column mechanism in UI/Tables.md,
   with the note playing the filler's part. nowrap keeps a name on one line; the depth-indent
   padding above is inside this cell's content width, so indentation still works. */
.gp-select-tree td:nth-child(2) {
    width: 1px;
    white-space: nowrap;
}

/* Note column — a standing description of what the row deletes, replaced in place by a live
   warning when the current selection produces one. Secondary to both the label and the count,
   so it stays muted and small; the warning modifier is the only thing that draws the eye.
   Deliberately no width: it is the column that takes the slack. */
.gp-select-tree .gp-tree-note {
    font-size: 0.75rem;
    color: var(--ink-3);
    text-align: left;
    white-space: normal;
}

.gp-select-tree .gp-tree-note-warning {
    color: var(--warn);
}

/* =====================================================================
   .gp-outline - an expandable outline list.

   A hierarchy of BOXES, one per item TITLE, each stretching the full width of
   its parent and each tier stepping in. Use it where a tree has to be opened
   and read rather than scanned or selected.

   THE BOX IS THE HEAD, NOT THE ITEM - see .gp-outline-head below. An item
   contains its children, so bordering the item would wrap the whole open
   subtree.

   BUILT ON <details>/<summary>, NOT ON A CLICK HANDLER. Expansion is the
   element's own behaviour, so this component ships NO JavaScript: it needs no
   initialiser, it survives being swapped into the page by gpAjax with nothing
   to re-bind, and it is keyboard- and screen-reader-operable for free. Every
   other expandable in the portal predates this and uses a handler; that is not
   a reason for the next one to.

   HOW IT DIFFERS FROM UI/Tables.md's D2 EXPANDABLE ROW, which remains the
   standard INSIDE a data table: D2 draws a spine and a notch down a dedicated
   .col-expand column, and its rows are cells in a shared grid. This draws a
   bordered box per title and nests them, so depth is structural rather than
   indicated. Use D2 when the items are ROWS OF A TABLE; use this when they are
   a nested outline that happens to be on a page.

   HOW IT DIFFERS FROM .gp-select-tree above: that is a flat .data-table whose
   depth is a class (gp-tree-d1..d4) and whose tiers dim one step down the ink
   scale, because indent alone is ambiguous in a flat row list and the depth is
   capped at four. Here nesting is real markup, depth is unbounded, and boxes
   inside boxes already read as depth - so the tiers deliberately do NOT dim.
   Dimming would only make the deepest text hardest to read.

   THE EXPAND GLYPH IS AT THE END OF A ROW, not the start - see the head rules
   below, and note the deliberate divergence from UI/Buttons.md's direction
   table recorded there.

   ONE KNOWN BEHAVIOUR: a link inside a head both follows and toggles, because
   toggling is <summary>'s default action and the anchor's is navigation. With
   target="_blank" that reads as "opened in a tab, and opened the section too",
   which is tolerable. A click guard would cost this component its no-JS
   property, so it is documented rather than fixed.
   ===================================================================== */
.gp-outline {
    /* No layout of its own - items are block boxes in normal flow. Declared so
       the container is a real, findable thing rather than an implied wrapper. */
    display: block;

    /* 0.813rem IS .data-table's SIZE, AND THAT IS THE POINT. This component
       stands in for expandable tables, so an outline must never read LARGER
       than the table it replaces. Without this line it inherits body's 1rem,
       which is a visible jump. */
    font-size: 0.813rem;
}

/* EVERY TIER BELOW THE FIRST, at 0.75rem. This is not a per-level step: the
   descendant selector matches depth 2, 3, 4 and beyond alike, so all nested
   content is one size.

   IT REPRODUCES THE OLD BUILD EXACTLY. `.detail-row .data-table` was 0.75rem
   for every nested table however deep, and the same shape here means no item
   at any depth is larger than it used to be.

   rem AND NEVER em, which is what makes "not a step" true. An em size would
   compound once per tier and a depth-four item would be unreadable; rem is
   absolute, so this rule applying twice over is the same as applying once. */
.gp-outline .gp-outline {
    font-size: 0.75rem;
}

/* One item. It carries NO BORDER OF ITS OWN - only spacing to the next.

   THE BOX IS THE HEAD, NOT THE ITEM. An item contains its children, so a border
   here would wrap the whole open subtree and the list would read as boxes
   inside boxes inside boxes. Bordering the head instead means every title gets
   the same box whatever its depth, and an open item shows its children as
   siblings below it rather than as contents of it. */
.gp-outline-item {
    margin-bottom: 0.25rem;
}

/* AN OPEN ITEM GETS A BIGGER GAP AFTER IT, and the pairing with the tight gap
   above its children is the whole point: a child should sit close to the title
   it belongs to, and the NEXT title should sit far enough away to read as a new
   branch rather than as one more child of the previous one.

   KEYED ON [open] SO IT ONLY APPEARS WHEN CHILDREN ARE SHOWING. A collapsed
   item is just another row in the list and takes the ordinary gap; there is no
   subtree beneath it to separate from what follows.

   Margins collapse through the body (no bottom padding, no border), so this
   value - not the sum of it and the last child's - is what actually shows. */
.gp-outline-item[open] {
    margin-bottom: 1rem;
}

/* After [open] deliberately: both selectors are (0,2,0), so source order is
   what makes the last item flush whether it is open or closed. */
.gp-outline-item:last-child {
    margin-bottom: 0;
}

/* The item's title line, and THE BOX. Also the click target when expandable. */
.gp-outline-head {
    border: 1px solid var(--line);
    border-radius: 0.25rem;
    background: var(--bg-2);
    padding: 0.5rem 0.75rem;
    font-weight: 600;
    color: var(--ink);

    /* NOT SELECTABLE, because the head is a click target. Opening and closing an
       item is a double-click on the same words, and without this the second
       click selects the title - so exploring a tree leaves a trail of
       highlighted text behind it.

       THE HEAD ONLY. .gp-outline-body is deliberately left selectable: the
       prose inside an item is content someone may want to copy, and nothing
       there is a click target.

       -webkit- prefix included: Safari still requires it. */
    -webkit-user-select: none;
    user-select: none;
}

/* list-style:none plus the -webkit- pseudo-element removes the UA's own
   disclosure triangle in every engine; the arrowhead below replaces it so the
   glyph matches every other expander in the portal. */
/* THE EXPAND GLYPH SITS AT THE END OF THE ROW, NOT THE START.

   It was dropped from the left first, because an arrowhead in front of every
   title is noise down a list where the boxes already carry the structure. At
   the trailing edge it is out of the text's way, aligns down the right margin,
   and still answers the one question the box cannot: whether this row opens.

   ONLY <summary> GETS IT. A static leaf is a <div> and matches none of these
   rules, so an arrow appears on exactly the rows that expand.

   list-style AND ::-webkit-details-marker STILL HAVE TO GO regardless. They
   suppress the UA's OWN disclosure triangle, which is a separate thing from
   ours - keeping ours does not make them unnecessary, and dropping them puts a
   second marker at the START of every row. */
summary.gp-outline-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    cursor: pointer;
    list-style: none;
}

summary.gp-outline-head::-webkit-details-marker {
    display: none;
}

/* DOWN WHEN CLOSED, UP WHEN OPEN - the arrow points at what the click will do.
   NOTE THIS IS A DELIBERATE DIVERGENCE from UI/Buttons.md's usage table, which
   pairs an expandable row as right-when-collapsed / down-when-expanded. The
   GLYPH FAMILY is that document's (the equilateral arrowheads, U+2B9C-2B9F);
   only the direction differs, and it was asked for explicitly.

   flex:none so a long title cannot squeeze the glyph out. */
summary.gp-outline-head::after {
    content: "\2B9F";
    font-size: 0.625rem;
    color: var(--ink-3);
    flex: none;
}

.gp-outline-item[open] > summary.gp-outline-head::after {
    content: "\2B9D";
}

summary.gp-outline-head:hover {
    background: linear-gradient(to right, rgba(255, 255, 255, 0.04), transparent);
}

/* A leaf. Same box, default cursor - it opens onto nothing, so it must not
   read as a control. */
.gp-outline-item-static > .gp-outline-head {
    cursor: default;
}

/* The opened content, OUTSIDE the head's box.

   The top padding is the gap below the head and is deliberately TIGHT - a
   child belongs to the title above it, and the separation that matters is the
   one AFTER the subtree, not the one inside it. See .gp-outline-item[open].

   THE LEFT PADDING IS THE TIER STEP, AND IT APPLIES TO EVERYTHING IN THE BODY.
   Prose and child items indent by the SAME amount, so an item's own words line
   up with the items listed under them instead of hanging out to their left.
   It used to sit on the nested list alone, which stepped the children in and
   left the prose flush with the head - two different left edges inside one
   block, which reads as a mistake rather than as a hierarchy.

   NO BOTTOM PADDING: it would add to the open item's margin instead of letting
   that one value govern the gap to the next branch. */
.gp-outline-body {
    padding: 0.25rem 0.75rem 0 1.5rem;
}

/* =====================================================================
   .gp-outline-framed - a modifier on a LIST whose DIRECT items are framed as
   whole sections rather than as titles.

   Use it on the outermost list when the top level is a set of major sections
   (in the tactics Journal: Overview, Stage One, Stage Two) and everything below
   is detail within one of them. The frame says "this whole block belongs
   together"; the plain head-only box says "this is one title among siblings".

   IT IS A MODIFIER AND NOT A DEPTH RULE. A selector for "the outline that is
   not inside another outline" is expressible, but it would force the treatment
   on every consumer and give none of them a way out. A class on the list that
   wants it is opt-in and reads at the call site.

   THE CHILD COMBINATORS ARE LOAD-BEARING. `>` keeps this to the list's own
   items - nested lists inside a framed item are ordinary outlines, which is the
   entire point of the distinction. A descendant selector would frame every
   level and undo it.
   ===================================================================== */
.gp-outline-framed > .gp-outline-item {
    border: 1px solid var(--line);
    border-radius: 0.25rem;
}

/* Inside a frame the head stops being a standalone box and becomes the
   section's header strip: no border of its own - which would double the frame's
   line - and a divider under it instead. */
.gp-outline-framed > .gp-outline-item > .gp-outline-head {
    border: 0;
    border-bottom: 1px solid var(--line);
    border-radius: 0.25rem 0.25rem 0 0;
}

/* A framed item showing nothing below it needs no divider, and its head fills
   the frame. :not([open]) covers a closed <details> AND a static leaf, which
   are the same case from the reader's side. */
.gp-outline-framed > .gp-outline-item:not([open]) > .gp-outline-head {
    border-bottom: 0;
    border-radius: 0.25rem;
}

/* The frame closes the section, so its content needs room above that line -
   the unframed body deliberately has none, because there its own margin
   governs the gap to the next item. */
.gp-outline-framed > .gp-outline-item > .gp-outline-body {
    padding-bottom: 0.75rem;
}

/* The nested list adds NOTHING of its own - the body's left padding above is
   the whole tier step, and it steps prose and children together. Each level
   still lands one step deeper because each body applies it again. */

/* =====================================================================
   Card inset utilities — for blocks inside a padding-less card.

   A card that contains a table is `gp-card gp-card-table`, which zeroes the
   card's own padding so the table reaches both edges (UI/Tables.md — Table
   Card). Every block in that card which is NOT the table has to supply the
   card's edge distance itself. These two carry that 1.125rem, matching
   gp-card-header-compact and gp-card-footer so everything lines up.

   Use `-box` for elements that draw their own border or background (notices,
   error panels): padding would leave their border stranded at the card edge,
   so the inset has to be margin. Use the plain class for everything else.

   Never substitute a negative margin or a calc() width to reach the edges —
   UI/Tables.md forbids it.
   ===================================================================== */
.gp-card-inset {
    padding-left: 1.125rem;
    padding-right: 1.125rem;
}

.gp-card-inset-box {
    margin-left: 1.125rem;
    margin-right: 1.125rem;
}

/* The TOP edge, when the inset block is the card's first child.

   These utilities carry the horizontal edge distance only, because every usage
   until now sat below a gp-card-header-compact or inside a PADDED card, where
   the top space came from something else. A gp-card-table has zero padding, so
   a notice or error panel opening the card sits flush against its top border
   while its sides and bottom are inset — see the Archives page empty state.

   Scoped to the first child of a table card deliberately: adding a blanket
   margin-top to the utility itself would push every existing usage down by an
   extra 1.125rem inside cards that already supply their own padding.

   Bottom spacing needs nothing here — .info-notice and .error-panel both carry
   margin-bottom: 1.125rem of their own. */
.gp-card-table > .gp-card-inset-box:first-child {
    margin-top: 1.125rem;
}

/* =====================================================================
   .gp-card-sub — annotation text under a card heading
   ---------------------------------------------------------------------
   The card-level counterpart of UI/PageHeading.md's .page-sub, and it
   carries that class's values deliberately: 0.813rem, --ink-3, normal
   weight. Same job one level down — a short description of the section,
   never a scope qualifier and never content.

   WHY IT EXISTS. Descriptive text in a card body was a bare <p>, which
   renders at body 1rem in full-strength ink. Beside real content — a
   list, a table, an editor — that is indistinguishable from the content
   itself, which is exactly how it was reported.

   NOT AN .info-notice. UI/Notices.md's decision guide covers errors,
   empty states, status and warnings; a permanent description is none of
   those, and a bordered blue box per section competes with the real
   notices a page raises.

   NOT .page-sub REUSED. That class is documented as sitting under the
   PAGE heading, inside .page-head. A name that lies about where it
   belongs is the .roster-list-item problem (UI/Layout.md) — RESOLVED 2026-08-18, it is now
   .gp-panel-row; kept as the worked example because the lesson outlives the rename — the values
   are shared, the name is not.

   NOT .roster-last-updated. That is the sub-line INSIDE a card header
   div, beneath the h2 and beside the header controls (UI/Cards.md, Card
   Header Styles). This one is a sibling AFTER the header, for prose that
   would crowd a flex row shared with buttons.

   margin-bottom is explicit because the global `p { margin-bottom: 1rem }`
   would otherwise leave a full rem between the note and what it
   describes. .page-sub never faces this — .page-head owns the rhythm
   there. Same global-rule-on-a-bare-element trap UI/Notices.md documents
   for a <p> inside a notice and UI/Layout.md for <li> as panel rows.

   Pair with .gp-card-inset in a gp-card-table, which has no padding of
   its own; inside a padded card it needs no inset.
   ===================================================================== */
.gp-card-sub {
    font-size: 0.813rem;
    color: var(--ink-3);
    margin-bottom: 0.75rem;
}

/* =====================================================================
   .gp-notice-dismissible — transient error the user can clear
   ---------------------------------------------------------------------
   UI/Notices.md's component for a TRANSIENT DISMISSIBLE ERROR: something
   retryable, whose message must not outlive its usefulness.

   Moved here from site.css and renamed 2026-08-07. It was
   .ranks-error-notice / .ranks-error-dismiss, invented for the Team Ranks
   page and since adopted by the Team Documents editor and the blog post
   editor — three unrelated surfaces, none of which is about ranks. Nothing
   in it ever was: a red flex row with a dismiss button. Same story as
   .role-rank-badge becoming .gp-badge (UI/ClassNaming.md).

   THE [hidden] RULE IS NOT DECORATION. This component sets display: flex,
   and an author display beats the UA's [hidden] { display: none } — so
   without the rule below, `hidden` silently fails to hide it and the
   notice is permanently on screen. The original never hit this because
   Razor rendered it conditionally and it had nothing to hide; the first
   consumer to reach for `hidden` would have.
   ===================================================================== */
.gp-notice-dismissible {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: rgba(255,107,107,0.05);
    border: 1px solid rgba(255,107,107,0.25);
    border-left: 0.188rem solid var(--bad);
    border-radius: var(--r-sm);
    padding: 0.625rem 0.875rem;
    margin-bottom: 0.875rem;
    font-size: 0.813rem;
    color: var(--bad);
    box-shadow: inset 3px 0 8px rgba(255,107,107,0.08);
}

.gp-notice-dismissible[hidden] { display: none; }

/* =====================================================================
   .gp-notice-canceled — status modifier on .info-notice
   Added 2026-08-12 (Session 194).

   Marks a raid occurrence that has been canceled. Used by the raider
   signup status area, the team signups content area and the RaiderView
   roster panel — three unrelated surfaces, which is why it is global and
   gp- prefixed rather than roster- or signups- scoped (UI/ClassNaming.md:
   the moment a component-scoped class is used by a second unrelated page
   its name is wrong).

   Follows the UI/Notices.md status-banner pattern EXACTLY: it overrides
   three properties and nothing else. Text colour stays var(--ink-2) from
   .info-notice — that doc forbids a status banner colouring its own text,
   for every state, so --bad appears on the borders and background only.

   --bad rather than --warn: the portal already reads cancellation as a
   negative state (.gp-badge-bad was the original Canceled chip), and
   Notices.md's two documented states (--ok open/active, --warn
   submitted/locked) had no form for this one. NEW DOCUMENTED FORM.
   ===================================================================== */
.gp-notice-canceled {
    background: color-mix(in oklch, var(--bad) 5%, transparent);
    border-color: color-mix(in oklch, var(--bad) 30%, transparent);
    border-left-color: var(--bad);
}

.gp-notice-dismiss {
    background: none;
    border: none;
    color: var(--ink-3);
    cursor: pointer;
    font-size: 1.1rem;
    padding: 0 0.2rem;
    line-height: 1;
}

.gp-notice-dismiss:hover {
    color: var(--ink);
}

/* =====================================================================
   Two-column card grid.

   Lays sibling cards side by side at equal width. Height is deliberately left
   to stretch rather than align-items: start — cards sitting next to each other
   read as a pair, and .gp-card-padded / .gp-card-table are flex columns, so a
   .gp-card-footer with margin-top:auto lines up across both.

   The gap matches .gp-card-mb-med (1.25rem) so horizontal and vertical spacing
   between cards agree.

   Collapses to a single column at the site phone breakpoint. Written here as a
   shared utility rather than per page: .survey-teams-grid and
   .site-logo-upload-grid are the same grid re-declared locally, and this would
   have been a third copy.

   THIS COMMENT NAMED .profile-sections AS A FOURTH COPY UNTIL 2026-09-06.
   That class is gone — the Profile page moved to the rails layout below, which
   is a different mechanism, not a different spelling of this one. The count is
   corrected here rather than left naming a class nothing declares.

   NOT THE SAME THING AS .gp-rails-2 BELOW, and the difference decides which to
   reach for. This grid shares ROWS across its columns, so two cards side by side
   line up and a .gp-card-footer with margin-top:auto agrees across both — which
   is the point when the cards read as a pair. Rails share no rows at all.
   ===================================================================== */
.gp-card-columns-2 {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.25rem;
}

@media (max-width: 48em) {
    .gp-card-columns-2 {
        grid-template-columns: 1fr;
    }
}

/* =====================================================================
   RAILS — columns of cards, each packing tight to its own content.

   Promoted to a shared standard 2026-09-06 (Session 218) from
   pages/recruitment.css, where .recruit-grid / .recruit-rail had been the only
   implementation since Session 202. The Profile page became the second user and
   Directive 18 says extend the standard rather than copy it, so the mechanism
   moved here and Recruitment was migrated onto it in the same pass. Documented
   in UI/Layout.md — "Rails Layout".

   A RAIL IS A REAL CONTAINER, NOT GRID FLOW, AND THAT IS THE WHOLE POINT.
   A CSS grid sizes its ROWS ACROSS EVERY COLUMN. With cards as direct grid
   items, a short card sharing a row track with a tall one is followed by a void
   the size of the difference before the next card begins. grid-auto-flow: column
   does not fix it — that changes the ORDER items are placed in, not the fact
   that rows are shared. Flex columns have no shared rows, so each rail packs to
   its own content.

   Use .gp-card-columns-2 instead when cards SHOULD line up as a pair; use rails
   when they should not.

   THE COUNT IS IN THE CLASS NAME, matching .gp-card-columns-2 above rather than
   inventing a modifier convention for one component. Add .gp-rails-4 here if a
   page ever needs it — do not set grid-template-columns on a page's own class.

   THE ARRANGEMENT IS NEVER DECIDED HERE. Which card lands in which rail is the
   view's business, and nothing in this file places anything. Do not add
   nth-child placement rules — two places deciding one layout is how they drift.

   minmax(0, 1fr) rather than 1fr: a grid track's default minimum is auto, so
   long content would push its rail wider than its share instead of ellipsing
   inside it.

   1fr rather than calc(100% / N): percentage tracks plus gaps exceed 100% and a
   percentage track does not shrink to make room for a gap. 1fr divides what is
   left AFTER the gaps.
   ===================================================================== */
.gp-rails-2,
.gp-rails-3 {
    display: grid;
    gap: 1rem;
    /* Each rail sizes to its own cards. Without this the shorter rail stretches
       to the taller one's height — harmless in itself, but it makes the
       container's height meaningless to anything laid out after it. */
    align-items: start;
}

.gp-rails-2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
}

.gp-rails-3 {
    grid-template-columns: repeat(3, minmax(0, 1fr));
}

.gp-rail {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    min-width: 0;
}

/* One rail on a phone — cards this wide do not fit side by side. The rails
   collapse into a single column and their cards flow on in order. 48em is the
   project's shared breakpoint. */
@media (max-width: 48em) {
    .gp-rails-2,
    .gp-rails-3 {
        grid-template-columns: 1fr;
    }
}

/* =====================================================================
   .gp-label-cell — the label column of a key/value data table.

   RENAMED FROM .profile-label ON 2026-09-06 (Session 218) and moved here from
   site.css. It was never Profile's: Guild Setup used it twelve times and Profile
   ten, and site.css carries a .gp-table-wide-label rule written FOR Guild Setup
   that selects it. UI/ClassNaming.md: "the moment a component-scoped class is
   used by a second, unrelated page, its name is wrong" — the same rule that
   turned .role-rank-badge into .gp-badge.

   min-width is a percentage so the column still yields on a narrow card, where
   a fixed width alone would push the value column out.

   white-space: nowrap ADDED 2026-09-06 — a row label is a short fixed phrase
   ("Last Login", "BattleTag") and wrapping one across two lines misaligns it
   against the value beside it for no gain.

   IT IS DECLARED HERE RATHER THAN BY STACKING .col-shrink ON THE CELL, and that
   is deliberate. .col-shrink is width: 1px + nowrap, so stacking it would put
   width: 1px against this rule's width: 8.75rem at EQUAL SPECIFICITY (0,1,0) and
   leave stylesheet source order to decide — global.css loads after site.css, so
   8.75rem would win today and silently flip the day the load order changes. A
   width conflict resolved by file order is not a thing to ship. The value column
   opposite carries .col-shrink, where there is no width to fight.
   ===================================================================== */
.gp-label-cell {
    color: var(--ink-3);
    font-size: 0.75rem;
    width: 8.75rem;
    min-width: 20%;
    vertical-align: middle;
    white-space: nowrap;
}

/* =====================================================================
   .gp-input-narrow — a single-line text input held to a readable width.

   RENAMED FROM .profile-name-input ON 2026-09-06 (Session 218) and moved here
   from site.css, for the same reason as .gp-label-cell: three pages used it —
   Guild Setup (four inputs), the Teams table's tag input, and Profile's name
   input — and only one of them is Profile.

   It sets a width and nothing else, deliberately. Every current use composes it
   with a class carrying the rest (.meta-field-input, .team-tag-input), so a
   colour or border here would fight them.
   ===================================================================== */
.gp-input-narrow {
    width: 12rem;
}

/* =====================================================================
   Toggle pill inside a card header.

   .toggle-pill-check is display:inline-flex, which shrink-wraps in a block
   context — a table cell, a paragraph. .gp-card-header-controls is a flex row,
   and a flex ITEM has its display blockified: inline-flex computes to flex, so
   the pill stops shrink-wrapping. Its own segments are flex:1 (basis 0), so
   nothing inside holds it at content width either and it spans the header.

   flex: 0 0 auto restores the intent explicitly — size to content, never grow,
   never shrink. Applies wherever a pill sits in a card header, not just here.
   ===================================================================== */
/*  .action-cell is the OTHER flex parent a pill lands in — a td.col-controls' wrapper
    (UI/Tables.md). Same fix, same reason as the card-header case below, added 2026-09-06 when the
    profile page's Discord card put a toggle beside a button in one. Without it the pill's
    inline-flex blockifies to flex and it stretches across the cell, taking the Delete button's
    width with it. UI/TogglePill.md — "any other flex parent needs the same treatment".

    !! IT REACHES A SECOND, PRE-EXISTING OCCURRENCE AND FIXES IT: Admin/_WorkerStatusTable's tick-log
    pill sits directly in an .action-cell and had NO rule anywhere, so it has been stretching since it
    was written. Checked before adding this — every other view pairing the two classes has the pill
    outside the action cell. Written global rather than scoped to the new card precisely so the next
    one is covered too; the cost is that a page nobody opened for this change looks different. */
.action-cell .toggle-pill-check,
.action-cell .toggle-pill,
.gp-card-header-controls .toggle-pill-check,
.gp-card-header-controls .toggle-pill {
    flex: 0 0 auto;
}

/* ---------------------------------------------------------------------------
   STATE MARKERS — an icon that says what a row IS, not what clicking it does.
   Promoted from attendance.css on 2026-08-11 (Session 193) when the manage/edit
   roster page's event list needed the same mark. UI/ClassNaming.md: a
   component-scoped class used by a second unrelated page has the wrong name.

   var(--bad) rather than a literal: it is the theme's negative colour, and the
   seven themes each define their own (Conventions §5.6).

   prohibited.svg, NOT cancel.svg. The two are different marks: cancel.svg is an
   X and means the ACTION cancel/close (blogs, survey results and the roster
   break control all use it), while this is the prohibition sign and means the
   STATE. Picking the X here would have said "click to cancel" on an element
   that does nothing.
   --------------------------------------------------------------------------- */
.gp-canceled-icon {
    display: inline-block;
    width: 0.875rem;
    height: 0.875rem;
    background-color: var(--bad);
    mask: url('/img/prohibited.svg') no-repeat center / contain;
    -webkit-mask: url('/img/prohibited.svg') no-repeat center / contain;
}

/* The icon ships on every row and is revealed by removing `hidden`, so this
   rule is load-bearing: display: inline-block above is an AUTHOR display and
   beats the UA's [hidden] { display: none }. Without it every row would show a
   canceled icon. Fifth instance of that trap in this codebase — UI/Badges.md. */
.gp-canceled-icon[hidden] {
    display: none;
}

/* =====================================================================
   Full-screen image viewer for rendered rich text (.gp-richtext).

   GLOBAL BECAUSE THE SCRIPT IS. richtext-image-viewer.js loads for every visitor — two of the
   five .gp-richtext surfaces are anonymously reachable — so its styles cannot live in a page
   stylesheet. See Systems/StaticAssets.md §5.0 and UI/Modals.md's full-screen image modifier.

   IT REUSES .gp-modal-overlay AND NOT .gp-modal. That form is a centred 28rem card built around
   an action and a button row; this has no action, no input and no confirm — only an image.
   ===================================================================== */
.gp-image-viewer-inner {
    position: relative;
    max-width: 95vw;
    max-height: 95vh;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* The image is shown at its own size, or fitted to the screen when it is larger than the
   viewport. object-fit is not used: the point is to read detail, so the image is never cropped. */
.gp-image-viewer-img {
    display: block;
    max-width: 95vw;
    max-height: 95vh;
    width: auto;
    height: auto;
    border-radius: var(--r-sm);
    /* The anchor around it opens the raw file in a new tab, so the pointer must say "link". */
    cursor: pointer;
}

.gp-image-viewer-link {
    display: block;
    line-height: 0;
}

/* Pinned to the image's corner rather than placed in a row: there is no dialog chrome here to
   hold a control, and a control below the image would sit off-screen on a tall one. */
.gp-image-viewer-close {
    position: absolute;
    top: -0.75rem;
    right: -0.75rem;
    z-index: 1;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: 50%;
    background: var(--bg-2);
    color: var(--ink-2);
    font-size: 1.125rem;
    line-height: 1;
    cursor: pointer;
}

.gp-image-viewer-close:hover {
    color: var(--ink);
    border-color: var(--accent);
}

/* An image in rendered rich text is clickable, so it must look it. Scoped to .gp-richtext and to
   images that are NOT already inside a link — a linked image follows its own href instead, and
   the two affordances must not both be promised. */
.gp-richtext img {
    cursor: zoom-in;
}

.gp-richtext a img {
    cursor: pointer;
}

/* The page behind the overlay must not scroll while it is open. */
body.gp-image-viewer-open {
    overflow: hidden;
}

/* Signups card footer — pins to the card's bottom edge.
   Added 2026-08-23 (Session 206). Moved here from site.css in the same pass, per Conventions 5.4:
   the class is gp- prefixed, so global.css is its home.

   WHY A SCOPED RULE RATHER THAN A CHANGE TO .gp-card-footer-padded. That class sets
   `margin-top: 0.75rem`, where .gp-card-footer sets `margin-top: auto` — which is the whole
   difference between the Team Signups footer floating under its content and the Attendance card's
   footer sitting on the bottom edge beside it. The 0.75rem is DELIBERATE (site.css keeps two
   earlier `auto` versions commented out above it) and the class has TEN consumers across Blogs,
   Changelog, FAQ, Seasons and the raider attendance card. Flipping it globally would move footers
   on all of them to fix one, so the fix is scoped to the card that reported the problem.

   IT WORKS BECAUSE THE CONTAINER ALREADY STRETCHES: .dash-signups-content is
   `display: flex; flex-direction: column; flex: 1`, so the footer had a bottom to be pushed to all
   along and only the auto margin was missing. */
.gp-card-signups h2 { margin-bottom: 0.125rem; }
.gp-card-signups .gp-card-footer-padded { margin-top: auto; }

/* ---------------------------------------------------------------------------
   PAGER (.gp-pager) — promoted to a global 2026-09-03 (Session 214).

   Was .changelog-pagination in site.css, a page-local class from the Changelog page that five
   other features had already borrowed — the same misnomer shape as the old worker-kv-table.
   Promoted on instruction; see UI/Pagination.md for the markup standard.

   THE SIX EXISTING USAGES WERE DELIBERATELY NOT MIGRATED and .changelog-pagination is still
   defined below in site.css. A better pattern in new work does not travel to existing surfaces:
   each of those pages adopts .gp-pager when it is next opened for other work, the same
   incremental rule the CSS and JavaScript splits follow. Documented divergence, not a sweep.
   --------------------------------------------------------------------------- */
.gp-pager {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}
.gp-pager-label { font-size: 0.813rem; color: var(--ink-3); }

/* The disabled end of the range is a <span class="btn gp-pager-disabled">, not a disabled
   <button> — it is not a control at all, it is the button shape holding its place so the row does
   not reflow between the first page and the middle ones. pointer-events: none is what makes that
   true for a pointer as well as visually. */
.gp-pager-disabled { opacity: 0.35; pointer-events: none; cursor: default; }

/* =====================================================================
   MEDIA STRIP (.gp-media-*) — promoted to a global 2026-09-26 (Session 230).

   The thumbnail strip and upload area above an editor that accepts uploads: each thumbnail is a
   copy control with a confirmed delete pinned to its corner, and the dashed box below is the
   drag-and-drop upload area. Was .tactics-image-list / .tactics-image / .tactics-image-copy /
   .tactics-dropzone in pages/tactics.css, renamed on instruction when the Team Documents editor
   gained the same strip — a component-scoped name used by a second feature is a wrong name
   (UI/ClassNaming.md). Declarations carried over unchanged.

   Occurrences: Views/Tactics/_TacticsImages.cshtml + _TacticsArticle.cshtml,
   Views/Teams/_TeamDocumentImages.cshtml + Documents.cshtml.

   THE DELETE BUTTON'S POSITION WAS KEYED ON A js- HOOK (.js-tactics-image-delete). A js- class must
   carry no CSS rule, so the button gained .gp-media-delete for it.
   ===================================================================== */
.gp-media-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 0.5rem;
}

/* The wrapper is `position: relative` so the delete button can sit over the image without
   reserving layout. */
.gp-media-item {
    position: relative;
    width: 6rem;
    height: 6rem;
}

.gp-media-copy {
    display: block;
    width: 100%;
    height: 100%;
    padding: 0;
    border: 0.0625rem solid var(--line);
    border-radius: var(--r-sm);
    background: none;
    cursor: pointer;
    overflow: hidden;
}

.gp-media-copy:hover {
    border-color: var(--accent);
}

/* Confirms the copy on the control that was pressed, without a notice. */
.gp-media-copy.is-copied {
    border-color: var(--ok);
}

/* A .webm clip shows its first frame in the same box as an image. The <video> is the drag source
   for inserting it, so it takes pointer events; a click on it still reaches the copy button. */
.gp-media-copy img,
.gp-media-copy video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.gp-media-item .gp-media-delete {
    position: absolute;
    top: 0.125rem;
    right: 0.125rem;
}

/* The upload area. Drag-and-drop only — Systems/FileUpload.md forbids <input type="file">, so this
   element IS the control and has to read as one. */
.gp-media-dropzone {
    margin-top: 0.5rem;
    padding: 1rem;
    border: 0.0625rem dashed var(--line);
    border-radius: var(--r-sm);
    text-align: center;
    font-size: 0.813rem;
    color: var(--ink-3);
}

.gp-media-dropzone.is-over {
    border-color: var(--accent);
    color: var(--ink);
}

/* =====================================================================
   VISIBILITY ICON (.gp-visibility-icon) — promoted to a global 2026-09-26 (Session 231).

   The open-eye / crossed-out-eye pair (img/visible.svg, img/invisible.svg). Was
   .roster-break-visibility-icon in site.css, a Manage Roster class; renamed on instruction when the
   My Characters card gained the same icon, because a component-scoped name used by a second page is
   a wrong name (UI/ClassNaming.md). Declarations carried over unchanged.

   The icon is the OPEN eye by default. Two things switch it to the crossed-out eye:
     - [aria-pressed="false"] on its button, below. The standard for a new toggle: the attribute is
       the state and nothing mirrors it.
     - Manage Roster's own .is-col-hidden state, which stays a page rule in site.css. That page's
       script toggles the class in eight places and was not rewritten for this.

   Occurrences: Views/MyCharacters/_Characters.cshtml (aria-pressed),
   Views/Roster/Manage.cshtml + wwwroot/js/pages/roster-manage.js (.is-col-hidden).
   ===================================================================== */
.gp-visibility-icon {
    display: block;
    width: 1rem;
    height: 1rem;
    background-color: currentColor;
    mask-image: url('/img/visible.svg');
    mask-size: 100% 100%;
    -webkit-mask-image: url('/img/visible.svg');
    -webkit-mask-size: 100% 100%;
}

[aria-pressed="false"] > .gp-visibility-icon {
    mask-image: url('/img/invisible.svg');
    -webkit-mask-image: url('/img/invisible.svg');
}


/* =====================================================================
   WIDE-TABLE CONTROLS — the table nav bar, the full width toggle, the
   Shift + wheel hint and the scroll-region focus ring.
   PROMOTED 2026-09-29 (Session 235) from pages/group-roster.css, where they
   were grp-roster-* classes, because the Attendance History tab needs the
   same controls and is not a roster page (Conventions 5.4, UI/ClassNaming.md:
   a group class used outside its group has the wrong name). ONE
   implementation for every page (Directive 18): the roster pages were switched
   to these names in the same change. The script half is gpTables.initWidthToggles
   / setFullWidth / showWhenOverflowing (gp-tables.js). Standard:
   UI/Tables.md "Horizontally scrolling table with pinned columns".
     .grp-roster-table-nav         -> .gp-table-nav
     .grp-roster-width-toggle/-icon -> .gp-width-toggle / .gp-width-icon (+ .gp-width-label)
     .grp-roster-scroll-hint(-mouse) -> .gp-scroll-hint / .gp-scroll-hint-mouse
     .grp-roster-scroll-region     -> .gp-scroll-region
     body.layout-full-width        (name kept: a <body> state, not a component)
   ===================================================================== */

/* The bar above a wide table: the width toggle, anything the page adds, and the hint on the right. */
.gp-table-nav { display: flex; align-items: center; gap: 0.375rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--line-soft); }

/* Full width / Standard width. The icon is swapped by gpTables through --col-icon; the text is the
   .gp-width-label child. */
.gp-width-toggle { display: inline-flex; align-items: center; gap: 0.375rem; }
.gp-width-icon {
    display: block;
    width: 1rem;
    height: 1rem;
    background-color: currentColor;
    mask-image: var(--col-icon, url('/img/expand.svg'));
    mask-size: 100% 100%;
    -webkit-mask-image: var(--col-icon, url('/img/expand.svg'));
    -webkit-mask-size: 100% 100%;
}

/* Full width: the page loses its site header and footer and main loses its max-width. A BODY state,
   set by gpTables.setFullWidth; the table keeps scrolling inside its own region either way. */
body.layout-full-width main { max-width: none; overflow-x: auto; }
body.layout-full-width header { display: none; }
body.layout-full-width footer { display: none; }

/* How to scroll the table sideways without reaching its bottom scrollbar: a Shift keycap, a plus, and
   the mouse-wheel icon; the sentence is the element's title and aria-label. Last item in the bar;
   margin-left: auto pushes it right. */
.gp-scroll-hint { margin-left: auto; display: inline-flex; align-items: center; gap: 0.375rem; font-size: 0.75rem; color: var(--ink-3); white-space: nowrap; cursor: help; }
/* Shown only while the table overflows (gpTables.showWhenOverflowing toggles `hidden`); the
   inline-flex above would beat the browser's own [hidden] rule without this one. */
.gp-scroll-hint[hidden] { display: none; }
/* The Shift key as a keycap. Not a control: nothing happens on click. */
.gp-scroll-hint kbd {
    font-family: inherit;
    font-size: 0.688rem;
    font-weight: 600;
    line-height: 1;
    color: var(--ink-2);
    padding: 0.25rem 0.5rem;
    background: var(--bg-3);
    border: 1px solid var(--line);
    border-bottom-width: 2px;
    border-radius: var(--r-sm);
}
.gp-scroll-hint-mouse {
    display: block;
    width: 1.25rem;
    height: 1.25rem;
    background-color: currentColor;
    mask-image: url('/img/mouse-middle-click.svg');
    mask-size: 100% 100%;
    -webkit-mask-image: url('/img/mouse-middle-click.svg');
    -webkit-mask-size: 100% 100%;
}
/* No hint on a touch-only device: no Shift key, no wheel, and swiping is the obvious gesture. A laptop
   with a touchscreen and a mouse keeps it. */
@media (hover: none) and (pointer: coarse) {
    .gp-scroll-hint { display: none; }
}

/* The table's scroll area as a keyboard Tab stop (tabindex="0", role="region", aria-label in the
   markup). Ring drawn INSIDE the edge because the card around it clips overflow. */
.gp-scroll-region:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: -2px;
}

/* =====================================================================
   SEARCH RESULTS — the list a search-as-you-type box paints under itself,
   one <button> per result.
   PROMOTED 2026-09-30 (Session 236) from pages/guild-contacts.css, where they
   were guild-contact-* classes, because the Loot Distribution item search is
   the second caller (Directive 18: one implementation). Declarations are
   unchanged, with one exception: the list's max-width stayed in
   guild-contacts.css — how wide the list is belongs to the caller's layout.
     .guild-contact-search-results  -> .gp-search-results
     .guild-contact-result          -> .gp-search-result
     .guild-contact-result-name     -> .gp-search-result-name
     .guild-contact-result-meta     -> .gp-search-result-meta
     .guild-contact-search-empty    -> .gp-search-empty
   Callers: GuildContacts/_GuildContactsEditor + _GuildContactSearchResults,
   LootDistribution/Index + _ItemSearchResults. The box above the list is the
   separate "Search Box with Clear Button" control (UI/Controls.md).
   ===================================================================== */
.gp-search-results {
    display: flex;
    flex-direction: column;
    margin-top: 0.25rem;
}

.gp-search-result {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.063rem;
    width: 100%;
    padding: 0.375rem 0.625rem;
    background: var(--bg-2);
    border: 1px solid var(--line);
    border-top-width: 0;
    color: inherit;
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.gp-search-result:first-child {
    border-top-width: 1px;
    border-radius: var(--r-sm) var(--r-sm) 0 0;
}

.gp-search-result:last-child {
    border-radius: 0 0 var(--r-sm) var(--r-sm);
}

.gp-search-result:only-child {
    border-radius: var(--r-sm);
}

.gp-search-result:hover:not(:disabled) {
    background: var(--bg-3);
}

/* A result that is listed but cannot be chosen (guild contacts: an account that is already a
   contact). */
.gp-search-result:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

.gp-search-result-name {
    font-size: 0.813rem;
}

.gp-search-result-meta {
    font-size: 0.75rem;
    color: var(--ink-3);
}

.gp-search-empty {
    font-size: 0.813rem;
    color: var(--ink-3);
    margin: 0.25rem 0 0;
}

/* =====================================================================
   DIFFICULTY BADGE AND LOOTBOX ICON — promoted 2026-09-30 (Session 236)
   from site.css's loot block when the Loot Distribution styles moved to
   pages/loot-distribution.css, because other pages use them (UI/ClassNaming.md:
   a class used by a second unrelated page has the wrong name). Declarations
   unchanged.
     .loot-diff-badge, -normal, -heroic, -mythic -> .gp-difficulty-badge, ...
       Loot Distribution (_CharacterHistory), Character (_LootHistory) and
       Guild Teams (raid progress). THE MODIFIER IS BUILT IN C# from the
       difficulty name (LootDistributionController, CharacterController,
       TeamInfoController), so a renamed modifier is a C# change too.
     .loot-history-icon -> .gp-lootbox-icon
       Loot Distribution (_PlayerColumn) and Admin/StylesExample. Its state
       colours on the loot player column stay in the page file
       (.row-icbtn.chest ...).
   Standard: UI/Badges.md.
   ===================================================================== */
.gp-difficulty-badge {
    display: inline-flex;
    align-items: center;
    font-size: 0.656rem;
    font-weight: 600;
    line-height: 1;
    text-transform: uppercase;
    letter-spacing: 0.025rem;
    padding: 0.188rem 0.375rem;
    border-radius: 0.25rem;
    border: 1px solid transparent;
    white-space: nowrap;
}
.gp-difficulty-badge-normal { color: #1EFF00; background: rgba(30,255,0,0.10);   border-color: rgba(30,255,0,0.45);   box-shadow: 0 0 10px rgba(30,255,0,0.20); }
.gp-difficulty-badge-heroic { color: #0070DD; background: rgba(0,112,221,0.12);  border-color: rgba(0,112,221,0.45);  box-shadow: 0 0 10px rgba(0,112,221,0.25); }
.gp-difficulty-badge-mythic { color: #d6a8ff; background: rgba(163,53,238,0.18); border-color: rgba(163,53,238,0.50); box-shadow: 0 0 10px rgba(163,53,238,0.25); }

.gp-lootbox-icon {
    display: inline-block;
    width: 0.813rem;
    height: 0.813rem;
    -webkit-mask: url('/img/lootbox.svg') no-repeat center / contain;
    mask: url('/img/lootbox.svg') no-repeat center / contain;
    background-color: rgba(255,255,255,0.2);
    vertical-align: middle;
    pointer-events: none;
    flex-shrink: 0;
}

/* =====================================================================
   STACKED LEFT COLUMN — several cards in one split-layout left column
   (2026-10-02, Session 241). UI/Layout.md "Stacked left column".
   Callers: FaqInfo/Index and CustomPagesInfo/Index — the page's own menu
   card first, then Shared/_LinkMenuCard cards (Guild information / FAQ,
   then Teams).

   THE STACK IS THE .gp-split-left, NOT EACH CARD. It keeps that class, so
   the sticky pin, the CSS max-height fallback and gp-split.js's measured
   max-height (signed-in visitors) all apply to the WHOLE column. A card
   carrying .gp-split-left inside it would pin and cap on its own and
   overlap its neighbours. Each card is a .gp-split-left-section.

   Two classes on the stack rule so it beats .gp-split-left (site.css,
   0,1,0) whatever the file order.
   ===================================================================== */
.gp-split-left.gp-split-left-stack {
    gap: 0.75rem;
    /* .gp-split-left clips with overflow: hidden, which is harmless when the column IS the card (a
       box never clips its own shadow) but would cut off every inner card's shadow here. The cards
       shrink to fit the cap (below), so nothing needs clipping. */
    overflow: visible;
}

/* One card in the stack. Takes its content height; when the cards together are taller than the
   column's cap they all shrink (min-height: 0 allows it) and each card's own .gp-split-left-body
   scrolls, with its header still pinned. .gp-card-table on the same element already makes it a flex
   column with overflow: hidden. */
.gp-split-left-section {
    display: flex;
    flex-direction: column;
    flex: 0 1 auto;
    min-height: 0;
    /* Clips the card's content to its rounded corners — what .gp-split-left's own overflow: hidden
       did when the column WAS the card. Stated here rather than left to .gp-card-table, because Team
       Info's nav card is a plain .gp-card holding a table (added with its migration, 2026-10-02). */
    overflow: hidden;
}

/* A link row in a _LinkMenuCard. The row METRICS of UI/Layout.md ("Left panel — row metrics") — the
   values .faq-info-row and .cpage-row already agree on — for an <a> rather than a <button>: no
   underline, and no font: inherit (an <a> inherits family and size; the size is still stated, since
   nothing in the layout sets one). Hover and cursor come from .gp-split-left-row; no row is ever
   .is-active, because the entry is on another page. Two classes so the colour beats the global link
   colour whatever the file order. */
.gp-split-left-row.gp-link-menu-row {
    display: block;
    font-size: 0.781rem;
    padding: 0.438rem 0.875rem 0.438rem 1.625rem;
    border-bottom: 1px solid var(--line-soft);
    color: inherit;
    text-decoration: none;
}

.gp-split-left-row.gp-link-menu-row:last-child {
    border-bottom: none;
}

.gp-split-left-row.gp-link-menu-row:hover {
    color: var(--ink);
}
