/* ──────────────────────────────────────────────────────────────────────────
   DESIGN SYSTEM — token reference
   ──────────────────────────────────────────────────────────────────────────
   Use the variables below for new UI. They are the single source of truth
   for colors, font sizes, spacing, and radii. See DESIGN_SYSTEM.md at the
   repo root for the full spec, button modifier table, and usage rules.

   Quick reference:
     • Font sizes:  --fs-micro 10px · --fs-xs 11px · --fs-sm 12px ·
                    --fs-md 13px · --fs-base 14px · --fs-lg 16px ·
                    --fs-xl 18px · --fs-2xl 22px
     • Section headings (uppercase tracked labels) use --fs-xs + --tracking-wide.
     • Body copy uses --fs-sm. Form labels use --fs-xs.
     • Semantic colors:  --c-good (green) · --c-warn (amber) · --c-bad (red) ·
                         --c-accent (teal) · --c-link (indigo).
     • Surface colors:   --c-bg (page) · --c-surface (cards) ·
                         --c-surface-2 (raised) · --c-border / --c-border-2.
     • Text:             --c-text (primary) · --c-text-2 (secondary) ·
                         --c-text-3 (muted/labels) · --c-text-faint.
     • Spacing scale:    --sp-1 4px ··· --sp-6 24px.
     • Radii:            --r-sm 3px · --r-md 5px · --r-lg 8px.

   Buttons:
     • Always start with `.tab-btn` as the base.
     • Add modifiers — `.btn-confirm`, `.btn-danger`, `.btn-warning`,
       `.btn-info`, `.btn-muted`, `.btn-small`, `.btn-micro`. Do NOT introduce new bases.
*/
:root {
    /* Font sizes */
    --fs-micro: 10px;
    --fs-xs:    11px;
    --fs-sm:    12px;
    --fs-md:    13px;
    --fs-base:  14px;
    --fs-lg:    16px;
    --fs-xl:    18px;
    --fs-2xl:   22px;
    --tracking-wide: 0.08em;
    --tracking-wider: 0.12em;

    /* Surfaces / borders */
    --c-bg:        #0d1117;
    --c-surface:   #121a26;
    --c-surface-2: #0f1724;
    --c-border:    #1d2d42;
    --c-border-2:  #1a2233;

    /* Text */
    --c-text:        #e2e8f0;
    --c-text-2:      #cfd9e6;
    --c-text-3:      #8899aa;
    --c-text-faint:  #6a7d96;

    /* Semantic */
    --c-accent:  #33E4DE;
    --c-good:    #1D9E75;
    --c-warn:    #d4aa44;
    --c-bad:     #e87070;
    --c-link:    #7289da;
    --c-special: #a890d8;
    /* Loot bucket colors — used on the wishlist button + the row's
       picked-state stroke + the bucket chips across All Reservations
       and Raid Mode. MS = green primary, OS = blue secondary, Trophy
       reuses --c-special (purple). Kept distinct from --c-warn so
       neither bucket clashes with actual warning copy elsewhere. */
    --c-ms:      var(--c-good);
    --c-os:      #5b9bd5;

    /* Spacing */
    --sp-1:  4px;
    --sp-2:  8px;
    --sp-3:  10px;
    --sp-4:  14px;
    --sp-5:  18px;
    --sp-6:  24px;

    /* Radii */
    --r-sm: 3px;
    --r-md: 5px;
    --r-lg: 8px;
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
    font-family: 'Trebuchet MS', sans-serif;
    background: var(--c-bg);
    color: #dddddd;
    min-height: 100vh;
    padding: 0.75rem;
}
/* Scroll-lock the page underneath any open modal. setupModalScrollLock()
   toggles .modal-open on BOTH <html> and <body> when any modal overlay is
   visible — the [id$="-modal"] HTML modals AND the position:fixed;inset:0 JS
   overlays. Locking both elements covers whichever is the real scroll
   container; the padding-right reserves the (now-hidden) scrollbar's width so
   the page behind the modal doesn't jump. Only the modal's own scrollable
   content scrolls. */
html.modal-open, body.modal-open { overflow: hidden; }
body.modal-open { padding-right: calc(0.75rem + var(--modal-sbw, 0px)); }

/* Tight 1px-black 4-corner outline + soft drop. Pulled out into a
   variable so the same stroke can be applied opt-in to specific places
   (class-colored player labels, roster + attendance table cells) without
   reapplying everywhere. Earlier we had this on `body` and opted out a
   long list of containers; the result smudged buttons / form inputs /
   the white header card. Inverted to opt-in: nothing has the stroke
   unless it's explicitly in the list below. */
:root {
    --txt-stroke:
        -1px -1px 0 #000,
         1px -1px 0 #000,
        -1px  1px 0 #000,
         1px  1px 0 #000,
         0    1px 2px rgba(0, 0, 0, 0.45);
}
/* Apply the stroke to anything with a class color, nothing else. The
   utility class `.cls-color` covers ad-hoc inline-styled cells (roster
   name, attendance name, buffs buff name, the class dropdowns); the
   semantic selectors below cover the named class-colored elements that
   already had a class hook. White-only text (player names on cards),
   neutral-gray meta lines, and form chrome stay shadow-free. */
.cls-color,
.player-card .player-meta,
.callout-card .player-meta,
.at-timeoff-name,
.recruit-card li strong,
.recruit-contact-name,
.tt-provider,
.buff-tag {
    text-shadow: var(--txt-stroke);
}

/* ── Toast ── */
#toast {
    position: fixed;
    bottom: 1.5rem;
    right: 1.5rem;
    padding: 7px 14px;
    border-radius: 5px;
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 1px;
    text-transform: uppercase;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s;
    z-index: 999;
}
#toast.show { opacity: 1; }
#toast.ok  { background: #0d2e22; border: 1px solid #1D9E7566; color: var(--c-good); }
#toast.err { background: #2e0d0d; border: 1px solid #c0303066; color: #e24b4a; }
#toast.warn { background: #2e250d; border: 1px solid #d4aa4466; color: var(--c-warn); }

/* ── Brand strip ── */
/* Thin VM-teal horizon line at the very top of the viewport. Same
   color family as the .prog-overall-fill progression bar (--c-accent
   #33E4DE → #66EDE8), so the brand cue at the top reads as a sibling
   of the page's hero progress visual. Gradient fades to transparent
   at the edges so it reads as a centered glow rather than a hard bar
   across the screen. Position:fixed so the cue persists as the user
   scrolls past the header — z-index sits above the sticky header
   (z:50) so the strip stays the topmost pixel row of the page.
   pointer-events:none so it never intercepts clicks on header chrome
   below. The downward box-shadow gives the strip a soft neon glow
   that bleeds onto the top of the header card, tying the brand color
   to the page chrome below. */
body::before {
    content: '';
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 2px;
    background: linear-gradient(
        90deg,
        transparent 0%,
        rgba(51, 228, 222, 0.45) 5%,
        rgba(51, 228, 222, 0.95) 30%,
        rgba(102, 237, 232, 0.95) 50%,
        rgba(51, 228, 222, 0.95) 70%,
        rgba(51, 228, 222, 0.45) 95%,
        transparent 100%
    );
    box-shadow: 0 0 14px 2px rgba(51, 228, 222, 0.42);
    z-index: 200;
    pointer-events: none;
    transition: box-shadow .25s ease;
    /* transform-origin pins the scale animation to the left edge so
       the loading bar reads as "fill from left" rather than a center-
       expanding pulse. */
    transform-origin: left center;
}
/* Brand strip plays a single "loading bar fills" animation on every
   tab switch — left-edge anchored scaleX from 0 → ~0.75 → 1.0 with
   the opacity easing in. JS adds .is-loading on showTab and clears
   it after the animation duration; consecutive tab switches force a
   reflow + restart so rapid clicks each get a fresh sweep. The
   animation inherits the strip's current color (info teal, warn
   amber, urgent red) since the body.notice-* tints stack with
   .is-loading via the same body::before rule. */
body.is-loading::before {
    animation: brand-load-sweep 0.55s ease-out;
}
@keyframes brand-load-sweep {
    0% {
        opacity: 0.35;
        transform: scaleX(0);
    }
    55% {
        opacity: 1;
        transform: scaleX(0.75);
    }
    100% {
        opacity: 1;
        transform: scaleX(1);
    }
}

/* ── Site notice banner ── */
/* Global banner above the header. Edited from the Account tab "Site
   notice" card. Severity = info | warn | urgent; the corresponding
   body class also tints the top brand strip (body::before) so the
   severity color carries even after the banner scrolls offscreen.
   Hidden by default (display:none in markup); JS toggles display +
   className when SITE_NOTICE.enabled flips. */
.site-notice {
    /* Edge-to-edge layout that sits flush against the top brand
       strip. Body has padding:0.75rem on desktop and padding:4px 0
       on mobile, so we use negative margins to neutralize ALL of
       that and extend the banner from viewport-top to viewport-edge
       (mirrors the same trick .header-wrap uses below us). Earlier
       this used margin:-4px 0 8px 0 which only handled the mobile
       case — desktop ended up with ~8px of dead space above the
       notice that read as "the text isn't vertically centered".
       8px bottom margin gives breathing room before the header card. */
    margin: -0.75rem -0.75rem 8px -0.75rem;
    /* Asymmetric vertical padding (16 top / 10 bottom) shifts the
       text ~3px below geometric center to compensate for the
       optical weight at the top of the banner — the brand strip
       (fixed at y=0) plus its 14px downward glow plus the banner's
       severity-tinted gradient (full saturation at top, fading to
       transparent at bottom) all stack visual heft on the upper
       half of the banner. With symmetric 13/13 padding the text
       appears too high; bumping top padding nudges it visually
       centered. */
    padding: 16px 0.75rem 10px;
    /* No solid border — each severity class paints a vertical
       linear-gradient background that fades to transparent at the
       bottom edge so the banner blends into the page below instead
       of reading as a hard panel boundary above the header card. */
}
.site-notice-inner {
    max-width: 1280px;
    margin: 0 auto;
    display: flex;
    align-items: center;
    /* Center the cluster as a whole, AND center each row's text when
       the banner wraps to multiple lines on narrow viewports — per
       user feedback: text on the banner should all be centered. */
    justify-content: center;
    text-align: center;
    gap: 12px;
    flex-wrap: wrap;
}
.site-notice-badge {
    display: inline-flex;
    align-items: center;
    padding: 2px 8px;
    border-radius: 999px;
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    flex-shrink: 0;
}
.site-notice-message {
    /* No flex-grow — let the message size to its content so the badge
       + message + CTA cluster naturally centers as one unit. (Earlier
       flex:1 1 280px stretched the message to fill leftover space,
       which pushed the cluster's center off when the message was
       short.) Bold weight per design — the notice is the page's hero
       attention element and shouldn't read as quiet body copy. */
    flex: 0 1 auto;
    color: var(--c-text);
    font-size: var(--fs-sm);
    line-height: 1.45;
    font-weight: 700;
}
.site-notice-cta { flex-shrink: 0; }
/* Carousel dots — only shown when there are 2+ messages to rotate
   between. Each dot is a click-target to jump to that index.
   White stroke + fill (rather than inheriting the severity tint via
   currentColor) so the dots read consistently against any severity
   gradient. Inactive dots stay hollow + dim; the active one fills
   solid white at 1.0 opacity. */
.site-notice-dots { display: inline-flex; gap: 6px; align-items: center; flex-shrink: 0; }
.site-notice-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    border: 1px solid #fff;
    background: transparent;
    padding: 0;
    cursor: pointer;
    opacity: 0.55;
    transition: opacity 0.15s ease, background 0.15s ease;
}
.site-notice-dot:hover    { opacity: 0.85; }
.site-notice-dot.is-active {
    background: #fff;
    opacity: 1;
}
/* Fade-swap when rotating between messages — _scheduleNoticeRotate
   adds .notice-fading, swaps innerHTML mid-fade, then removes the
   class to fade back in. Only the inner content fades; the banner's
   background tint stays steady. */
.site-notice .site-notice-inner { transition: opacity 0.35s ease; }
.site-notice.notice-fading .site-notice-inner { opacity: 0; }
/* Admin form: rotating-messages list. Each row stacks the textarea +
   CTA inputs and gets a small remove button at the top. Compact so
   3-4 rows still fit in the card without overflowing. */
.notice-msg-list {
    display: flex;
    flex-direction: column;
    gap: 0;
    /* Cap the list height so a 5-message Site notice card doesn't
       balloon the admin section vertically — paired with overflow-y
       auto for scroll. Same idea as the Backups card's .admin-pw-list
       (260px / 456px) — sized for ~3 message rows visible at once. */
    max-height: 360px;
    overflow-y: auto;
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    background: var(--c-surface);
}
.notice-msg-row {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 10px 12px;
    /* No outer border — the .notice-msg-list container holds one.
       Each row gets a bottom divider line; the last row drops it
       so the list doesn't end on a stray line. */
    border: none;
    border-bottom: 1px solid var(--c-border);
    border-radius: 0;
    background: var(--c-surface-2);
}
.notice-msg-list > .notice-msg-row:last-child { border-bottom: none; }
/* Disabled message rows dim hard (~35%) so the editor reads as
   "staged but not live" at a glance. Button row stays full opacity
   (it's the action surface) so the user can clearly see + click
   Enable / Remove. */
.notice-msg-row.is-disabled {
    opacity: 0.35;
    border-style: dashed;
}
.notice-msg-row.is-disabled .notice-msg-row-head,
.notice-msg-row.is-disabled .notice-msg-toggle-btn,
.notice-msg-row.is-disabled .notice-msg-row-head .notice-msg-row-name,
.notice-msg-row.is-disabled .notice-msg-row-head .notice-msg-row-num,
.notice-msg-row.is-disabled .notice-msg-row-head button {
    opacity: 1;
}
.notice-msg-row-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
}
.notice-msg-row-num {
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-faint);
    font-weight: 600;
}
/* Named row header — replaces "#N" when the admin gives the message
   a name. Slightly bolder + brighter so the user-given identity reads
   as the primary label instead of the fallback index. */
.notice-msg-row-name {
    font-size: var(--fs-sm);
    font-weight: 700;
    color: var(--c-text);
    letter-spacing: 0;
    text-transform: none;
}
.notice-msg-name {
    width: 100%;
}
/* Audience pill in the row header — at a glance "Public" vs "Members
   only" so the admin can scan a list without expanding rows. Live
   toggled by the .notice-msg-public checkbox below. */
.notice-msg-audience-pill {
    display: inline-flex;
    align-items: center;
    padding: 1px 7px;
    border-radius: 999px;
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    margin-left: 8px;
    margin-right: auto;
}
.notice-msg-audience-pill.is-public {
    background: rgba(51, 228, 222, 0.18);
    color: var(--c-accent);
}
.notice-msg-audience-pill.is-members {
    background: rgba(120, 130, 145, 0.20);
    color: var(--c-text-2);
}
/* Public-toggle row — checkbox + label aligned tight so it doesn't
   look like a full second form row. Smaller font for the helper text
   so it reads as a subtle option, not a primary control. */
.notice-msg-public-row {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: var(--fs-xs);
    color: var(--c-text-2);
    cursor: pointer;
}
.notice-msg-public-row input[type="checkbox"] { margin: 0; cursor: pointer; }
/* Severity group panels (admin). Only one is visible at a time —
   the radio-change handler toggles display. The wrap holds them
   stacked so layout shifts are minimal during the swap. */
.notice-groups-wrap {
    display: flex;
    flex-direction: column;
}
.notice-group-panel {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 10px;
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    background: var(--c-surface);
}
/* Severity-tinted left border on the active panel — matches the
   color the live banner will use, so the admin visually associates
   the message list with the brand-strip color it'll produce. */
.notice-group-panel.notice-group-info   { border-left: 3px solid var(--c-accent); }
.notice-group-panel.notice-group-warn   { border-left: 3px solid var(--c-warn); }
.notice-group-panel.notice-group-urgent { border-left: 3px solid #e24b4a; }
/* Per-severity count chips next to the "Messages" label so the admin
   can see at a glance how many messages each severity holds without
   flipping the radio. */
.notice-group-counts {
    display: inline-flex;
    gap: 4px;
    margin-left: 6px;
    vertical-align: middle;
}
.notice-group-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    padding: 1px 6px;
    border-radius: 999px;
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: 0;
    text-transform: none;
    line-height: 1;
}
.notice-group-count-info   { background: rgba(51, 228, 222, 0.14);  color: var(--c-accent); }
.notice-group-count-warn   { background: rgba(232, 140, 77, 0.16);  color: var(--c-warn); }
.notice-group-count-urgent { background: rgba(226, 75, 74, 0.18);   color: #f06b6a; }
/* Severity-specific palette. Background is a vertical fade — full
   tint at the top (where it abuts the brand strip), tapering to
   transparent at the bottom so the banner reads as a soft "glow" bleed
   under the strip rather than a hard panel above the header. The
   badge keeps a fully saturated chip so the urgency still reads at a
   glance. */
.site-notice.notice-info {
    background: linear-gradient(180deg,
        rgba(51, 228, 222, 0.12) 0%,
        rgba(51, 228, 222, 0.10) 55%,
        rgba(51, 228, 222, 0)    100%);
}
.site-notice.notice-info .site-notice-badge {
    background: rgba(51, 228, 222, 0.18);
    color: var(--c-accent);
}
.site-notice.notice-warn {
    background: linear-gradient(180deg,
        rgba(232, 140, 77, 0.16) 0%,
        rgba(232, 140, 77, 0.12) 55%,
        rgba(232, 140, 77, 0)    100%);
}
.site-notice.notice-warn .site-notice-badge {
    background: rgba(232, 140, 77, 0.18);
    color: var(--c-warn);
}
.site-notice.notice-urgent {
    background: linear-gradient(180deg,
        rgba(226, 75, 74, 0.20) 0%,
        rgba(226, 75, 74, 0.14) 55%,
        rgba(226, 75, 74, 0)    100%);
}
.site-notice.notice-urgent .site-notice-badge {
    background: rgba(226, 75, 74, 0.20);
    color: #f06b6a;
}
/* Severity tints for the top brand strip. body::before's default rule
   ships the teal (--c-accent) gradient; these overrides swap the
   gradient stops + glow color when a notice is active. info reuses
   the default teal so it visually matches the in-banner badge color. */
body.notice-warn::before {
    background: linear-gradient(
        90deg,
        transparent 0%,
        rgba(232, 140, 77, 0.18) 15%,
        rgba(232, 140, 77, 0.95) 50%,
        rgba(232, 140, 77, 0.18) 85%,
        transparent 100%
    );
    box-shadow: 0 0 16px 2px rgba(232, 140, 77, 0.55);
}
body.notice-urgent::before {
    background: linear-gradient(
        90deg,
        transparent 0%,
        rgba(226, 75, 74, 0.20) 12%,
        rgba(226, 75, 74, 1.00) 50%,
        rgba(226, 75, 74, 0.20) 88%,
        transparent 100%
    );
    box-shadow: 0 0 18px 3px rgba(226, 75, 74, 0.60);
    /* Subtle pulse on urgent so the eye catches the strip even when
       the user is scrolled past the banner. Keep the period long
       (3s) so it doesn't read as anxious / distracting. */
    animation: notice-urgent-pulse 2.4s ease-in-out infinite;
}
@keyframes notice-urgent-pulse {
    0%, 100% { box-shadow: 0 0 18px 3px rgba(226, 75, 74, 0.60); }
    50%      { box-shadow: 0 0 26px 5px rgba(226, 75, 74, 0.85); }
}
/* Notice form (admin card) — minimal stacked-row layout with a
   severity radio group that mirrors the banner's color treatment so
   the admin sees what they're configuring. */
.notice-form {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 8px 0 4px;
}
.notice-row {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}
.notice-row-stack { flex-direction: column; align-items: stretch; }
.notice-row-label {
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-faint);
}
.notice-sev-group {
    display: flex;
    /* One row on desktop with the three severity options sitting
       side-by-side. Each chip takes its natural content width
       (flex: 0 0 auto via the default) so the longest label
       ("Warning · Action requested") doesn't get cut off — earlier
       flex:1 1 0 forced equal widths and the middle chip clipped.
       Wraps on phones (≤520px) where total natural width > viewport. */
    flex-wrap: nowrap;
    gap: 8px;
}
.notice-sev-group .notice-sev-opt {
    /* Keep natural width — don't grow, don't shrink below content */
    flex: 0 0 auto;
}
@media (max-width: 520px) {
    .notice-sev-group { flex-wrap: wrap; }
    .notice-sev-group .notice-sev-opt { flex: 1 1 auto; }
}
.notice-sev-opt {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 10px;
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    cursor: pointer;
    background: var(--c-surface-2);
    font-size: var(--fs-sm);
    /* Keep "Action requested" / "Time-sensitive" on a single line
       within the chip — no wrapping mid-label even if flex sizing
       squeezes the chip. The row's flex:1 1 0 / min-width:0 lets
       the chip shrink, but white-space:nowrap on the chip + its
       children means the label text stays intact (chip just gets
       narrower / pushes its hint out if needed). */
    white-space: nowrap;
}
.notice-sev-opt .notice-sev-label,
.notice-sev-opt .notice-sev-hint {
    white-space: nowrap;
}
.notice-sev-opt:has(input:checked) { border-color: var(--c-accent); }
.notice-sev-opt.notice-sev-warn:has(input:checked)   { border-color: var(--c-warn); }
.notice-sev-opt.notice-sev-urgent:has(input:checked) { border-color: #e24b4a; }
.notice-sev-opt input[type="radio"] { margin: 0; cursor: pointer; }
.notice-sev-label { font-weight: 600; }
.notice-sev-hint  { color: var(--c-text-faint); font-size: var(--fs-xs); }
.notice-cta-row { display: flex; gap: 8px; flex-wrap: wrap; }
.notice-cta-row > input[type="text"] { flex: 1 1 240px; }
.notice-cta-row > .admin-select       { flex: 0 1 200px; }

/* ── Header ── */
/* Sticky wrapper carries the page-color strip so the rounded white card
   inside can stay rounded without letting content peek above it. */
.header-wrap {
    position: sticky;
    top: 0;
    margin: -0.75rem -0.75rem 0.75rem -0.75rem;
    padding: 0.75rem 0.75rem 0 0.75rem;
    background: var(--c-bg);
    z-index: 50;
}
.header {
    display: flex;
    align-items: center;
    gap: 10px;
    background: #ffffff;
    border: 1px solid #dde1e7;
    border-radius: 8px;
    padding: 8px 14px;
}
.logo-wrap {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-shrink: 0;
    padding-right: 15px;
}
.logo-img {
    height: 72px;
    width: auto;
    object-fit: contain;
    display: block;
    cursor: pointer;
}
.logo-wrap { cursor: pointer; }
.subtitle {
    font-size: var(--fs-xs);
    color: #888;
    letter-spacing: 2px;
    text-transform: uppercase;
}
.tabs {
    margin-left: auto;
    display: flex;
    gap: 10px;
}
/* "Left the guild" card lives above the roster table (filled by
   renderDepartureBanner); empty container collapses to nothing. */
#tab-roster #roster-departure-banner:empty { display: none; }
/* "How fresh is this" note on the Pending list. */
.pending-fresh { color: var(--c-text-3); font-size: var(--fs-xs); margin: 0 0 var(--sp-3); line-height: 1.5; }
/* Count badge on the Raiders / Non-Raiders sub-nav pills — how many
   members left from that group. Hidden (inline display:none) at 0. */
.roster-pill-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    margin-left: 4px;
    border-radius: 8px;
    background: var(--c-warn);
    color: var(--c-bg);
    font-size: var(--fs-micro);
    font-weight: 700;
    line-height: 1;
}
#header-auth {
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: var(--fs-sm);
    color: #555;
    flex-shrink: 0;
    padding-left: 10px;
    padding-right: 3px;
    border-left: 1px solid #dde1e7;
    margin-left: 4px;
}
/* "View as" impersonation picker — gm/officer only, sits to the LEFT of
   the #header-auth divider so it reads as a separate cluster. Empty for
   raiders/guests. The right-cluster auto-margin lives on whichever
   element actually has content: when impersonation has buttons, it
   carries the auto-margin so both it AND header-auth slide right
   together; when impersonation is empty (collapsed via :empty), the
   adjacent-sibling rule below moves the auto-margin onto header-auth
   so the user info still right-aligns. */
#header-impersonation {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: var(--fs-sm);
    color: #555;
    flex-shrink: 0;
    margin-left: auto;
}
#header-impersonation:empty { display: none; }
#header-impersonation:empty + #header-auth { margin-left: auto; }
.ha-imp-label {
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: #888;
    font-weight: 600;
    cursor: default;
}
.ha-imp-select {
    font-family: inherit;
    font-size: var(--fs-sm);
    padding: 3px 26px 3px 8px;
    background-color: #f3f4f6;
    border: 1px solid #dde1e7;
    border-radius: 4px;
    color: #444;
    max-width: 160px;
    cursor: pointer;
    /* Custom caret inset from the right edge (native chevron sits flush).
       background-color is used everywhere below instead of the `background`
       shorthand so this image layer survives the hover / active states. */
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%23666' d='M0 0l5 6 5-6z'/></svg>");
    background-repeat: no-repeat;
    background-position: right 8px center;
}
.ha-imp-select:hover { background-color: #e8ebef; }
.ha-imp-select.is-active {
    background-color: rgba(232, 140, 77, 0.15);
    border-color: rgba(232, 140, 77, 0.55);
    color: #c46a2a;
    font-weight: 600;
}
/* Impersonation indicator: thin amber bar at the top of the page so
   the override is impossible to miss while it's active. */
body.impersonating::before {
    content: '';
    position: fixed;
    top: 0; left: 0; right: 0;
    height: 2px;
    background: linear-gradient(90deg, transparent, #e88c4d, transparent);
    z-index: 100;
    pointer-events: none;
}
/* Stack the player name on top and the role label underneath, right-aligned
   to sit cleanly next to the avatar. The previous side-by-side layout
   sometimes wrapped awkwardly with longer names + ranks ("Achtungforever"
   + "Officer" overflowed the header on mid-width displays). 2px gap so the
   role doesn't sit flush against the descenders of the name. */
#header-auth .ha-user { display: inline-flex; flex-direction: column; align-items: flex-end; gap: 2px; line-height: 1.15; }
#header-auth .ha-name { font-weight: 600; color: #222; }
#header-auth .ha-role {
    font-size: var(--fs-micro);
    letter-spacing: 0.05em;
    color: #888;
    text-transform: uppercase;
}
#header-auth .ha-btn {
    font-family: inherit;
    font-size: var(--fs-sm);
    line-height: 1.4;
    padding: 5px 13px;
    /* 4px extra breathing room between the .ha-user name+role block
       and the Logout / Sign in button. The flex container's gap of
       4px alone made the button sit too close to the user-info text. */
    margin-left: 4px;
    background: #f3f4f6;
    border: 1px solid #dde1e7;
    border-radius: 4px;
    color: #444;
    cursor: pointer;
    text-decoration: none;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    box-sizing: border-box;
}
#header-auth .ha-btn:hover { background: #e8ebef; }
#header-auth .ha-btn.primary {
    background: var(--c-accent);
    border-color: var(--c-accent);
    color: #0a0f1a;
    font-weight: 600;
}
#header-auth .ha-btn.primary:hover { background: #29CCC6; }
.tab-btn {
    padding: 5px 13px;
    font-size: var(--fs-sm);
    line-height: 1.4;
    border-radius: 4px;
    cursor: pointer;
    letter-spacing: 1px;
    text-transform: uppercase;
    background: transparent;
    border: 1px solid #ccd0d8;
    color: var(--c-text-2);
    font-family: inherit;
    transition: all 0.15s;
    /* Buttons should never break text across lines — they're short
       imperative labels ("+ Add Player", "Process Attendance", etc.).
       Without nowrap, narrow flex containers (notably the roster
       toolbar on phones, where seg-toggle + button + search compete
       for width) squeeze button width below its text's natural size
       and the label wraps to two lines. The parent row should use
       flex-wrap:wrap so the button itself wraps to the next line
       intact instead of being compressed. */
    white-space: nowrap;
    /* Kill the browser's default dark tap-highlight overlay that flashes over
       the button on click/tap — the .active / .is-active teal is the intended
       pressed feedback; we don't want a grey-black wash showing through on top. */
    -webkit-tap-highlight-color: transparent;
}
.tab-btn:hover { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }
/* Same, applied broadly to every interactive control so no button/link/toggle
   flashes the default dark tap-highlight on click/tap. */
button, a, [role="button"], .points-link, .player-link { -webkit-tap-highlight-color: transparent; }

/* Size modifiers (compose with .tab-btn) */
.tab-btn.btn-small { font-size: var(--fs-xs); padding: 3px 10px; }
.tab-btn.btn-micro { font-size: var(--fs-micro); padding: 2px 8px; letter-spacing: 0.5px; }
/* Square icon-only button (×, +, etc.) sized to align with .admin-input. */
.tab-btn.btn-icon { padding: 5px 10px; line-height: 1; letter-spacing: 0; min-width: 0; }

/* Color modifiers (compose with .tab-btn) — replace inline color/border-color overrides */
.tab-btn.btn-confirm { color: var(--c-accent); border-color: #33E4DE44; }
.tab-btn.btn-confirm:hover { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }
.tab-btn.btn-danger  { color: var(--c-bad); border-color: #e8707044; }
.tab-btn.btn-danger:hover  { background: var(--c-bad); border-color: var(--c-bad); color: var(--c-bg); }
.tab-btn.btn-warning { color: var(--c-warn); border-color: #d4aa4444; }
.tab-btn.btn-warning:hover { background: var(--c-warn); border-color: var(--c-warn); color: var(--c-bg); }
/* Blue / informational action (e.g. the "Maybe" tentative RSVP) — uses the
   indigo --c-link so it reads as a distinct, lower-commitment choice next to
   the teal Join and red Call-out, and matches Discord's blurple Maybe button. */
.tab-btn.btn-info    { color: var(--c-link); border-color: color-mix(in srgb, var(--c-link) 40%, transparent); }
.tab-btn.btn-info:hover    { background: var(--c-link); border-color: var(--c-link); color: var(--c-bg); }
/* Loot-bucket-specific button colors (MS / OS). Used on the wishlist
   row buttons; kept distinct from btn-confirm (teal accent, generic
   primary action) and btn-warning (yellow, generic warning) so the
   loot row's color signal doesn't drift if other parts of the app
   re-theme those modifiers. */
.tab-btn.btn-ms      { color: var(--c-ms);  border-color: color-mix(in srgb, var(--c-ms) 35%, transparent); }
.tab-btn.btn-ms:hover { background: var(--c-ms); border-color: var(--c-ms); color: var(--c-bg); }
.tab-btn.btn-os      { color: var(--c-os);  border-color: color-mix(in srgb, var(--c-os) 35%, transparent); }
.tab-btn.btn-os:hover { background: var(--c-os); border-color: var(--c-os); color: var(--c-bg); }
/* Hard Reserve copper-amber matches the AR row HR badge (#e88c4d). */
.tab-btn.btn-hr      { color: #e88c4d; border-color: rgba(232,140,77,0.35); }
.tab-btn.btn-hr:hover { background: #e88c4d; border-color: #e88c4d; color: var(--c-bg); }
.tab-btn.btn-muted   { color: rgba(207, 217, 230, 0.55); border-color: rgba(207, 217, 230, 0.25); }
.tab-btn.btn-muted:hover { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }

/* Config dropdown — match tab-btn look/height */
.config-select {
    font-family: inherit;
    font-size: var(--fs-sm);
    line-height: 1.4;
    padding: 5px 28px 5px 13px;
    border-radius: 4px;
    border: 1px solid #ccd0d8;
    background: transparent;
    color: #444;
    cursor: pointer;
    min-width: 200px;
    box-sizing: border-box;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%23666' d='M0 0l5 6 5-6z'/></svg>");
    background-repeat: no-repeat;
    background-position: right 10px center;
    transition: all 0.15s;
}
.config-select:hover { border-color: var(--c-accent); }
.config-select:focus { outline: none; border-color: var(--c-accent); }
.config-select.on-dark:hover, .config-select.on-dark:focus { border-color: var(--c-accent); }
.config-select option { text-transform: none; letter-spacing: normal; color: #333; background: #fff; }

/* Dark-bg variant for pages with dark surfaces (e.g. Attendance) —
   matches .admin-select styling so all dark-surface dropdowns are uniform. */
.config-select.on-dark {
    background: var(--c-bg);
    color: var(--c-text);
    border-color: #223049;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%23cfd9e6' d='M0 0l5 6 5-6z'/></svg>");
    background-repeat: no-repeat;
    background-position: right 10px center;
}
.config-select.on-dark option { color: var(--c-text); background: var(--c-bg); }
.tab-btn.active {
    background: #33E4DE12;
    border-color: var(--c-accent);
    color: #2d9aa4;
}

/* ── Missing banner ── */
.missing-banner {
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 6px;
    padding: 10px 14px;
    margin-bottom: 0;
    font-size: var(--fs-sm);
    color: #e24b4a;
}
.missing-banner strong { color: #e24b4a; display: block; margin-bottom: 8px; }
#missing-list {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0 16px;
}
.missing-col-title {
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    margin-bottom: 6px;
    padding-bottom: 5px;
    border-bottom: 1px solid var(--c-border);
}
.missing-benefit-row {
    display: flex;
    align-items: flex-start;
    gap: 6px;
    margin-bottom: 5px;
}
.missing-benefit-label {
    font-size: var(--fs-micro);
    color: #999;
    letter-spacing: 1px;
    text-transform: uppercase;
    white-space: nowrap;
    width: 130px;
    flex-shrink: 0;
    padding-top: 3px;
}
.missing-pills-wrap {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}
.missing-tag {
    padding: 3px 9px;
    border-radius: 3px;
    border: 1px solid;
    font-size: var(--fs-sm);
}

/* ── Comp grid ── */
.comp-grid {
    display: grid;
    grid-template-columns: repeat(6, 1fr);
    gap: 8px;
}

/* ── Group / Bench card ── */
.group-card {
    background: var(--c-surface);
    border-radius: 8px;
    border: 1px solid var(--c-border);
    overflow: hidden;
    display: flex;
    flex-direction: column;
}
.group-header {
    background: var(--c-surface-2);
    padding: 7px 10px;
    font-size: var(--fs-md);
    font-weight: 700;
    letter-spacing: 2px;
    color: var(--c-accent);
    text-transform: uppercase;
    border-bottom: 1px solid var(--c-border);
    flex-shrink: 0;
    height: 36px;
    box-sizing: border-box;
    display: flex;
    align-items: center;
}
.group-disabled { opacity: 0.25; pointer-events: none; }
.bench-header { color: #ffffff99; }
.group-slots { padding: 5px; }
.slot {
    margin-bottom: 3px;
    border-radius: 4px;
    min-height: 34px;
    border: 1px solid transparent;
    transition: all 0.1s;
}
.slot.drag-over {
    border-color: #33E4DE44;
    background: #33E4DE08;
}
/* "+ Stand by" button lit up while a card is dragged over it (drop target). */
/* Strip-card drag target — a group's callout strip lights up when a dragged
   callout/standby/maybe card can be re-homed onto it. */
.group-callouts.drop-hot {
    outline: 1px dashed var(--c-accent);
    outline-offset: 2px;
    border-radius: var(--r-md);
}
.tab-btn.drop-hot {
    border-color: var(--c-accent);
    color: var(--c-accent);
    background: #33E4DE14;
}
.slot-empty {
    /* Match the dimensions of a filled .player-card so rows line up */
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 5px 8px;
    border-radius: 4px;
    background: var(--c-surface-2);
    border: 1px solid #1a1e2a;
    height: 42px;
    box-sizing: border-box;
    font-size: var(--fs-md);
    color: #3d4a66;
    font-weight: 700;
}
.slot-empty-actions {
    display: flex;
    gap: 4px;
    justify-content: flex-end;
    flex-wrap: nowrap;
    margin-left: auto;
}
.pug-btn {
    font-size: 9px;
    padding: 2px 6px;
    border-radius: 3px;
    border: 1px solid #33E4DE44;
    background: transparent;
    color: #33E4DE88;
    cursor: pointer;
    font-family: inherit;
    letter-spacing: 0.5px;
    transition: all 0.15s;
    white-space: nowrap;
}
.pug-btn:hover { background: #33E4DE14; color: var(--c-accent); border-color: var(--c-accent); }

/* ── Player card ── */
.player-card {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 5px 8px;
    border-radius: 4px;
    cursor: grab;
    user-select: none;
    background: var(--c-surface-2);
    border: 1px solid #22222e;
    transition: opacity 0.15s;
    min-height: 42px;
    box-sizing: border-box;
}
.player-card:active { cursor: grabbing; opacity: 0.7; }
.player-card.dragging { opacity: 0.4; }
.recruit-edit-row.dragging { opacity: 0.4; }
.recruit-edit-row.drag-over-top { border-top-color: var(--c-accent) !important; }
.recruit-edit-row.drag-over-bot { border-bottom-color: var(--c-accent) !important; }
.player-name {
    font-size: var(--fs-md);
    font-weight: 600;
    color: #eeeeee;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.player-meta {
    font-size: var(--fs-xs);
    margin-top: 1px;
}

/* ── Callouts strip (per-group, below .group-slots) ──
   When a scheduled raider marks time off, they auto-move out of their
   slot into here so the slot opens up for a sub. The original assignment
   is preserved so the GM can see (and audit) who called out. */
.group-callouts {
    border-top: 1px dashed var(--c-border);
    padding: 6px 5px 4px;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.callout-strip-label {
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-faint);
    margin-bottom: 2px;
}
.callout-card {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 4px 8px;
    border-radius: 4px;
    background: var(--c-surface-2);
    border: 1px dashed #d4aa4455;
    min-height: 38px;
    box-sizing: border-box;
}
/* Dim only the player text + badge — keep the X remove button fully
   visible so it's obviously clickable. Previously the whole card was
   opacity:0.65 which made the X grey-on-grey-on-dim → effectively
   invisible, and users couldn't tell how to uncall someone out. */
.callout-card .player-name,
.callout-card .player-meta,
.callout-card .callout-badge,
.callout-card .callout-slot-note {
    opacity: 0.7;
}
/* Remove ✕ button: rendered as an obvious always-visible chip on the
   right edge of the callout card. Background + border + bold X so it
   reads as a button, not an easy-to-miss decorative character. Earlier
   plain-span treatment was too subtle and users were missing it. */
.callout-card-x {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    font-size: 14px;
    font-weight: 700;
    line-height: 1;
    color: #ff8a89;
    background: #e24b4a22;
    border: 1px solid #e24b4a55;
    cursor: pointer;
    border-radius: 4px;
    flex-shrink: 0;
    transition: color 0.1s, background 0.1s, border-color 0.1s;
    user-select: none;
}
.callout-card-x:hover {
    color: #fff;
    background: #e24b4a;
    border-color: #e24b4a;
}
/* Callout name keeps its color but no strikethrough — the CALLOUT badge
   already says it. Line-through stacked on top of the badge read as
   noise. */
.callout-badge {
    font-size: 8px;
    font-weight: 700;
    padding: 1px 4px;
    border-radius: 3px;
    background: #d4aa44;
    color: #1a1a1a;
    flex-shrink: 0;
    letter-spacing: var(--tracking-wide);
}
/* NO SHOW variant — punitive, escalates to red since it's worse than
   a regular callout (callouts cost no points; no-shows cost 15). The
   color swap (CALLOUT=amber, NO SHOW=red) was a user call after the
   initial pass had them reversed — red reads as "this is bad" and
   amber as "heads up." */
.callout-badge-noshow {
    background: #e24b4a;
    color: #fff;
}
/* No-show card border tints red, replacing the dashed amber of a
   regular callout card. The inline class-color border-left stays. */
.callout-card-noshow {
    border-color: #e24b4a55 !important;
    border-style: dashed;
}
/* STANDBY variant — a volunteer sub for a full raid. Brand teal (reads as
   "available / positive", distinct from amber callout + red no-show). */
.callout-badge-standby {
    background: #33E4DE;
    color: #08211f;
}
.callout-card-standby {
    border-color: #33E4DE55 !important;
    border-style: dashed;
}
/* MAYBE variant — a tentative RSVP. Blue (matches the Maybe button), distinct
   from amber callout / red no-show / teal standby. */
.callout-badge-maybe {
    background: var(--c-link);
    color: #fff;
}
.callout-card-maybe {
    border-color: color-mix(in srgb, var(--c-link) 45%, transparent) !important;
    border-style: dashed;
}
/* + Callout / + No Show button row — centered with breathing room
   between the two buttons. The strip puts cards on top and this
   button row beneath them; the top margin gives the buttons space
   to breathe away from the last card so the action bar reads as a
   distinct cluster instead of crammed against the entries. The
   `:first-child` selector handles the empty-state (no cards above
   → no extra top margin needed). */
.callout-add-btns {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 6px;
    margin-bottom: 4px;
}
.callout-add-btns:first-child {
    margin-top: 0;
}
/* Count line ('1 callout · 2 no-shows') — sits at the TOP of the
   strip, above the cards. Subtle, since the cards themselves convey
   the same info via their badges. Acts as a quick "N people sitting
   this raid out" header before the eye scans the card list. */
.callout-strip-count {
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-faint);
    text-align: center;
    margin-bottom: 4px;
}
/* Same counts, but pinned under the roster pool column — sits below the list,
   so it needs top spacing + a divider instead of the strip's bottom margin. */
#bench-strip-count {
    margin: 8px 0 0;
    padding-top: 8px;
    border-top: 1px solid var(--c-border);
    line-height: 1.9;
}
.callout-slot-note {
    font-size: var(--fs-micro);
    color: var(--c-text-faint);
    font-weight: 400;
    text-transform: lowercase;
    letter-spacing: 0;
}
/* Variant of the strip label used when no callouts exist but the writer
   still gets a "+ Callout" affordance. Different visual weight than the
   N-callouts label so an empty strip doesn't shout for attention. */
.callout-strip-label.callout-strip-label-empty {
    color: var(--c-text-faint);
    opacity: 0.65;
    display: flex;
    align-items: center;
}
.callout-strip-label { display: flex; align-items: center; }

/* Inline picker for "+ Callout". Lives at the bottom of .group-callouts,
   only when the writer clicks the button. Search input + scrollable list
   of class-colored roster names. */
.callout-picker {
    margin-top: 6px;
    padding: 8px;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-sm);
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.callout-picker-head {
    display: flex; align-items: center; justify-content: space-between;
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-faint);
}
.callout-picker-search { width: 100%; box-sizing: border-box; }
.callout-pick-list {
    display: flex; flex-direction: column; gap: 2px;
    max-height: 220px; overflow-y: auto;
}
.callout-pick-row {
    display: flex; align-items: center; gap: 8px;
    padding: 5px 8px;
    background: var(--c-surface-2); border: 1px solid var(--c-border);
    border-radius: var(--r-sm);
    font-size: var(--fs-sm);
    color: inherit;
    cursor: pointer;
    text-align: left;
    font-family: inherit;
    transition: background 0.1s, border-color 0.1s;
}
.callout-pick-row:hover { border-color: var(--c-accent); background: var(--c-surface); }
.callout-pick-name { font-weight: 600; }
.callout-pick-meta { color: var(--c-text-faint); font-size: var(--fs-xs); flex: 1; }
.callout-pick-alum {
    font-size: 8px;
    font-weight: 700;
    padding: 1px 4px;
    border-radius: 3px;
    background: #000;
    color: #fff;
    letter-spacing: var(--tracking-wide);
}
.callout-pick-empty {
    font-size: var(--fs-sm);
    color: var(--c-text-faint);
    font-style: italic;
    padding: 8px;
    text-align: center;
}

/* ── Buff tags ── */
.buff-tags {
    border-top: 1px solid var(--c-border);
    padding: 6px 8px;
    min-height: 32px;
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: auto;
}
.buff-tag {
    font-size: var(--fs-sm);
    padding: 3px 9px;
    border-radius: 3px;
    white-space: nowrap;
    cursor: default;
}
/* buff-tag colors set inline by class */

/* ── Buff priority groups ── */
.buff-group { margin-bottom: 5px; }
.buff-group:last-child { margin-bottom: 0; }
.buff-group-label {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    margin-bottom: 3px;
    opacity: 0.7;
}
.buff-group-tags { display: flex; flex-wrap: wrap; gap: 3px; }
.buff-app-section { margin-top: 18px; margin-bottom: 10px; }
.buff-app-section:first-child { margin-top: 0; }
.buff-app-section:last-child { margin-bottom: 0; }
.buff-app-label {
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 2px;
    text-transform: uppercase;
    padding-bottom: 2px;
    margin-bottom: 6px;
    border-bottom: 1px solid;
}
.buff-prio-block { margin-top: 12px; margin-left: 4px; }
.buff-prio-block:first-child { margin-top: 4px; }
.buff-prio-label {
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    margin-bottom: 2px;
    opacity: 0.85;
}

/* ── Buff visibility toggles ── */
.buff-vis-bar {
    display: flex;
    align-items: center;
    gap: 6px;
}
/* .bvb-toolbar wraps the size + Show toggle buttons so they can move as a
   single unit on mobile; on desktop it flattens into the parent flex row. */
.bvb-toolbar { display: contents; }
.buff-vis-bar span {
    font-size: var(--fs-micro);
    color: var(--c-text-3);
    letter-spacing: 1px;
    text-transform: uppercase;
    margin-right: 2px;
}
/* ── Nav dropdown ── */
.main-nav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.main-nav-btn {
    font-family: inherit;
    font-size: var(--fs-sm);
    line-height: 1.4;
    padding: 5px 13px;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 4px;
    color: #444;
    cursor: pointer;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: 600;
    transition: all 0.15s;
}
.main-nav-btn:hover { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }
.main-nav-btn.active { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }
/* Chat connectivity dot on the Chat nav button — green = relay + in-game
   capture both live, amber = relay only (game side stale/dead — attention),
   red = relay down. White ring keeps it readable on the teal hover/active
   button background. */
.nav-chat-dot {
    display: inline-block;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    margin-left: 6px;
    vertical-align: 1px;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25);
}
.nav-chat-dot.ok   { background: var(--c-good); }
.nav-chat-dot.warn { background: var(--c-warn); }
.nav-chat-dot.bad  { background: var(--c-bad); }
/* Mobile nav toggle + collapsible panel (≤700px). Desktop keeps inline buttons. */
.mobile-nav-toggle { display: none; }
.mobile-nav-toggle .caret { font-size: var(--fs-micro); margin-left: 6px; }

.subnav {
    display: flex;
    align-items: center;
    gap: 10px;
    /* Match .tab-content-wrap below it: same 1280px max-width AND same
       var(--sp-6) (24px) side padding so the subnav buttons sit at the
       same left/right edges as the page content (comp grid, coverage
       groups, etc.). Earlier 14px side padding made the subnav inner
       extend ~10px wider on each side than the content below — visible
       misalignment on wide viewports. */
    margin: 8px auto 0;
    max-width: 1280px;
    padding: 8px var(--sp-6);
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: 8px;
    flex-wrap: wrap;
}
/* Scoped overrides so subnav buttons match the dark surface (borders blend instead of looking like stray light-theme chips) */
.subnav .tab-btn { border-color: #223049; color: var(--c-text-2); }
.subnav .config-select { font-size: var(--fs-xs); padding: 3px 26px 3px 10px; min-width: 160px; }
.subnav .tab-btn:hover { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }
.subnav .tab-btn.active { background: #33E4DE12; border-color: var(--c-accent); color: var(--c-accent); }
.subnav .tab-btn.btn-danger { color: var(--c-bad); border-color: #e8707044; }
.subnav .tab-btn.btn-danger:hover { background: var(--c-bad); border-color: var(--c-bad); color: var(--c-bg); }

.nav-wrap { position: relative; }
.nav-trigger {
    font-size: var(--fs-micro);
    color: #666;
    letter-spacing: 2px;
    text-transform: uppercase;
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 2px 0;
    transition: color 0.15s;
}
.nav-trigger:hover,
.nav-trigger.open { color: #0f7e7a; }
.nav-trigger .caret { font-size: 8px; }
.nav-dropdown {
    display: none;
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 6px;
    min-width: 220px;
    box-shadow: 0 4px 20px #0008;
    z-index: 100;
    overflow: hidden;
}
.nav-dropdown.open { display: block; }
.nav-dropdown a {
    display: block;
    padding: 10px 14px;
    font-size: var(--fs-md);
    color: #ccc;
    text-decoration: none;
    border-bottom: 1px solid var(--c-border);
    transition: background 0.1s, color 0.1s;
}
.nav-dropdown a:last-child { border-bottom: none; }
.nav-dropdown a:hover { background: #33E4DE14; color: var(--c-accent); }

/* ── WoW-style buff tooltip ── */
#buff-tooltip {
    position: fixed;
    z-index: 9999;
    pointer-events: none;
    display: none;
    min-width: 205px;
    max-width: 295px;
    background: linear-gradient(160deg, #1c1508 0%, #0d0b07 100%);
    border: 1px solid #8b7355;
    border-radius: 4px;
    padding: 10px 13px;
    box-shadow: 0 6px 24px #0009, inset 0 0 0 1px #3a2e1a;
    font-family: 'Trebuchet MS', sans-serif;
}
.tt-title {
    font-size: var(--fs-base);
    font-weight: 700;
    color: #ffd100;
    margin-bottom: 7px;
    line-height: 1.2;
}
.tt-divider {
    height: 1px;
    background: linear-gradient(to right, transparent, #8b7355aa, transparent);
    margin: 6px 0;
}
.tt-row {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    font-size: var(--fs-xs);
    margin-bottom: 3px;
    color: #7a7060;
}
.tt-row span:last-child { color: #d0c8b8; text-align: right; }
.tt-provider { font-size: var(--fs-xs); margin-bottom: 2px; }
.tt-body {
    font-size: var(--fs-sm);
    color: #c8c0a8;
    line-height: 1.5;
    margin-bottom: 2px;
}
.tt-notes {
    font-size: var(--fs-xs);
    color: #7a7060;
    font-style: italic;
    line-height: 1.4;
    margin-bottom: 2px;
}
.tt-section-label {
    font-size: 9px;
    color: #7a7060;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    margin-bottom: 4px;
}
.tt-prio {
    display: inline-block;
    margin-top: 6px;
    padding: 2px 8px;
    border-radius: 3px;
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: 1px;
    text-transform: uppercase;
}

/* ── Bench ── */
.bench-scroll {
    padding: 5px;
    /* ~6 player cards (42px + 3px margin each) before scrolling — keeps the
       sit-out counts pinned below the list visible instead of cut off. */
    max-height: 277px;
    overflow-y: auto;
}
.bench-scroll .player-card { margin-bottom: 3px; }
.bench-empty { font-size: var(--fs-sm); color: #555; font-style: italic; padding: 6px 8px; }

/* ── Coverage tab ── */
.coverage-top {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
}
.cov-card {
    background: var(--c-surface);
    border-radius: 8px;
    border: 1px solid var(--c-border);
    padding: 1rem;
}
.cov-title {
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 2px;
    text-transform: uppercase;
    margin-bottom: 10px;
}
.cov-title.critical { color: var(--c-bad); }
.cov-title.raid     { color: var(--c-accent); }
.cov-row {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 6px;
    font-size: var(--fs-md);
}
.cov-row .check { font-size: var(--fs-base); }
.cov-row .check.ok  { color: var(--c-good); }
.cov-row .check.bad { color: #c03030; }
.cov-row .buff-name.ok  { color: #ddd; }
.cov-row .buff-name.bad { color: var(--c-bad); }
.cov-row .sources { color: #888; font-size: var(--fs-xs); margin-left: auto; }
.coverage-groups {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 8px;
}
/* Loot Balance sub-tab — item-centric tier-token contention. One
   .lb-group per tier-token bucket; inside each group a list of
   .lb-piece rows (one per token piece — helm / shoulders / chest /
   legs / hands). Each .lb-piece shows the item link + boss + the
   contesting comp players inline as MS / OS / Owns chips. */

/* Two-column container: column 1 = active raid's contention, column 2
   = optional comparison config picked via dropdown. Side-by-side on
   desktop so officers can read contention across two raids without
   tab-juggling; stacks on mobile so each column gets full width. */
/* The Balance sub nests its own .tab-content-wrap inside the loot tab's
   wrapper — zero its padding/width so it doesn't double the top + side gap
   (it sat lower than the History sub, which has no nested wrapper). */
#loot-sub-balance > .tab-content-wrap { padding: 0; max-width: none; margin: 0; }
.lb-cols {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--sp-5);
    align-items: start;
}
.lb-col {
    /* min-width:0 lets grid children honor their content's wrap
       behavior (without it, a long item name in either column can
       force the column wider than 1fr would permit). */
    min-width: 0;
}
@media (max-width: 900px) {
    .lb-cols {
        grid-template-columns: 1fr;
    }
}
/* Per-column header row — sits above each column's rendered
   contention. Both columns use the same `.lb-col-head` so column 2's
   "Compare with: [dropdown]" row matches column 1's "Loot contention:
   <name>" row in height, keeping the section titles below them
   vertically aligned across both columns. Min-height matches the
   .config-select's natural height (~36px) so the text-only column 1
   head doesn't read shorter than the column 2 head with its select. */
.lb-col-head {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin-bottom: var(--sp-3);
    min-height: 36px;
    flex-wrap: wrap;
}
.lb-col-head-label {
    font-size: var(--fs-sm);
    color: var(--c-text-3);
    font-weight: 600;
}
.lb-col-head-name {
    color: var(--c-text);
    font-size: var(--fs-base);
    font-weight: 500;
}
.lb-col-head .config-select {
    flex: 1;
    min-width: 200px;
}
.lb-header {
    font-size: var(--fs-base);
    color: var(--c-text);
    margin-bottom: var(--sp-3);
    padding: 4px 0;
}
.lb-header .lb-raid {
    color: var(--c-text-3);
    font-size: var(--fs-sm);
    margin-left: 6px;
}
.lb-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--sp-3);
}
.lb-card {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-left: 3px solid var(--c-border);
    border-radius: 6px;
    padding: 10px 12px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}
.lb-card.lb-warn  { border-left-color: var(--c-warn); }
.lb-card.lb-bad   { border-left-color: var(--c-bad); }
.lb-card.lb-ok    { border-left-color: var(--c-good); }
.lb-card.lb-empty { border-left-color: var(--c-text-faint); }
.lb-card-head {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.lb-title {
    font-size: var(--fs-base);
    font-weight: 600;
    color: var(--c-text);
}
.lb-classes {
    font-size: var(--fs-xs);
    color: var(--c-text-3);
}
/* ── Section dividers (Tier tokens / All other loot) ── */
.lb-section-title {
    font-size: var(--fs-sm);
    font-weight: 700;
    letter-spacing: 0.1em;
    color: var(--c-text-faint);
    text-transform: uppercase;
    margin: var(--sp-5) 0 var(--sp-2);
    padding-bottom: 4px;
    border-bottom: 1px solid var(--c-border);
}
.lb-section-title:first-child { margin-top: 0; }
.lb-empty-section {
    font-size: var(--fs-sm);
    color: var(--c-text-faint);
    font-style: italic;
    padding: var(--sp-3);
    background: var(--c-surface);
    border: 1px dashed var(--c-border);
    border-radius: 6px;
}
/* Pieces where every contestant already owns the item — dim them
   so officers see at a glance they're 'parked' (not actively
   needing more contention). */
.lb-piece-owned-only {
    opacity: 0.55;
}
/* ── Item-centric layout (replaces the bucket-summary cards) ── */
.lb-groups {
    display: flex;
    flex-direction: column;
    gap: var(--sp-4);
}
.lb-group {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 6px;
    overflow: hidden;
}
.lb-group-head {
    background: var(--c-surface-2);
    padding: 10px 14px;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    border-bottom: 1px solid var(--c-border);
    flex-wrap: wrap;
}
.lb-group-title {
    font-size: var(--fs-base);
    font-weight: 700;
    letter-spacing: 0.04em;
    color: var(--c-accent);
    text-transform: uppercase;
}
.lb-group-meta {
    display: flex;
    align-items: baseline;
    gap: 14px;
    font-size: var(--fs-xs);
    color: var(--c-text-3);
}
.lb-group-classes { color: var(--c-text-3); }
.lb-group-pieces {
    color: var(--c-text-faint);
    font-weight: 600;
}
.lb-pieces {
    display: flex;
    flex-direction: column;
}
.lb-piece {
    padding: 9px 14px;
    border-bottom: 1px solid var(--c-border);
}
.lb-piece:last-child { border-bottom: none; }
.lb-piece-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 12px;
    margin-bottom: 6px;
}
.lb-piece-item {
    flex: 1;
    min-width: 0;
}
/* The .wl-item anchor inside .lb-piece-item already styles itself
   with the icon + colored quality name from item-meta. We just need
   it to behave inline-flex for vertical alignment with the boss. */
.lb-piece-item .wl-item {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    text-decoration: none;
}
.lb-piece-name-fallback {
    color: var(--c-text-2);
    font-weight: 500;
}
.lb-piece-boss {
    color: var(--c-text-faint);
    font-size: var(--fs-sm);
    font-style: italic;
    flex-shrink: 0;
    text-align: right;
}
.lb-piece-contestants {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 6px;
}
.lb-contestant {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    padding: 2px 8px;
    border-radius: 12px;
    font-size: var(--fs-xs);
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid var(--c-border);
}
.lb-contestant.lb-c-ms   { border-color: rgba(226, 75, 74, 0.4); background: rgba(226, 75, 74, 0.08); }
.lb-contestant.lb-c-os   { border-color: rgba(232, 140, 77, 0.4); background: rgba(232, 140, 77, 0.06); }
.lb-contestant.lb-c-owns { border-color: rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.02); opacity: 0.55; }
.lb-c-name {
    font-weight: 600;
}
.lb-c-tag {
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--c-text-3);
}
.lb-contestant.lb-c-ms .lb-c-tag   { color: var(--c-bad); }
.lb-contestant.lb-c-os .lb-c-tag   { color: var(--c-warn); }
.lb-contestant.lb-c-owns .lb-c-tag { color: var(--c-text-faint); }

/* ── Drag-drop on Loot Balance compare ──
   Officers/GM can pick up a pill and drop on another column / pool
   strip / different group / different pill to move or swap.
   - .lb-c-draggable: grab cursor on hover so the affordance is obvious
   - .lb-pill-dragging: ghost the source pill during drag
   - .lb-drop-target / .lb-drop-hover on whole zones use a subtle
     bg-tint only — no big dashed border around the entire column /
     row (per user: "I need to be able to see not the entire border
     selected with dashed lines, but I want to see that I am able to
     drop a player right in front of another by having a divider").
   - The PRECISE drop position is shown by the .lb-drop-before /
     .lb-drop-after divider lines on the target pill (.lb-gp-pill),
     glowing accent stripe on the cursor-side of that pill. */
.lb-contestant.lb-c-draggable {
    cursor: grab;
}
.lb-contestant.lb-c-draggable:active {
    cursor: grabbing;
}
.lb-contestant.lb-pill-dragging {
    /* Strong cue that the pill is being dragged — opacity drop +
       slight scale-down + grayscale so it pops the moment the
       browser fires dragstart. No transitions so the change is
       instant (per user feedback that the visual was lagging). */
    opacity: 0.25;
    transform: scale(0.92);
    filter: grayscale(0.6);
}
.lb-col.lb-drop-target,
.lb-pool-row.lb-drop-target {
    transition: background 0.12s ease;
}
.lb-col.lb-drop-hover,
.lb-pool-row.lb-drop-hover {
    background: rgba(70, 200, 195, 0.05);
}

/* ── Available roster "free pool" strip ──
   Sits above the two columns in compare mode. Shows roster members
   not currently assigned to either compared raid as draggable pills.
   Drop a pill here to release a player; drag from here into a column
   to assign. Single horizontal flex row that wraps as needed.
   Sticky-pinned BELOW the .raid-subnav so it stays in view while
   the user scrolls through long Loot Balance contention — the offset
   is `--sticky-header-h + --sticky-raid-subnav-h`, where the subnav
   height is measured at runtime by a ResizeObserver in
   _lbInstallSubnavHeightTracker so wrap-onto-two-rows on mobile or
   feature-flag toggles (e.g. Loot System bar showing/hiding) keep
   the offset accurate. z-index sits below the subnav's z=5 so the
   subnav covers if they ever overlap. */
.lb-pool-row {
    /* Stack label-on-top, pills below, optional hint last. Per user
       — the inline left-of-pills layout pushed the pill row into a
       narrower remaining-width slot and read cluttered when the
       roster pool got wide. Vertical stack gives the pills the full
       row width and the label a clear header treatment. */
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--sp-1);
    padding: var(--sp-2) 0;
    margin-bottom: var(--sp-3);
    /* Match the .raid-subnav's bg + drop the surrounding card border
       so the pool reads as a continuation of the sticky nav rather
       than a separate boxed surface. Border-bottom mirrors the
       subnav's bottom rule so there's still a clean horizontal
       divider separating the unified sticky from the columns below. */
    background: var(--c-bg);
    border: none;
    border-bottom: 1px solid var(--c-border);
    border-radius: 0;
    font-size: var(--fs-sm);
    min-height: 36px;
    position: sticky;
    /* Pin flush under the page header. On the Loot tab (where this lives now)
       the only sticky element above the pool is the header — the loot
       sub-tabs nav is NOT sticky — so offset by just --sticky-header-h.
       (It used to add --sticky-raid-subnav-h from its old Raid-Builder home,
       but that subnav doesn't exist here, which left a see-through gap.) */
    top: var(--sticky-header-h, 100px);
    z-index: 4;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0), 0 8px 22px rgba(0, 0, 0, 0);
    transition: box-shadow 0.18s ease;
}
/* Doubled drop-shadow once pinned (close inner + softer outer) so the strip
   clearly lifts off the contention scrolling beneath — same treatment as the
   pug-reserve sticky hero. */
.lb-pool-row.is-stuck {
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6), 0 8px 22px rgba(0, 0, 0, 0.55);
}
/* Pool strip drops its sticky on mobile in lockstep with the
   .raid-subnav (the offset calc for top would point at the subnav
   height, which itself scrolls away on phones). Static layout on
   phones reads cleaner and doesn't fight the subnav for screen
   real estate. */
@media (max-width: 700px) {
    .lb-pool-row {
        position: static;
        box-shadow: none;
    }
}
.lb-pool-label {
    font-weight: 700;
    color: var(--c-text-3);
    text-transform: uppercase;
    font-size: var(--fs-xs);
    letter-spacing: 0.05em;
    flex-shrink: 0;
}
.lb-pool-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    min-width: 0;
}
.lb-pool-empty {
    color: var(--c-text-faint);
    font-style: italic;
    font-size: var(--fs-xs);
}
.lb-pool-hint {
    color: var(--c-text-faint);
    font-style: italic;
    font-size: var(--fs-xs);
    flex-shrink: 0;
    /* Pool layout is column-flex now (label on top, pills below).
       The hint sits at the bottom — no margin-left:auto needed
       since the column doesn't push siblings horizontally. */
}
@media (max-width: 700px) {
    /* On phones, stack the hint below the pills so they get full width. */
    .lb-pool-hint { display: none; }
}
/* Pool pill chrome — neutral (no MS/OS/Owns coloring) since pool
   players aren't contesting anything yet. Class color carries on
   .lb-c-name like the column pills. */
.lb-contestant.lb-c-pool {
    background: rgba(255, 255, 255, 0.03);
    border-color: rgba(255, 255, 255, 0.12);
}
/* Partial-availability cue — pill is dimmed slightly + warm border
   to signal "available for ONE of the two raids but not both". The
   tooltip on hover names the specific raid that's a no-go. Players
   unavailable for BOTH raids are filtered out of the pool entirely. */
.lb-contestant.lb-c-pool.lb-c-pool-partial {
    opacity: 0.65;
    border-color: rgba(232, 140, 77, 0.5);
    background: rgba(232, 140, 77, 0.06);
}

/* ── Party preview (10m only) ──
   Two rows of 5 pills under each column head showing the full Group 1
   + Group 2 comp. Lets officers see at-a-glance who's in each raid
   AND drag any rostered slot — including players with no contested
   wishlist items — into the other column or the pool. */
.lb-groups-preview {
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
    margin-bottom: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 6px;
    font-size: var(--fs-xs);
}
.lb-gp-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
}
.lb-gp-label {
    font-weight: 700;
    color: var(--c-text-3);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    flex-shrink: 0;
    min-width: 56px;
}
.lb-gp-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}
/* On mobile, stack the group label above the pill row instead of
   having "Group 1: pill pill pill" all on one line. Per user — the
   label-on-top layout reads cleaner on phone widths where 5 pills
   would otherwise spill into multiple wrapped lines beside the label. */
@media (max-width: 700px) {
    .lb-gp-row {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--sp-1);
    }
    .lb-gp-label {
        min-width: 0;
    }
    .lb-gp-pills {
        width: 100%;
    }
}
.lb-gp-pill {
    padding: 2px 8px;
    border-radius: 12px;
    background: rgba(255, 255, 255, 0.04);
    border: 1px solid var(--c-border);
    font-weight: 600;
    cursor: default;
}
.lb-gp-pill.lb-c-draggable {
    cursor: grab;
}
.lb-gp-pill.lb-c-draggable:active {
    cursor: grabbing;
}
.lb-gp-pill.lb-pill-dragging {
    opacity: 0.4;
}
.lb-gp-empty {
    padding: 2px 8px;
    color: var(--c-text-faint);
    font-style: italic;
}
/* Row-level drop hover — subtle bg-tint only, no dashed outline.
   The PRECISE drop position is conveyed by the per-pill divider
   indicator (.lb-drop-before / .lb-drop-after below). */
.lb-gp-row.lb-drop-target {
    transition: background 0.12s ease;
}
.lb-gp-row.lb-drop-hover {
    background: rgba(70, 200, 195, 0.04);
}

/* Per-pill insertion-line indicator. A vertical accent stripe
   appears on the cursor-side of the pill being hovered, showing
   exactly where the dragged player will land. .lb-drop-before pins
   the stripe to the left edge (drop in front of this pill); .lb-drop-
   after pins it to the right edge (drop after this pill). The pill
   itself doesn't get an outline — the stripe is the only visual cue,
   per user feedback. */
.lb-gp-pill {
    position: relative;
}
.lb-gp-empty {
    position: relative;
}
.lb-gp-pill::before,
.lb-gp-pill::after,
.lb-gp-empty::before,
.lb-gp-empty::after {
    content: '';
    position: absolute;
    top: -3px;
    bottom: -3px;
    width: 3px;
    border-radius: 2px;
    background: transparent;
    pointer-events: none;
    /* No transition — the divider should pop the moment the cursor
       crosses into a pill's drop-side zone. Per user: "can it show
       up sooner" — fading in via a transition felt laggy. */
}
.lb-gp-pill::before, .lb-gp-empty::before { left: -4px; }
.lb-gp-pill::after,  .lb-gp-empty::after  { right: -4px; }
.lb-gp-pill.lb-drop-before::before,
.lb-gp-empty.lb-drop-before::before,
.lb-gp-pill.lb-drop-after::after,
.lb-gp-empty.lb-drop-after::after {
    background: var(--c-accent);
    box-shadow: 0 0 8px rgba(70, 200, 195, 0.7);
}
.lb-piece-empty {
    color: var(--c-text-faint);
    font-size: var(--fs-xs);
    font-style: italic;
}
/* Summary row: MS / OS / Owns counts at the top of each card. Pills
   live in a flex strip with the piece-count caption pushed right. */
.lb-summary {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: baseline;
    font-size: var(--fs-sm);
}
.lb-summary-num {
    font-weight: 600;
    padding: 1px 6px;
    border-radius: 3px;
    background: rgba(255, 255, 255, 0.04);
}
.lb-summary-ms   { color: var(--c-bad); }
.lb-summary-os   { color: var(--c-warn); }
.lb-summary-have { color: var(--c-text-3); }
.lb-summary-pieces {
    margin-left: auto;
    font-size: var(--fs-xs);
    color: var(--c-text-faint);
}
.lb-badge {
    font-size: var(--fs-xs);
    color: var(--c-text-3);
}
.lb-badge-warn { color: var(--c-warn); }
.lb-badge-bad  { color: var(--c-bad); }
.lb-badge-ok   { color: var(--c-good); }
/* Per-player rows. Name on the left, MS / OS / ✓ tag pills on the
   right. Reads as a mini-table at a glance: who's contesting and how
   hard. */
.lb-players {
    display: flex;
    flex-direction: column;
    gap: 3px;
    font-size: var(--fs-sm);
    border-top: 1px solid var(--c-border);
    padding-top: 6px;
}
.lb-player {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px;
}
.lb-player-name {
    font-weight: 500;
    flex: 1;
    min-width: 0;
}
.lb-player-tags {
    display: inline-flex;
    gap: 4px;
    flex-shrink: 0;
}
.lb-tag {
    font-size: var(--fs-xs);
    font-weight: 700;
    padding: 1px 5px;
    border-radius: 3px;
    letter-spacing: 0.02em;
}
.lb-tag-ms   { color: var(--c-bad); background: rgba(226, 75, 74, 0.12); }
.lb-tag-os   { color: var(--c-warn); background: rgba(232, 140, 77, 0.12); }
.lb-tag-have { color: var(--c-text-2); background: rgba(255, 255, 255, 0.06); }
.lb-noint {
    font-size: var(--fs-xs);
    color: var(--c-text-faint);
    font-style: italic;
}
.lb-subtitle {
    color: var(--c-text-faint);
    font-size: var(--fs-xs);
    margin-left: 4px;
}
.lb-drops {
    font-size: var(--fs-xs);
    color: var(--c-text-3);
    border-top: 1px solid var(--c-border);
    padding-top: 6px;
    line-height: 1.5;
}
.lb-drops-label {
    color: var(--c-text-faint);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-size: 9px;
    margin-bottom: 4px;
}
/* Each drop is a flex row so item names left-align and boss names
   right-align — gives a clean two-column read across all drops in
   the card without needing a real <table>. The 8px gap keeps the
   columns visually separated when item + boss are short. */
.lb-drops-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.lb-drop {
    display: flex;
    justify-content: space-between;
    gap: 8px;
    align-items: baseline;
}
.lb-drop-item {
    color: var(--c-text-2);
    flex: 1;
    min-width: 0;
}
.lb-drop-boss {
    color: var(--c-text-faint);
    flex-shrink: 0;
    text-align: right;
}
.cov-group {
    background: var(--c-surface);
    border-radius: 8px;
    border: 1px solid var(--c-border);
    overflow: hidden;
}
.cov-group-header {
    background: var(--c-surface-2);
    padding: 6px 10px;
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: 2px;
    color: var(--c-accent);
    text-transform: uppercase;
    border-bottom: 1px solid var(--c-border);
}
.cov-group-body { padding: 9px; }
/* Shrink player rows to content width so empty whitespace doesn't
   stretch from the name to the group-card edge. The hover handler
   (showPlayerTooltip) now triggers only over the name itself, which
   matches the affordance — there was nothing to interact with in
   the trailing whitespace anyway. Per user. */
.cov-player { font-size: var(--fs-sm); margin-bottom: 3px; width: fit-content; max-width: 100%; }
.cov-buff-list { border-top: 1px solid var(--c-border); padding-top: 8px; margin-top: 8px; }
.cov-prio-section { margin-top: 8px; }
.cov-prio-section:first-child { margin-top: 0; }
.cov-prio-label {
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--c-text-faint);
    margin-bottom: 4px;
}
.cov-prio-section .buff-group-tags { flex-direction: column; align-items: flex-start; gap: 3px; }
.cov-global-card {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 8px;
    padding: 10px 12px 12px;
    /* No margin — the parent .tab-content-wrap is a flex column with
       gap: var(--sp-2), which handles the spacing between this card
       and the .coverage-groups grid below. The earlier 18px bottom
       margin stacked on top of the flex gap (margin doesn't collapse
       inside flex) for ~26px total — read as too much breathing room. */
    margin: 0;
}
.cov-global-title {
    font-size: var(--fs-md);
    font-weight: 700;
    letter-spacing: 2px;
    color: var(--c-accent);
    text-transform: uppercase;
    margin-bottom: 10px;
}
.cov-global-cols {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 18px;
}
.cov-global-col {
    min-width: 0;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 6px;
    padding: 10px 12px;
}
.cov-global-coltitle {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #aab4c2;
    margin-bottom: 8px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--c-border);
}
@media (max-width: 720px) {
    .cov-global-cols { grid-template-columns: 1fr; gap: 12px; }
}
.cov-buff-row {
    display: flex;
    align-items: center;
    gap: 5px;
    margin-bottom: 4px;
    font-size: var(--fs-xs);
}
.cov-dot {
    width: 5px; height: 5px;
    border-radius: 50%;
    flex-shrink: 0;
}
.cov-dot.Critical    { background: #E0703A; }
.cov-dot.High        { background: #4080c0; }
.cov-dot.Situational { background: #bbb; }
.cov-buff-name { color: #555; }
.cov-scope { font-size: 9px; color: #bbb; margin-left: auto; }
.cov-none { font-size: var(--fs-xs); color: #888; font-style: italic; }

/* ── Tab panels ── */
.tab-panel { display: none; padding-bottom: 12px; }
.tab-panel.active { display: block; }

/* ── Raid Builder subnav ── */
/* Mirrors .loot-subnav structure: subnav row with tab buttons +
   per-raid config controls, sitting at the top of the unified
   tab-raidbuilder content. Was previously a free-floating element
   between the page header and tab content (in header.php); moved
   inside the tab-panel so it visually belongs to the content area. */
.raid-tab-wrap {
    /* Match .loot-tab-wrap so subnav is aligned with the centered
       content + sits with the same top inset (var(--sp-5) above,
       var(--sp-6) on each side, max-width 1280 with auto margins).
       Earlier this was padding:0 which made the subnav run flush
       to the viewport edges and read as detached from the content. */
    padding: var(--sp-5) var(--sp-6) 0;
    max-width: 1280px;
    margin: 0 auto;
}
/* The inner sub-view partials (tab-comp / tab-buffs / tab-coverage)
   each contain a `.tab-content-wrap` with its own padding + max-width
   + auto margins — that styling is correct for their original use
   (top-level tab-panels), but when nested inside the new
   .raid-tab-wrap they'd double-pad horizontally and re-cap width.
   Zero out the inner wrapper's offsets so the outer wrap wins. The
   inner flex column layout (display:flex; flex-direction:column;
   gap:var(--sp-2)) is preserved. */
.raid-tab-wrap .tab-content-wrap {
    padding: 0;
    max-width: none;
    margin: 0;
}
.raid-subnav {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    align-items: center;
    margin-bottom: var(--sp-5);
    border-bottom: 1px solid var(--c-border);
    padding-bottom: var(--sp-3);
    /* Sticky pin against the page header — same pattern the wishlist
       sticky toolbar (.wl-sticky-toolbar) uses on the Loot page. The
       subnav stays in view while the user scrolls through long
       sub-views (Group Editor / Loot Balance / Buff Coverage / Buff
       Editor) so they never lose access to the sub-tab buttons or the
       Select Raid dropdown.
       - top: var(--sticky-header-h) anchors below the page header.
       - background: var(--c-bg) keeps content from showing through.
       - padding-top + negative margin-top neutralize each other so the
         unpinned visual position stays where it was; the breathing
         room above the buttons only matters once pinned.
       - box-shadow gates on .is-stuck (added by IntersectionObserver
         once the bar starts pinning) so unpinned scroll position has
         no shadow. */
    position: sticky;
    top: var(--sticky-header-h, 100px);
    z-index: 5;
    background: var(--c-bg);
    /* Padding-top matches the EFFECTIVE breathing room above the
       wishlist sticky toolbar's filter buttons (=parent .wl-sticky-
       toolbar's padding-top var(--sp-2) + child .wl-armor-filter's
       padding-top var(--sp-2) = ~16px combined). The raid subnav is a
       single flat row so we collapse that into one var(--sp-4) (14px)
       so the buttons don't kiss the page-header bottom when pinned.
       Negative margin-top neutralizes the new padding so the unpinned
       bar sits at its original Y. */
    padding-top: var(--sp-4);
    margin-top: calc(-1 * var(--sp-4));
    box-shadow: 0 6px 8px -4px rgba(0, 0, 0, 0);
    transition: box-shadow 0.15s ease;
}
.raid-subnav.is-stuck {
    box-shadow: 0 6px 8px -4px rgba(0, 0, 0, 0.55);
}
/* On mobile, the sticky raid subnav burns precious vertical space —
   the page is already short enough that scrolling-as-normal is fine.
   Per user "on mobile raid builder loot balance the sticky sub nav
   is not needed". position: static drops the sticky behavior; padding
   and negative margin reset to default so the bar sits at its natural
   Y without the desktop's anti-overlap math. */
@media (max-width: 700px) {
    .raid-subnav {
        position: static;
        padding-top: 0;
        margin-top: 0;
        box-shadow: none;
    }
}
.raid-subnav-divider,
.roster-subnav-divider {
    display: inline-block;
    flex: 0 0 auto;   /* a 1px rule must never shrink in the flex row */
    width: 1px;
    height: 22px;
    background: var(--c-border);
    align-self: center;
    margin: 0 var(--sp-1);
}
/* Wraps the three sub-tab buttons so they can be reordered as a unit
   on mobile (config controls go above them, with the subnav's
   border-bottom as the divider between rows). On desktop this is
   transparent — inline-flex with the same gap as .raid-subnav so
   buttons sit side-by-side with no extra spacing. */
.raid-subnav-tabs {
    display: inline-flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    align-items: center;
}
.raid-subnav-extras {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    margin-left: auto;
    flex-wrap: wrap;
}
/* "Loot System: Vis Maior  TMB EXPORT  EDIT" cluster on the right of
   the Raid Builder subnav. The label+provider ("Loot System: Vis
   Maior") is now ONE flex child with a literal space character —
   that handles the colon-to-name gap as natural word-spacing
   (~1 character). This rule only adds spacing BETWEEN that label
   group and the buttons that follow (and between buttons). Margin-
   left rather than flex-gap because gap on the inline-flex span
   collapses to 0 depending on the surrounding flex context (gap
   was 14px before — read as flush). Desktop-scoped (≥701px) so it
   doesn't compound with the mobile rule's `gap: 8px 14px`. 8px =
   smaller breathing room than the previous 14px, per user. */
@media (min-width: 701px) {
    #comp-sr-bar > * + * {
        margin-left: 8px !important;
    }
}
/* The divider following #comp-sr-bar (visual separator between the
   Loot System bar and the config-controls cluster). Hidden when the
   bar itself is empty — i.e. no raid config selected — so we don't
   leave an orphan vertical line next to a blank slot. Order in the
   subnav row was reshuffled per user so the bar now sits BEFORE the
   config controls; the divider's adjacent-sibling check flips
   accordingly. */
.raid-subnav #comp-sr-bar:empty + .comp-sr-divider {
    display: none;
}
/* Site-setting toggle: when lootProviderUiEnabled is false (the
   default), hide the Loot System bar AND its surrounding divider on
   the Raid Builder subnav. The GM can re-enable from the Admin tab.
   Soft hide — the bar itself still renders into the DOM, just gets
   display:none, so existing renderers keep working under the hood. */
body.hide-loot-provider-ui #comp-sr-bar,
body.hide-loot-provider-ui .comp-sr-divider {
    display: none !important;
}
/* The .config-select dropdown's base size (fs-sm + 5px vertical
   padding) is TALLER than .tab-btn.btn-small (fs-xs + 3px vertical),
   so a default-sized select stretched the whole subnav row taller
   than the loot subnav — visually the raid subnav sat ~4px lower
   because the row's vertical center shifted. Scope a btn-small-
   matching height override so the row aligns with the loot subnav.
   Same trick the .loot-subnav-extras .admin-select rule uses. */
.raid-subnav-extras .config-select {
    font-size: var(--fs-xs);
    line-height: 1.4;
    padding: 3px 26px 3px 10px;
    min-width: 160px;
    box-sizing: border-box;
    /* Pin the dropdown's outer height to .tab-btn.btn-small's
       computed height (fs-xs 11px × line-height 1.4 + padding 3+3 +
       border 1+1 ≈ 23.4px) so the SELECT RAID dropdown is identical
       in height to the +New / Edit / Delete buttons sitting next
       to it. <select> elements pick up subtle browser-native
       padding even with appearance:none, so an explicit min-height
       forces parity. */
    min-height: 24px;
    height: 24px;
}
/* Sub-views inside the Raid Builder: only one visible at a time.
   Same idea as .loot-sub. */
.raid-sub { display: none; }
.raid-sub.active { display: block; }

/* ── PUG modal ── */
#pug-modal, #add-player-modal, #add-buff-modal, #player-picker, #confirm-modal, #flags-modal, #notes-modal, #away-modal, #avail-modal, #schedule-modal, #softres-modal, #ssc-resist-modal, #resources-modal, #change-gm-pw-modal, #lootmgr-modal, #feedback-modal, #scout-notes-modal, #scout-export-addon-modal, #scout-import-addon-modal, #journal-video-modal {
    position: fixed; inset: 0; z-index: 200;
    background: rgba(0,0,0,0.5);
    display: flex; align-items: center; justify-content: center;
}
/* Mobile: every modal in this app shares the same flex-centered
   shell (.pug-box / .cfg-box). On phones we want:
     1. The box not to render edge-to-edge (16px viewport padding).
     2. Tall content scrollable — but scrolling the OUTER shell
        rather than the inner box. That way short modals still
        center-align nicely and long modals (e.g. the schedule
        editor with many raid slots) just scroll the whole shell.
        Anchoring align-items at flex-start means the top of the
        modal is always reachable; without it, a content height
        slightly under viewport can still center-overflow above
        the visible area on some browsers' 100vh quirks. */
@media (max-width: 600px) {
    #pug-modal, #add-player-modal, #add-buff-modal, #player-picker,
    #confirm-modal, #flags-modal, #notes-modal, #away-modal,
    #avail-modal, #schedule-modal, #softres-modal, #ssc-resist-modal,
    #resources-modal, #change-gm-pw-modal, #lootmgr-modal,
    #feedback-modal, #scout-notes-modal, #scout-export-addon-modal,
    #scout-import-addon-modal, #journal-video-modal {
        padding: 16px;
        align-items: flex-start;
        overflow-y: auto;
        /* iOS momentum scrolling so the modal feels native. */
        -webkit-overflow-scrolling: touch;
    }
    .cfg-box, .pug-box {
        /* Natural height — the OUTER shell handles the scroll.
           min-width:0 lets the box shrink to fit the padded shell
           rather than insisting on its desktop min-width:300px
           (which would force horizontal scroll on 360px phones). */
        max-height: none;
        min-width: 0;
        margin: auto;
    }
}
.pug-box {
    background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: 8px;
    padding: 18px 22px; min-width: 240px;
}
.pug-box h3 { font-size: var(--fs-xs); color: var(--c-accent); letter-spacing: 2px; text-transform: uppercase; margin-bottom: 12px; }
.pug-box label { font-size: var(--fs-micro); color: #aaa; display: block; margin-bottom: 4px; margin-top: 8px; }
/* Modal-scoped form controls inherit the canonical .admin-input / .admin-select
   sizing (padding 5px 13px, --fs-sm, --c-bg surface) so unmarked selects/inputs
   inside pug-box / cfg-box modals look identical to ones with explicit classes. */
.pug-box select, .cfg-box select,
.pug-box input[type="text"], .pug-box input[type="email"], .pug-box input[type="url"],
.pug-box input[type="password"], .pug-box input[type="search"], .pug-box input[type="number"],
.pug-box input[type="date"], .pug-box input[type="time"], .pug-box textarea,
.cfg-box input[type="text"], .cfg-box input[type="email"], .cfg-box input[type="url"],
.cfg-box input[type="password"], .cfg-box input[type="search"], .cfg-box input[type="number"],
.cfg-box input[type="date"], .cfg-box input[type="time"], .cfg-box textarea {
    width: 100%; box-sizing: border-box;
    background: var(--c-bg); border: 1px solid #223049; color: var(--c-text);
    font-family: inherit; font-size: var(--fs-sm); line-height: 1.4;
    padding: 5px 13px; border-radius: 4px; outline: none;
    transition: border-color 0.15s;
}
/* The native date/time picker icon renders near-black, which is invisible on
   our dark inputs (the calendar/clock control in New Raid Config etc.). Invert
   it to a light grey so it reads against the dark background. */
.cfg-box input[type="date"]::-webkit-calendar-picker-indicator,
.cfg-box input[type="time"]::-webkit-calendar-picker-indicator,
.pug-box input[type="date"]::-webkit-calendar-picker-indicator,
.pug-box input[type="time"]::-webkit-calendar-picker-indicator {
    filter: invert(0.85);
    cursor: pointer;
    opacity: 0.85;
}
.cfg-box input[type="date"]::-webkit-calendar-picker-indicator:hover,
.cfg-box input[type="time"]::-webkit-calendar-picker-indicator:hover,
.pug-box input[type="date"]::-webkit-calendar-picker-indicator:hover,
.pug-box input[type="time"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }
/* Mirror the .admin-select chevron treatment so modal selects look like
   their first-class siblings (Schedule modal, Add Player modal, etc.).
   Native chevron is suppressed via appearance:none and replaced with a
   cleaner inline SVG; padding-right makes room for it. */
.pug-box select, .cfg-box select {
    cursor: pointer;
    padding-right: 28px;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%23cfd9e6' d='M0 0l5 6 5-6z'/></svg>");
    background-repeat: no-repeat;
    background-position: right 10px center;
}
.pug-box select:focus, .cfg-box select:focus,
.pug-box input:focus, .cfg-box input:focus,
.pug-box textarea:focus, .cfg-box textarea:focus { border-color: var(--c-accent); }
.pug-box-actions { display: flex; gap: 8px; margin-top: 14px; }
.pug-box-actions button { flex: 1; }

/* ── Config modal ── */
#config-modal,
#gargul-export-modal, #add-loot-modal, #help-modal {
    position: fixed; inset: 0; z-index: 200;
    background: rgba(0,0,0,0.55);
    display: flex; align-items: center; justify-content: center;
}
/* Help modal: cap height + scroll so long help content (loot import, journal
   editing, etc.) stays usable. The body reuses .wl-help styling. */
#help-modal .help-modal-box { max-height: 82vh; overflow-y: auto; }
#help-modal #help-modal-title { color: var(--c-accent); margin: 0 0 var(--sp-3); padding-right: 28px; }
#help-modal #help-modal-body { margin-bottom: 0; }
.cfg-box {
    background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: 8px;
    padding: 22px 26px; min-width: 300px; max-width: 380px; width: 100%;
}
.cfg-box h3 { font-size: var(--fs-xs); color: var(--c-accent); letter-spacing: 2px; text-transform: uppercase; margin-bottom: 16px; }
.cfg-box label:not(.cfg-raid-opt) { font-size: var(--fs-micro); color: #aaa; display: block; margin-bottom: 4px; margin-top: 12px; }
/* (cfg-box input/select sizing now lives in the shared modal-form rule above) */
.cfg-raids {
    display: grid; grid-template-columns: 1fr 1fr; gap: 4px 12px;
    margin-top: 4px;
}
.cfg-raid-opt {
    display: flex; align-items: center; gap: 6px;
    font-size: var(--fs-sm); color: #ccc; cursor: pointer; padding: 3px 0;
}
.cfg-raid-opt input[type="checkbox"] { accent-color: var(--c-accent); cursor: pointer; }
.cfg-box .cfg-preview {
    margin-top: 14px; padding: 8px 10px; background: var(--c-surface);
    border: 1px solid var(--c-border); border-radius: 4px;
    font-size: var(--fs-xs); color: #888;
}
.cfg-box .cfg-preview span { color: var(--c-accent); font-weight: 600; }
.cfg-box-actions { display: flex; gap: 8px; margin-top: 16px; }
.cfg-box-actions button { flex: 1; }
.tt-provider {
    font-size: var(--fs-xs); margin-bottom: 3px; padding: 2px 6px;
    border-radius: 3px;
}

/* ── Attendance / Roster tab ── */
.at-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.at-table th {
    text-align: left; padding: 7px 12px; font-size: var(--fs-xs); letter-spacing: 1px;
    text-transform: uppercase; color: #888; border-bottom: 1px solid var(--c-border);
    font-weight: 600; cursor: pointer; user-select: none; white-space: nowrap;
}
.at-table th:hover { color: #aaa; }
.at-table th.sort-asc::after  { content: ' ↑'; color: var(--c-accent); }
.at-table th.sort-desc::after { content: ' ↓'; color: var(--c-accent); }
.at-table td { padding: 7px 12px; border-bottom: 1px solid var(--c-surface); font-size: var(--fs-md); color: #bbb; }
.at-table tr:last-child td { border-bottom: none; }
.at-table tr:hover td { background: #ffffff08; }
.at-table td.at-ap { font-weight: 700; color: var(--c-accent); font-size: var(--fs-md); }
.at-table td.at-lifetime { color: var(--c-text-2); }
.at-table td.at-clickable { cursor: pointer; }
.at-table td.at-clickable:hover { text-decoration: underline; text-underline-offset: 3px; }
/* Reusable clickable-points control (pointsLink) — bare button that inherits
   its host's colour/size and opens the points-breakdown modal. */
.points-link { background: none; border: none; padding: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; user-select: none; -webkit-user-select: none; }
.points-link:hover { text-decoration: underline; text-underline-offset: 3px; }
/* No lingering focus ring after clicking (mouse); keep it for keyboard users. */
.points-link:focus:not(:focus-visible) { outline: none; }
/* ── Points breakdown modal ── */
.pbd-modal {
    background: var(--c-surface); border: 1px solid var(--c-border);
    border-radius: var(--r-md); padding: 18px; width: 94%; max-width: 480px;
    max-height: 82vh; overflow: auto; box-shadow: 0 10px 40px rgba(0,0,0,.5);
}
.pbd-h3 { margin: 0 0 12px; font-size: var(--fs-md); }
.pbd-totals { display: flex; gap: 10px; }
.pbd-total { flex: 1; background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: var(--r-md); padding: 8px 12px; text-align: center; }
.pbd-total-label { font-size: var(--fs-xs); color: var(--c-text-faint); text-transform: uppercase; letter-spacing: .04em; }
.pbd-total-val { font-size: var(--fs-lg); font-weight: 800; }
.pbd-formula { font-size: var(--fs-sm); color: var(--c-text-3); margin: 10px 0 4px; }
.pbd-section { margin-top: 14px; }
.pbd-sec-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; border-bottom: 1px solid var(--c-border); padding-bottom: 4px; }
.pbd-sec-title { font-weight: 700; font-size: var(--fs-sm); }
.pbd-count { color: var(--c-text-faint); font-weight: 400; }
.pbd-sec-sub { font-size: var(--fs-xs); color: var(--c-text-faint); }
.pbd-row { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding: 3px 0; font-size: var(--fs-sm); }
.pbd-cfg { color: var(--c-text-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pbd-cfg-sub { color: var(--c-text-faint); font-size: var(--fs-xs); }
.pbd-amt { flex-shrink: 0; font-weight: 700; font-variant-numeric: tabular-nums; }
.pbd-plus { color: var(--c-good); }
.pbd-minus { color: var(--c-bad); }
.pbd-zero { color: var(--c-text-faint); }
.pbd-empty { color: var(--c-text-faint); font-size: var(--fs-sm); padding: 3px 0; }
.pbd-actions { display: flex; justify-content: flex-end; margin-top: 16px; }
/* ── Roster Summary ── */
#roster-summary { display: flex; flex-direction: column; gap: 12px; margin-bottom: 24px; }
.rsm-section { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: 8px; padding: 10px 14px; min-width: 0; }
.rsm-section-label { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .08em; color: var(--c-text-faint); margin-bottom: 8px; font-weight: 600; }
.rsm-pills { display: flex; flex-wrap: wrap; gap: 6px; }
/* Cap a pill row at 6 columns: line wraps to row 2 with item 7+,
   AND each pill sizes to its own content (no column-shared width),
   AND the parent section sizes to the widest row's content (so the
   sibling Classes section can claim freed horizontal space).
   Implemented as a column-flex of explicit `.rsm-rank-row` chunks
   (6 pills each), grouped in JS — each chunk is its own no-wrap
   flex row, so pill widths in row 1 don't leak into row 2 (and
   vice versa) and there's no flex-basis:100% spacer to confuse the
   parent's natural-width resolver.
   Earlier attempts:
   - CSS grid `repeat(6, max-content)` → every column inherited its
     widest cell's width across both rows; "GM 1" in col 1 row 1
     padded out to match "Vanguard 10" in col 1 row 2.
   - Flex with a `flex-basis: 100%` row-break stub → forced a wrap
     correctly, but the stub's "100% of container" basis blew the
     parent .rsm-section's natural width up to fill the page,
     squeezing the Classes section into a 1-pill column. */
.rsm-pills.rsm-pills-cap6 {
    display: flex;
    flex-direction: column;
    gap: 6px;
    align-items: flex-start;
}
.rsm-rank-row {
    display: flex;
    flex-wrap: nowrap;
    gap: 6px;
}
/* On mobile, the no-wrap row of 6 rank pills can overflow the
   viewport — particularly with two-word labels like "Officer Alt"
   that don't shrink. Allow wrapping inside the row so pills cascade
   to the next line instead of clipping past the screen edge. The
   6-per-row "cap" is then no longer enforced visually on phones,
   but every pill stays readable, which is what matters there. */
@media (max-width: 700px) {
    .rsm-rank-row {
        flex-wrap: wrap;
    }
}
/* white-space:nowrap stops two-word ranks like "Officer Alt" / "Core Alt"
   from wrapping their text across two lines when the row gets tight on
   mobile. The 6-pill no-wrap row will still overflow horizontally if
   the screen is narrow enough — handled in the mobile media query below. */
.rsm-pill { display: flex; align-items: center; gap: 6px; background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: 6px; padding: 4px 10px; font-size: var(--fs-sm); white-space: nowrap; }
/* Roster "Level" column shows only for the Non-Raiders view (table gets
   .show-level there). Hidden by default so the raider table stays lean. */
#roster-table:not(.show-level) .rcol-level { display: none; }
.rsm-total .rsm-count { font-size: 20px; font-weight: 700; color: var(--c-accent); }
.rsm-total .rsm-label { color: var(--c-text-3); font-size: var(--fs-sm); }
/* Alts pill sits next to the mains total — muted so the mains count
   stays the focal number. */
.rsm-alt { background: transparent; border-style: dashed; cursor: default; }
.rsm-alt .rsm-count { color: var(--c-text-2); font-weight: 600; }
.rsm-alt .rsm-label { color: var(--c-text-faint); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .06em; }
.rsm-icon { font-size: var(--fs-md); }
.rsm-label { color: #bbc8d4; }
.rsm-count { font-weight: 700; color: #e0e8f0; margin-left: 2px; }
.rsm-attunes { display: flex; flex-direction: column; gap: 7px; }
.rsm-attune { display: flex; align-items: center; gap: 10px; }
.rsm-attune-label { font-size: var(--fs-sm); color: #bbc8d4; width: 80px; flex-shrink: 0; }
.rsm-attune-bar { flex: 1; height: 7px; background: var(--c-surface-2); border-radius: 4px; overflow: hidden; border: 1px solid var(--c-border); }
.rsm-attune-fill { height: 100%; background: var(--c-accent); border-radius: 4px; transition: width .3s; }
.rsm-attune-count { font-size: var(--fs-sm); color: var(--c-text-3); width: 36px; text-align: right; flex-shrink: 0; }

/* ── Attendance Recorder ── */
/* When a sit-outs sub-group is present, the existing 5 raid groups
   condense slightly (smaller padding) so the 6-column row still fits
   without forcing a wrap. */
.at-rec-groups { display: flex; gap: 8px; }
.at-rec-group { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: 8px; padding: 8px 10px; flex: 1; min-width: 0; }
.at-rec-group-label { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .08em; color: var(--c-text-faint); margin-bottom: 6px; font-weight: 600; }
.at-rec-players { display: flex; flex-direction: column; gap: 3px; }
.at-rec-player { display: flex; align-items: center; gap: 7px; padding: 3px 7px; border-radius: 4px; cursor: pointer; transition: background .1s; border-left: 3px solid transparent; }
.at-rec-player:hover { background: #ffffff0a; }
.at-rec-check { font-size: var(--fs-md); font-weight: 700; width: 14px; flex-shrink: 0; }
/* Let a long name ellipsize instead of pushing the trailing points / ✕ out
   past the card edge (the sit-out cards are narrow flex columns). */
.at-rec-name { font-size: var(--fs-sm); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.at-sitout-x { flex-shrink: 0; }
.at-rec-player.present { background: #1D9E7511; }
.at-rec-player.absent { background: #e8707008; }
/* Sit-outs column — same .at-rec-group container but flagged so we can
   tint the header / give the badge inside each row room to breathe. */
.at-rec-group-sitouts { border-left: 3px solid var(--c-warn); }
.at-rec-group-sitouts .at-rec-group-label { color: var(--c-warn); }
.at-rec-sitout .callout-badge {
    /* Tighter so the row doesn't stretch beyond the column width. */
    font-size: 8px;
    padding: 1px 4px;
    flex-shrink: 0;
}

.at-panel {
    background: var(--c-surface); border: 1px solid var(--c-border); border-radius: 8px;
    padding: 14px; max-height: 70vh; overflow-y: auto;
}
/* Buffs table: don't wrap descriptions — let the panel scroll horizontally */
#buffs-table { width: auto; min-width: 100%; }
#buffs-table th, #buffs-table td { white-space: nowrap; }
/* Description cell: smaller than the editable cells so it reads as meta.
   Desktop gets ~11px for readability; on narrow screens 9px fits the
   cramped columns without forcing wrap. */
#buffs-table td:nth-child(8) { font-size: var(--fs-xs); }
@media (max-width: 820px) {
    #buffs-table td:nth-child(8) { font-size: 9px; }
}
#tab-buffs .at-panel { overflow-x: auto; }
.editable { cursor: pointer; }
.editable:hover { background: #ffffff0a; }
.roster-input {
    background: var(--c-surface-2); color: #ddd; border: 1px solid var(--c-accent);
    border-radius: 3px; padding: 2px 6px; font-family: inherit;
    font-size: var(--fs-md); width: 100%; box-sizing: border-box;
}
.roster-select {
    background: var(--c-surface-2); color: #ddd; border: 1px solid #333;
    border-radius: 3px; padding: 2px 22px 2px 4px; font-family: inherit;
    font-size: var(--fs-md); width: 100%; cursor: pointer;
    transition: border-color 0.15s;
    color-scheme: dark;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%238899aa' d='M0 0l5 6 5-6z'/></svg>");
    background-repeat: no-repeat;
    background-position: right 6px center;
}
.roster-select:hover, .roster-select:focus { border-color: var(--c-accent); outline: none; }

/* ── Mobile ── */
@media (max-width: 700px) {
    .comp-grid { grid-template-columns: 1fr; }
    .coverage-groups { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .coverage-top { grid-template-columns: 1fr; }
    #missing-list { grid-template-columns: 1fr; }
    .header { flex-wrap: wrap; gap: 8px; position: relative; }
    .tabs { margin-left: 0; }
    .mobile-nav-toggle {
        display: inline-flex; align-items: center; justify-content: space-between;
        font-family: inherit; font-size: var(--fs-sm); line-height: 1.4;
        padding: 5px 10px; border-radius: 4px;
        background: transparent; border: 1px solid #dde1e7; color: #444;
        text-transform: uppercase; letter-spacing: 1px; font-weight: 600;
        cursor: pointer;
    }
    .mobile-nav-toggle:hover, .mobile-nav-toggle.open { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }
    .main-nav {
        display: none; flex-direction: column; gap: 0;
        /* Anchor to the row-1 white pill (logo + toggle + auth) instead
           of `top: 100%` of .header — the mobile header is a multi-row
           grid (tabs, config controls, view-as picker), so 100% would
           drop the dropdown several hundred pixels below the toggle. */
        position: absolute; top: 64px; right: 0;
        background: #ffffff; border: 1px solid #dde1e7; border-radius: 8px;
        min-width: 200px; box-shadow: 0 4px 20px #0006; z-index: 100; overflow: hidden;
        padding: 4px;
    }
    .main-nav.open { display: flex; }
    .main-nav .main-nav-btn { width: 100%; text-align: left; border-radius: 4px; }
    /* .ha-user is already column-stacked at desktop now, so the mobile
       override that used to flip column-on-small / row-on-wide is gone — the
       layout is consistent at every width. */

    /* Raid Builder subnav on mobile — three-row stack:
         row 1: Group Editor / Buff Coverage tabs
         row 2: config dropdown (full width, wide enough for long raid names)
         row 3: +New / Edit / Delete / Copy URL write buttons */
    .subnav #config-controls { flex-wrap: wrap; flex: 1 1 100%; min-width: 0; gap: 8px; }
    .subnav #config-controls > #config-select { flex: 1 1 100%; min-width: 0; max-width: none; width: 100%; }
    .subnav #config-controls > div[style*="width:1px"] { display: none; }

    /* Raid-builder on mobile: hoist role-counts and SR bar out of the toolbar
       (via display:contents on .buff-vis-bar) so they can slot as full-width
       rows. Order top→bottom:
         1. Softres info (right under subnav)
         2. Toolbar (Raid 10/25 + Show buffs)
         3. Buff coverage cards (when "Show" is on — sit up top above groups)
         4. Tanks/heals/DPS summary (directly above groups)
         5. Group grid
       The flex container is .tab-content-wrap (already flex-column at every
       width), so order: rules apply to .tab-content-wrap's children. */
    #tab-comp.active .tab-content-wrap > .buff-vis-bar { display: contents; }
    #tab-comp.active #comp-sr-bar {
        order: 1;
        justify-content: center; flex-wrap: wrap; row-gap: 6px;
        /* 18 → 14 (~20% trim) per user — tighten the gap above the
           Raid 10/25 + Show buffs toolbar. */
        margin: 0 0 14px;
    }
    /* Token sits on its own line below the URL + buttons (and only renders
       for GM/officers — the row simply isn't here for guests/raiders). */
    #tab-comp.active #comp-sr-bar .comp-sr-token {
        flex-basis: 100%; justify-content: center;
    }
    #tab-comp.active .bvb-toolbar {
        display: flex; flex-wrap: wrap; align-items: center;
        gap: 10px; row-gap: 6px;
        order: 2; justify-content: center;
        /* Bumped from 22px → 30px per user — more padding below the
           Raid 10/25 + Show buffs toolbar so it reads as distinct
           from the role-counts row that follows. */
        margin: 0 0 30px;
    }
    #tab-comp.active .tab-content-wrap > .coverage-top {
        order: 3; margin: 0 0 28px;
    }
    #tab-comp.active #comp-role-counts {
        order: 4; justify-content: center;
        margin: 0 0 4px;
        /* On phones, wrap the four role pills (Tanks / Healers / Melee /
           Ranged) into two rows of two — line 1 = Tanks/Healers,
           line 2 = Melee/Ranged. The display:flex !important overrides
           the inline display:inline-flex from the partial so flex-wrap
           and the max-width constraint actually take effect; without the
           override the inline-flex container hugs its content width and
           pills stay on one line until the viewport gets very narrow. */
        display: flex !important;
        flex-wrap: wrap;
        max-width: 240px;
        margin-left: auto !important;
        margin-right: auto !important;
        column-gap: 16px;
        row-gap: 4px;
    }
    #tab-comp.active .tab-content-wrap > .comp-grid { order: 5; margin: 0; }

    /* Buff Coverage tab: center the toolbar and legend on mobile. DOM order
       already puts the missing-buffs banner above the group grid so no flex
       reordering is needed. */

/* Leaderboard toolbar: put Reset All Attendance above the search box
       when the header wraps — destructive action shouldn't hang off the
       bottom of the page where it's easy to miss. Search keeps sharing
       the row with the Active/Alumni toggle (flex: 1 with min-width: 0
       so it can shrink) instead of dropping to its own line. */
    .at-leaderboard-reset { order: 2; margin-left: 0 !important; flex: 1 1 100%; }
    .at-leaderboard-search { order: 1; flex: 1 1 120px; min-width: 0; width: auto !important; }
}

.footer {
    margin-top: 0;
    /* 24px above the footer text gives a consistent gap from the last
       content section across every tab. Wrapped tabs (.tab-content-wrap,
       .loot-tab-wrap) explicitly drop their bottom padding to 0 so the
       gap is driven entirely from here — same value applies to non-
       wrapped tabs (Buffs, Coverage) too. */
    padding: 24px 0 25px;
    font-size: var(--fs-xs);
    color: #444;
    text-align: center;
    /* Flex column with uniform 12px gap drives spacing between the
       three footer "lines" (footer-meta line 1, footer-meta line 2,
       presence strip). The footer-meta's own grid row-gap is also
       12px, so all three gaps come out to the same 12px. */
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
}
/* Footer meta: three logical groups (version, credits, links) laid out
   in a CSS grid so the desktop and mobile breakpoints can shuffle their
   ordering without restructuring the DOM:
     ≥521px (desktop): row 1 = version · credits, row 2 = links
     ≤520px (mobile):  row 1 = version, row 2 = links, row 3 = credits
   Grid-area names drive both layouts; the inter-row gap handles spacing
   so children don't need their own margin-top.
   `.footer-sep-after-version` is the inline · between version + credits
   on desktop; hidden on mobile since the grid stacks them. */
.footer-meta {
    /* margin-top removed — .footer (flex column) now drives all
       inter-line spacing via its 12px gap. */
    display: grid;
    grid-template-columns: auto auto auto;
    grid-template-areas:
        "version sep credits"
        "links   links links";
    justify-content: center;
    align-items: center;
    /* 12px row-gap between line 1 (version · credits) and line 2 (links).
       Matches the .footer's flex gap so all three footer lines are
       evenly spaced. */
    gap: 12px 0;
}
.footer-version { grid-area: version; }
.footer-sep-after-version { grid-area: sep; }
.footer-credits { grid-area: credits; }
.footer-links { grid-area: links; justify-self: center; }
@media (max-width: 520px) {
    .footer-meta {
        grid-template-columns: 1fr;
        grid-template-areas:
            "version"
            "links"
            "credits";
    }
    .footer-sep-after-version { display: none; }
}
/* Presence strip — "who's online right now" row above the footer text.
   Each card is a small class-colored chip with avatar + name. Click a
   card to open that player's profile page. The whole strip lives inside
   .footer so it shares the same bottom-of-page anchor every tab pads
   to. Empty state ("No one online right now") renders when nobody's
   pinged in the last 15 minutes. */
.presence-strip {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 0 12px;
    /* margin-top removed — .footer (flex column) now drives all
       inter-line spacing via its 12px gap, matching the gap between
       footer-meta line 1 and line 2. */
    min-height: 24px;
}
.presence-label {
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-faint);
    margin-right: 4px;
    align-self: center;
}
.presence-empty {
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-faint);
    opacity: 0.55;
}
.presence-card {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 2px 7px 2px 2px;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 999px;
    cursor: pointer;
    transition: background 0.1s, border-color 0.1s, transform 0.05s;
    font-family: inherit;
}
.presence-card:hover {
    background: var(--c-surface);
    transform: translateY(-1px);
}
/* Guests pill — same chrome as player chips so it visually fits the
   row, just with a 👤-icon avatar in place of a real one and the
   "Guests · N" label in normal-case (no SHOUTING uppercase). Not
   clickable; default cursor + no hover-lift since there's no profile
   to open. */
.presence-card-guests {
    cursor: default;
}
.presence-card-guests:hover {
    transform: none;
}
/* Dim state for the always-rendered Guests pill when count is 0.
   Officers can still see the tracker is alive without it shouting
   for attention during quiet windows. */
.presence-card-guests.is-dim {
    opacity: 0.45;
}
.presence-card-guests.is-dim:hover {
    opacity: 0.7;
}
.presence-guest-avatar {
    background: var(--c-surface);
    color: var(--c-text-2);
    border: 1px solid #1d2839;
    overflow: visible;
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
    line-height: 1;
}
.presence-guest-avatar::after {
    /* Same green dot as the player chips — guests are also "online." */
    content: '';
    position: absolute;
    right: -1px;
    bottom: -1px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #4caf50;
    border: 2px solid var(--c-surface-2);
    box-sizing: border-box;
}
.presence-name-guests {
    font-weight: 600;
    color: var(--c-text-2);
}
/* Highlight the signed-in user's own chip with a subtle accent ring so
   they spot themselves immediately. The chip is always sorted to the
   front of the row by _renderPresenceList, but the visual cue helps. */
.presence-card.presence-card-me {
    box-shadow: 0 0 0 1px var(--c-accent) inset;
}
.presence-card .avatar-circle.presence-avatar {
    border: 1px solid #1d2839;
    /* Override the default avatar-circle's overflow:hidden so the green
       dot can spill out of the bottom-right corner. position:relative
       anchors the dot's absolute positioning. */
    overflow: visible;
    position: relative;
}
/* Tiny green dot in the lower-right corner of each avatar to reinforce
   "online now" — same convention Discord / Slack / etc. use. */
.presence-card .avatar-circle.presence-avatar::after {
    content: '';
    position: absolute;
    right: -1px;
    bottom: -1px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #4caf50;
    border: 2px solid var(--c-surface-2);
    box-sizing: border-box;
}
.presence-card:hover .avatar-circle.presence-avatar::after {
    border-color: var(--c-surface);
}
/* The avatar's <img> child uses border-radius:0 by default — round it
   so the image follows the parent's circular shape now that the parent
   no longer clips with overflow:hidden. */
.presence-card .avatar-circle.presence-avatar img {
    border-radius: 50%;
}
.presence-name {
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 0.2px;
    white-space: nowrap;
}

/* ── Progression (Home) ──────────────────────────────────────── */
#progression-root {
    display: flex;
    flex-direction: column;
    gap: 18px;
}
.prog-raid-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
}
.prog-raid { flex: 1 1 auto; min-width: 0; }
/* Section divider between phase-status groups (Active / Upcoming / Completed).
   flex-basis:100% forces a full-width line break so each group starts a fresh
   row of cards, with a centered uppercase label on a rule. */
.prog-group-divider {
    flex: 0 0 100%;
    display: flex; align-items: center; gap: 12px;
    margin: 6px 2px 0;
    font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.10em;
    text-transform: uppercase; color: var(--c-text-faint);
}
.prog-group-divider::before,
.prog-group-divider::after { content: ""; flex: 1 1 auto; height: 1px; background: var(--c-border); }
.prog-group-divider.prog-group-active { color: var(--c-accent); }
.prog-group-divider.prog-group-active::before,
.prog-group-divider.prog-group-active::after { background: rgba(51, 228, 222, 0.35); }
.prog-overall {
    /* Background defaults to plain surface; if there's a custom raid bg
       (uploaded via the hover edit button) the renderer overrides via
       inline `background:` with the same dual-layer pattern (dark
       gradient overlay + image + surface fallback). */
    background: var(--c-surface);
    border: 1px solid #223049;
    border-radius: 8px;
    /* Bumped padding (was 14/16/16/16) to give the card more vertical
       breathing room — combined with the lighter overlay in
       _raidBgStyle this lets significantly more of the raid bg image
       show through behind the title + bars. */
    padding: 24px 20px 20px 20px;
    margin-bottom: 8px;
    position: relative;
}
.prog-raid-grid { margin-bottom: 8px; }
#progression-root > *:last-child { margin-bottom: 0; }
.prog-overall-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    margin-bottom: 8px;
}
.prog-overall-title { font-size: 15px; font-weight: 600; color: var(--c-text); letter-spacing: 0.02em; }
.prog-overall-count { font-size: var(--fs-sm); color: var(--c-text-3); letter-spacing: 0.03em; }
.prog-overall-bar {
    height: 8px;
    background: var(--c-border);
    border-radius: 3px;
    overflow: hidden;
    margin-bottom: 14px;
}
.prog-overall-fill {
    height: 100%;
    background: linear-gradient(90deg, var(--c-link), #9db0ee);
    transition: width 0.25s ease;
}
.prog-phase-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
    gap: 10px;
}
.prog-phase {
    padding: 8px 10px;
    border: 1px solid transparent;
    border-radius: 6px;
    transition: border-color .15s, background .15s, opacity .15s;
}
/* Phase status, derived from the leader-set active phase. The active phase is
   featured (accent border + glow); earlier phases read completed, later ones
   dim as upcoming. */
.prog-phase-foot {
    display: flex; align-items: center; justify-content: flex-end;
    margin-top: 6px; min-height: 20px;
}
/* Right-aligned status label. For leaders, hovering it swaps the label for the
   Set active / Clear button (overlaid in place, so the layout doesn't shift). */
.prog-phase-status { position: relative; display: inline-flex; align-items: center; }
.prog-phase-setactive { display: none; }
/* Only swap the label for the button on hover when a Set active/Clear button is
   actually present (leaders). For non-leaders there's no button, so the label
   must NOT hide on hover. */
.prog-phase-status.has-btn:hover .prog-phase-badge { visibility: hidden; }
.prog-phase-status.has-btn:hover .prog-phase-setactive {
    display: inline-block; position: absolute; right: 0; top: 50%;
    transform: translateY(-50%); white-space: nowrap; color: var(--c-text);
}
/* No active phase set yet → no label to hover, so reveal Set active on
   phase-card hover (button sits inline since there's nothing to overlay). */
.prog-phase-status.is-empty .prog-phase-setactive { position: static; transform: none; }
.prog-phase:hover .prog-phase-status.is-empty .prog-phase-setactive { display: inline-block; }
/* Status labels are text-only and all share the VM green for a clean,
   consistent read; the card-level treatment (green border on active, dimmed
   upcoming) carries the visual hierarchy. */
.prog-phase-badge { font-size: var(--fs-micro); font-weight: 700; letter-spacing: 0.06em; }
.prog-badge-active { color: var(--c-accent); }
.prog-badge-completed,
.prog-badge-upcoming { color: var(--c-text-faint); }
.prog-phase-setactive {
    background: none; border: 1px solid var(--c-border); color: var(--c-text-3);
    font-size: var(--fs-micro); font-weight: 600; letter-spacing: 0.04em;
    padding: 2px 8px; border-radius: 4px; cursor: pointer; white-space: nowrap;
}
.prog-phase-setactive:hover { color: var(--c-text); border-color: var(--c-text-3); }
.prog-phase-setactive.is-clear { color: var(--c-text-faint); }
.prog-phase-top {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    margin-bottom: 4px;
}
.prog-phase-name { font-size: var(--fs-micro); font-weight: 700; letter-spacing: 0.05em; color: #7a8aa0; }
.prog-phase-count { font-size: var(--fs-micro); color: #aab4c2; }
.prog-phase-bar {
    height: 8px;
    background: var(--c-border);
    border-radius: 3px;
    overflow: hidden;
}
.prog-phase-fill {
    height: 100%;
    /* Phase bars use VM green (--c-accent); the overall bar above uses purple
       (--c-link) so the top-level TBC % reads as a different tier from the
       per-phase breakdown. */
    background: var(--c-accent);
    transition: width 0.25s ease;
}

/* Recruiting */
/* Apply / Applications buttons pinned top-right of the About card, same size. */
.about-cta { position: absolute; top: 16px; right: 18px; display: flex; gap: 8px; z-index: 3; }
@media (max-width: 560px) { .about-cta { position: static; justify-content: flex-end; margin-bottom: 10px; } }
/* Filled teal variant of the unified .btn-teal (for the primary Apply CTA). */
.btn-teal.btn-teal-fill { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }
.btn-teal.btn-teal-fill:hover { background: color-mix(in srgb, var(--c-accent) 82%, #000); border-color: color-mix(in srgb, var(--c-accent) 82%, #000); color: var(--c-bg); }
/* Discord "chat first" note under the apply-modal submit button. */
.japp-discord-note { font-size: var(--fs-sm); color: var(--c-text-3); margin: 10px 0 0; }
/* Raid-experience checkbox groups (Retail / Classic) in the apply modal. */
.join-apply-expgroups { display: flex; flex-wrap: wrap; gap: 12px 24px; margin: 2px 0 4px; }
.japp-expgroup { min-width: 150px; }
.japp-expgroup-head { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--c-text-faint); font-weight: 700; margin-bottom: 4px; }
.japp-expgroup .join-apply-chks { flex-direction: column; gap: 4px; margin: 0; }
.japp-tbc-chks { gap: 6px 16px; }
.japp-more-exp { margin: -2px 0 2px; }
.japp-more-exp > summary { cursor: pointer; font-size: var(--fs-xs); color: var(--c-text-faint); font-weight: 600; padding: 2px 0; user-select: none; }
.japp-more-exp[open] > summary { margin-bottom: 8px; }
/* The two Join modals (apply form + officer review). */
.japp-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: flex-start; justify-content: center; z-index: 2000; padding: 5vh 16px; overflow-y: auto; }
.japp-modal { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-md); box-shadow: 0 12px 48px rgba(0,0,0,.55); width: 100%; max-width: 520px; margin: auto; }
.japp-modal.japp-modal-wide { max-width: 640px; }
.japp-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 14px 16px; border-bottom: 1px solid var(--c-border); }
.japp-modal-head h3 { margin: 0; font-size: var(--fs-md); }
.japp-modal-x { background: none; border: none; color: var(--c-text-faint); font-size: 22px; line-height: 1; cursor: pointer; padding: 0 4px; }
.japp-modal-x:hover { color: var(--c-text); }
.japp-modal-body { padding: 14px 16px 16px; max-height: 78vh; overflow-y: auto; }
.japp-lede { font-size: var(--fs-sm); color: var(--c-text-3); margin: 0 0 12px; line-height: 1.5; }
.japp-sec-head { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .05em; color: var(--c-text-2); font-weight: 700; margin: 0 0 4px; }
.japp-review-tools { display: flex; justify-content: flex-end; margin-bottom: 8px; }
.japp-chanbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid var(--c-border); font-size: var(--fs-sm); color: var(--c-text-2); }
.japp-chanbar > span { color: var(--c-text-faint); }
.japp-chanbar select { max-width: 240px; }
/* "What we look for" expectations block at the top of the apply modal. */
.japp-expect { background: color-mix(in srgb, var(--c-accent) 6%, var(--c-bg)); border: 1px solid var(--c-border); border-radius: var(--r-md); padding: 10px 14px; margin: 0 0 14px; }
.japp-expect-head { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .05em; color: var(--c-accent); font-weight: 700; margin-bottom: 6px; }
.japp-expect-list { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 5px; }
.japp-expect-list li { font-size: var(--fs-sm); color: var(--c-text-2); line-height: 1.45; }
.japp-expect-list li strong { color: var(--c-text); }
.japp-expect-list li em { color: var(--c-text-faint); font-style: normal; }
/* Join page sub-nav — shows one content section at a time to keep the page
   short. Mirrors the roster/loot sub-nav look. */
.join-subnav { display: flex; flex-wrap: wrap; gap: 6px; margin: 14px 0 12px; }
.join-section { display: none; }
.join-section.active { display: block; }
/* "How ranks work" card on the Join page. */
.join-ranks-intro { line-height: 1.55; }
.rank-ladder { display: flex; flex-direction: column; gap: 8px; margin: 14px 0 0; }
.rank-tier { border: 1px solid var(--c-border); border-left: 3px solid var(--c-border); border-radius: var(--r-md); padding: 9px 13px; background: rgba(255, 255, 255, 0.02); }
.rank-tier-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 3px; }
.rank-badge { font-weight: 700; font-size: var(--fs-base); }
.rank-tagline { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .06em; color: var(--c-text-faint); }
.rank-tier-desc { margin: 0; font-size: var(--fs-sm); color: var(--c-text-2); line-height: 1.5; }
.rank-tier-core { border-left-color: var(--c-accent); }
.rank-tier-core .rank-badge { color: var(--c-accent); }
.rank-tier-vanguard { border-left-color: var(--c-special); }
.rank-tier-vanguard .rank-badge { color: var(--c-special); }
.rank-tier-initiate { border-left-color: var(--c-warn); }
.rank-tier-initiate .rank-badge { color: var(--c-warn); }
.rank-eval { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--c-border); }
.rank-eval-title { font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: .05em; color: #aab4c2; font-weight: 700; margin-bottom: 8px; }
.rank-eval > p { font-size: var(--fs-sm); color: var(--c-text-2); line-height: 1.5; margin: 0 0 8px; }
.rank-eval .rank-eval-note { color: var(--c-text-3); font-style: italic; margin: 8px 0 0; }
/* "What we look for" folded into the Recruiting card on the Join page. */
.recruit-lookfor { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--c-border); }
.recruit-lookfor-title { font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: .05em; color: #aab4c2; font-weight: 700; margin-bottom: 8px; }
/* On the Join page, the card section headings pop in VM teal (the "What we
   look for" subheading stays gray so it reads as a subheading, not a title). */
#join-root .recruit-title { color: var(--c-accent); }
/* Recruit application form (rendered inside the apply modal). */
.join-apply-form { display: flex; flex-direction: column; gap: 12px; margin-top: 8px; }
.join-apply-row { display: flex; gap: 12px; flex-wrap: wrap; }
.join-apply-field { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 160px; }
.join-apply-field.japp-narrow { flex: 0 1 160px; }
/* Class / Main spec / Off spec share one line — narrow enough to fit three-up. */
.japp-spec-row .join-apply-field { min-width: 96px; }
.join-apply-field > span { font-size: var(--fs-xs); color: var(--c-text-2); font-weight: 600; }
.join-apply-field > span em { color: var(--c-text-faint); font-weight: 400; font-style: normal; }
/* Red asterisk marking required apply fields. */
.japp-req { color: var(--c-bad); font-weight: 700; }
.join-apply-field textarea.admin-input { resize: vertical; min-height: 40px; width: 100%; box-sizing: border-box; }
.join-apply-chks { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 2px 0 4px; }
.japp-chk { display: inline-flex; align-items: center; gap: 5px; font-size: var(--fs-sm); color: var(--c-text-2); cursor: pointer; }
.japp-chk input { accent-color: var(--c-accent); }
/* "Select all" toggle above the TBC raid grid. */
.japp-chk.japp-tbc-all { margin: 2px 0 4px; color: var(--c-text-faint); }
.japp-chk.japp-tbc-all strong { color: var(--c-text-2); }
/* "New to TBC raiding" escape hatch below the grid (mutually exclusive). */
.japp-chk.japp-tbc-none-lbl { margin-top: 4px; }
.japp-chk.japp-tbc-none-lbl em { color: var(--c-text-faint); font-style: normal; }
.join-apply-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 2px; }
.join-apply-status { font-size: var(--fs-sm); }
/* Officer review list */
.join-apps-card { margin-bottom: var(--sp-3); }
.japp-item { border-top: 1px solid var(--c-border); padding: 4px 0; }
.japp-item:first-of-type { border-top: none; }
.japp-item.japp-dismissed { opacity: 0.6; }
.japp-item-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; cursor: pointer; padding: 6px 8px; border-radius: var(--r-sm); user-select: none; }
.japp-item-head:hover { background: var(--c-surface-2); }
.japp-item.japp-expanded > .japp-item-head { background: var(--c-surface-2); }
.japp-caret { color: var(--c-text-faint); font-size: var(--fs-xs); flex: 0 0 auto; align-self: center; }
.japp-item-body { padding: 6px 8px 4px 22px; }
.japp-name { font-weight: 700; }
.japp-sub { font-size: var(--fs-xs); color: var(--c-text-faint); }
.japp-when { font-size: var(--fs-xs); color: var(--c-text-faint); margin-left: auto; }
.japp-actions { display: inline-flex; gap: 4px; }
.japp-meta { font-size: var(--fs-sm); color: var(--c-text-2); margin-top: 3px; }
.japp-meta > span { color: var(--c-text-faint); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .04em; margin-right: 4px; }
/* Auto-built Armory / Warcraft Logs profile links on an application. */
.japp-lookup a { font-weight: 600; }
.japp-lookup .japp-lookup-sep { text-transform: none; letter-spacing: 0; margin: 0 2px; }
.japp-dismissed-wrap { margin-top: 8px; }
.japp-dismissed-wrap > summary { cursor: pointer; font-size: var(--fs-xs); color: var(--c-text-faint); }
.recruit-card {
    background: var(--c-surface);
    border: 1px solid #223049;
    border-radius: 8px;
    padding: 14px 16px 16px 16px;
    margin-bottom: 8px;
}

/* ──────── Recruitment Scout (admin-card on the Join page) ────────
   Blizzard-API-backed scouting tool for finding guildless candidates.
   Lives inside an .admin-card (which provides border / radius /
   padding / surface bg). Outer margin via .scout-card-frame because
   admin-card has no outer margin of its own (admin-grid handles
   spacing on the Admin tab; here on Join there's no grid wrapper).
   Mirrors .recruit-card's spacing: only a bottom margin of 8px, no
   top margin — the card above (Recruiting) provides the gap via
   its own margin-bottom: 8px. Earlier this had margin-top: 12px
   which compounded with Recruiting's 8px to feel too airy. */
.scout-card-frame {
    margin-bottom: var(--sp-2);
}
.scout-body {
    margin-top: var(--sp-2);
}
.scout-spec-missing {
    color: var(--c-text-faint);
    font-style: italic;
}
/* Off-spec — fainter so the primary stays visually dominant; for
   shamans with dual-spec this reads as "Restoration / Enhancement"
   with the slash + off-spec name dimmed. */
.scout-offspec {
    color: var(--c-text-faint);
    font-size: var(--fs-xs);
}
/* Source-guild rank tag — appears next to the Guild cell when the
   candidate was imported via 'Scout guild' AND their current guild
   matches the source. Reads as "Loyal Order rank 2" → tells the
   officer that THIS player is the GM/officer of the rival guild,
   useful for prioritizing pitches. */
.scout-guild-rank {
    color: var(--c-text-faint);
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    margin-left: 4px;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 3px;
    padding: 1px 5px;
}
/* Guild scout form — sits under the names paste box. Inline row of
   guild slug + 'on' + realm slug + Scout button. */
.scout-input-row-compact {
    margin-top: var(--sp-3);
    padding-top: var(--sp-3);
    border-top: 1px dashed var(--c-border);
}
.scout-guild-form {
    display: flex; align-items: center; gap: var(--sp-2);
    flex-wrap: wrap;
}
.scout-guild-input {
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 4px;
    padding: 4px 8px;
    color: var(--c-text);
    font-family: inherit;
    font-size: var(--fs-sm);
    min-width: 180px;
}
.scout-guild-input:focus { outline: none; border-color: var(--c-accent); }
.scout-guild-sep {
    color: var(--c-text-faint);
    font-size: var(--fs-sm);
}
.scout-guild-progress {
    margin-top: var(--sp-2);
    font-size: var(--fs-xs);
}
.scout-guild-progress-msg { color: var(--c-text-2); }
/* Notes cell — single-button affordance. The cell never shows note
   text directly (notes can be long, sensitive recruit info, or just
   clutter the row). Click opens the modal where the full note
   lives. Filled state ('View note') shows a teal-bordered pill so
   officers see at a glance which candidates have notes; empty
   state ('+ Add note') is faint so it doesn't shout. Hover title=
   on the filled button quick-previews the note text. */
.scout-notes-cell {
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--r-sm);
    padding: 3px 10px;
    color: var(--c-text-2);
    font-family: inherit;
    font-size: var(--fs-xs);
    text-align: center;
    cursor: pointer;
    line-height: 1.3;
    white-space: nowrap;
    transition: background 120ms, border-color 120ms;
}
.scout-notes-cell:hover {
    border-color: var(--c-accent);
    background: var(--c-surface-2);
}
.scout-notes-empty {
    color: var(--c-text-faint);
    font-style: italic;
    border-color: var(--c-border);
    border-style: dashed;
}
.scout-notes-filled {
    color: var(--c-accent);
    background: var(--c-surface-2);
    border-color: #33E4DE66;
    font-weight: 600;
}
/* Notes modal — opens when officer clicks a notes cell or '+ note'.
   Uses the shared #scout-notes-modal backdrop (added to the modal id
   list above) so it gets the same fixed-overlay + flex-center +
   click-outside-to-close treatment as the rest of the app's modals.
   Inner box is .scout-notes-box (a sibling of .cfg-box / .pug-box). */
.scout-notes-box {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    padding: var(--sp-4);
    width: 520px;
    max-width: 100%;
    box-shadow: 0 12px 48px #0008;
}
.scout-notes-title {
    font-size: var(--fs-md);
    margin: 0 0 var(--sp-1);
    color: var(--c-text);
    font-weight: 700;
    letter-spacing: 0;
    text-transform: none;
}
.scout-notes-name {
    color: var(--c-accent);
}
.scout-notes-sub {
    color: var(--c-text-3);
    font-size: var(--fs-sm);
    margin: 0 0 var(--sp-3);
    line-height: 1.5;
}
.scout-notes-textarea {
    width: 100%;
    min-height: 140px;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: var(--r-sm);
    padding: var(--sp-3);
    color: var(--c-text);
    font-family: inherit;
    font-size: var(--fs-sm);
    resize: vertical;
    box-sizing: border-box;
}
.scout-notes-textarea:focus { outline: none; border-color: var(--c-accent); }
.scout-notes-meta {
    display: flex;
    justify-content: flex-end;
    margin-top: 4px;
    font-size: var(--fs-xs);
    color: var(--c-text-faint);
}
.scout-notes-counter { font-variant-numeric: tabular-nums; }
.scout-notes-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--sp-2);
    margin-top: var(--sp-3);
}
/* Mode toggle — segmented Search players / Search guild switch. Sits
   in the Scout header next to the meta line. Pads down on mobile so
   the meta wraps below it cleanly. */
.scout-mode-toggle { flex-shrink: 0; }
/* Download chip for the Vis Maior Scout WoW addon — links to the
   pre-zipped /addons/VisMaiorScout.zip on the server. Sits in the
   scout header next to the meta line. Faint pill so it doesn't
   compete with the action buttons; hover-darkens to make the
   click target obvious. */
/* Wrapper that stacks the three addon chips vertically and pushes
   the whole stack to the right end of .scout-header. Prior layout
   was three sibling chips spread along the row — user wanted them
   stacked on three lines, right-aligned. */
.scout-addon-actions {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 4px;
    margin-left: auto;
    flex-shrink: 0;
}
.scout-addon-link {
    font-size: var(--fs-xs);
    padding: 3px 10px;
    color: var(--c-accent);
    background: var(--c-surface-2);
    border: 1px solid #33E4DE40;
    border-radius: var(--r-sm);
    text-decoration: none;
    white-space: nowrap;
    font-weight: 600;
}
.scout-addon-link:hover {
    background: var(--c-accent);
    color: var(--c-bg);
    border-color: var(--c-accent);
}
/* Source-guild dropdown — replaces the older per-guild chip row.
   Scales to many scouted guilds (the chip row would wrap into a
   wall of buttons once you've scouted 5+ guilds). Single-select:
   "All sources" or one specific guild at a time. Same look as the
   admin-page selects so it slots in visually. */
.scout-source-select {
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 4px;
    padding: 3px 26px 3px 10px;
    color: var(--c-text-2);
    font-family: inherit;
    font-size: var(--fs-xs);
    line-height: 1.4;
    min-width: 200px;
    cursor: pointer;
    -webkit-appearance: none;
    -moz-appearance: none;
    appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%238899aa' d='M0 0l5 6 5-6z'/></svg>");
    background-repeat: no-repeat;
    background-position: right 8px center;
}
.scout-source-select:focus { outline: none; border-color: var(--c-accent); }
.scout-source-select:hover { border-color: var(--c-accent); }
/* Auto-refresh badge — appears in the Roster subnav (right-aligned
   slot) during the background Vis Maior refresh fired by
   _scoutMaybeAutoRefresh, AND during a synchronous addon import.
   Faint so it doesn't yank attention from whatever the officer is
   actually doing while the refresh runs in parallel. Plain text —
   the previous pill chrome (bg + border + padding) read as a
   permanent UI element rather than a transient status; user asked
   to drop it. */
.scout-autorefresh-badge {
    color: var(--c-accent);
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    font-weight: 700;
}

/* Right-aligned status slot on the Roster subnav. Holds either the
   auto-refresh badge or the addon-import progress bar — both are
   transient and the user shouldn't have to leave the page or stare
   at a hung modal to know something's happening. */
.roster-subnav-status {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    font-size: var(--fs-xs);
    color: var(--c-text-3);
    min-height: 22px;
}
/* ── Roster Tree ── */
.rtree-legend { font-size: var(--fs-sm); color: var(--c-text-faint); margin-bottom: 14px; }
/* List view — three tiered columns (leadership / raiders / recruits). */
.rtree-cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px 24px; align-items: start; }
.rtree-col { min-width: 0; }
@media (max-width: 760px) { .rtree-cols { grid-template-columns: 1fr; } }
.rtree-rank { margin-bottom: 18px; }
.rtree-rank-head {
    font-size: var(--fs-sm); font-weight: 700; color: var(--c-text);
    text-transform: uppercase; letter-spacing: .05em;
    border-bottom: 1px solid var(--c-border); padding-bottom: 4px; margin-bottom: 8px;
}
.rtree-rank-count { color: var(--c-text-faint); font-weight: 400; text-transform: none; letter-spacing: 0; }
.rtree-group { margin-bottom: 6px; }
/* Very subtle hairline between one main (and its alts) and the next. */
.rtree-col .rtree-group + .rtree-group { border-top: 1px solid color-mix(in srgb, var(--c-border) 55%, transparent); padding-top: 6px; }
/* Points as "current / lifetime", pushed to the right edge of the row. */
.rtree-node .points-link.rtree-pts { margin-left: auto; padding-left: 8px; font-size: var(--fs-xs); color: var(--c-text-faint); font-variant-numeric: tabular-nums; white-space: nowrap; }
.rtree-pts-sep { opacity: .45; }
.rtree-node { display: flex; align-items: center; gap: 8px; padding: 3px 0; font-size: var(--fs-sm); flex-wrap: wrap; }
.rtree-main { font-weight: 600; }
.rtree-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; display: inline-block; }
.rtree-alts { margin-left: 16px; border-left: 1px solid var(--c-border); padding-left: 10px; }
.rtree-alt { color: var(--c-text-2); }
.rtree-branch { color: var(--c-text-faint); }
.rtree-meta { color: var(--c-text-faint); font-size: var(--fs-xs); }
.rtree-alt-tag {
    font-size: 10px; color: var(--c-text-faint); border: 1px solid var(--c-border);
    border-radius: 3px; padding: 0 4px; text-transform: uppercase; letter-spacing: .03em;
}
.rtree-alt-count {
    font-size: 10px; color: var(--c-accent); border: 1px solid color-mix(in srgb, var(--c-accent) 40%, transparent);
    border-radius: 999px; padding: 1px 7px;
}
.rtree-node .player-link { font-size: var(--fs-sm); }
.rtree-viewtoggle { margin-bottom: 12px; }
/* Map view — SVG node-graph with HTML nodes layered on top. */
.rtree-map-scroll { overflow: auto; max-width: 100%; border: 1px solid var(--c-border); border-radius: var(--r-md); background: var(--c-surface); padding: 4px; }
.rtree-map-wrap { position: relative; width: 100%; }
.rtree-map-svg { position: absolute; inset: 0; pointer-events: none; overflow: visible; }
.rtree-map-link { fill: none; stroke: var(--c-border); stroke-width: 1.5; }
/* Full-width dotted rule between rank groups (spans the panel, not just the
   tree). Uses the original SVG dash rhythm — 2px dash, 5px gap — via a
   repeating gradient rather than CSS `dotted` (which renders chunkier). */
.rtree-map-sep {
    position: absolute; left: 0; right: 0; height: 1px;
    background: repeating-linear-gradient(to right, var(--c-text-faint), var(--c-text-faint) 2px, transparent 2px, transparent 7px);
    opacity: .35; pointer-events: none;
}
.rtree-map-node {
    position: absolute; box-sizing: border-box; transform: translateY(-50%);
    background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: 6px;
    padding: 4px 8px; font-size: var(--fs-sm); overflow: hidden; text-align: left;
}
.rtree-map-guild {
    background: color-mix(in srgb, var(--c-accent) 16%, transparent);
    border-color: color-mix(in srgb, var(--c-accent) 45%, transparent);
    color: var(--c-accent); font-weight: 800; text-align: center;
}
.rtree-map-rank {
    font-weight: 700; text-transform: uppercase; letter-spacing: .03em; font-size: var(--fs-xs);
    display: flex; align-items: center; justify-content: space-between; gap: 6px;
}
.rtree-map-rankcount { color: var(--c-text-faint); font-weight: 400; }
.rtree-map-main, .rtree-map-alt { border-left-width: 3px; display: flex; align-items: center; gap: 6px; }
.rtree-map-info { min-width: 0; flex: 1; }
.rtree-map-pts { flex-shrink: 0; font-weight: 700; color: var(--c-accent); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.rtree-map-alt { background: transparent; }
.rtree-map-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rtree-map-name .player-link { font-size: var(--fs-sm); max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
.rtree-map-meta { font-size: 10px; color: var(--c-text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.roster-subnav-status .scout-import-progress {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
}
/* Indeterminate progress bar — used during addon import when the
   server endpoint isn't streaming progress, so we can only tell the
   user "yes, work is happening". A 120px-wide track with a moving
   60px chip animating left↔right via @keyframes. */
.scout-import-progress-bar {
    position: relative;
    width: 120px;
    height: 4px;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 2px;
    overflow: hidden;
}
.scout-import-progress-fill {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 60px;
    background: var(--c-accent);
    border-radius: 2px;
    animation: scout-import-progress-slide 1.4s ease-in-out infinite;
}
@keyframes scout-import-progress-slide {
    0%   { left: -60px; }
    50%  { left: 120px; }
    100% { left: -60px; }
}
@media (max-width: 600px) {
    .scout-header { gap: var(--sp-2); }
    .scout-mode-toggle { width: 100%; }
    .scout-mode-toggle .seg-btn { flex: 1 1 50%; }
}
.scout-header {
    display: flex; align-items: baseline; gap: var(--sp-3);
    flex-wrap: wrap;
    margin-bottom: var(--sp-3);
}
.scout-meta {
    color: var(--c-text-faint);
    font-size: var(--fs-xs);
}
.scout-badge {
    font-size: var(--fs-micro);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    padding: 2px 8px;
    border-radius: 3px;
}
.scout-badge-warn {
    background: #4d3919;
    color: #ffce5d;
    border: 1px solid #6b5320;
}
.scout-loading, .scout-empty {
    color: var(--c-text-faint);
    font-style: italic;
    padding: var(--sp-3) 0;
    text-align: center;
}
.scout-error {
    color: var(--c-bad);
    padding: var(--sp-3) 0;
}
/* Setup pane (no creds yet) */
.scout-setup-lede { color: var(--c-text-2); margin: 0 0 var(--sp-3) 0; }
.scout-setup-steps { margin: 0 0 var(--sp-3) 0; padding-left: var(--sp-4); color: var(--c-text-2); }
.scout-setup-steps li { margin-bottom: var(--sp-2); line-height: 1.55; }
.scout-setup-hint { color: var(--c-text-faint); font-size: var(--fs-sm); margin: 0; }
.scout-setup code, .scout-setup-hint code {
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 3px;
    padding: 1px 6px;
    font-size: var(--fs-xs);
    color: var(--c-text-2);
}
.scout-link { color: var(--c-link); text-decoration: underline; text-decoration-style: dotted; }
.scout-link:hover { color: var(--c-accent); }
/* Input row (paste names + buttons) */
.scout-input-row {
    display: flex; flex-direction: column; gap: var(--sp-2);
    margin-bottom: var(--sp-3);
}
.scout-input-label { color: var(--c-text-2); font-size: var(--fs-sm); }
.scout-input-label code {
    background: var(--c-surface-2);
    padding: 1px 5px; border-radius: 3px;
    font-size: var(--fs-xs);
    color: var(--c-text-3);
}
.scout-input {
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 4px;
    padding: var(--sp-2) var(--sp-3);
    color: var(--c-text);
    font-family: inherit;
    font-size: var(--fs-sm);
    resize: vertical;
    min-height: 60px;
}
.scout-input:focus { outline: none; border-color: var(--c-accent); }
.scout-input-actions {
    display: flex; gap: var(--sp-2); flex-wrap: wrap;
}
/* Filter chip rows */
.scout-filters {
    display: flex; flex-direction: column; gap: var(--sp-2);
    margin-bottom: var(--sp-3);
    padding: var(--sp-2) 0;
    border-top: 1px solid var(--c-border);
    border-bottom: 1px solid var(--c-border);
}
.scout-filter-group {
    display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
}
.scout-filter-label {
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-faint);
    font-weight: 700;
    margin-right: var(--sp-1);
}
/* Results table */
.scout-results-meta {
    color: var(--c-text-faint);
    font-size: var(--fs-xs);
    margin-bottom: var(--sp-2);
}
.scout-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--fs-sm);
}
.scout-table th {
    text-align: left;
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-faint);
    font-weight: 700;
    padding: var(--sp-2) var(--sp-2);
    border-bottom: 1px solid var(--c-border);
}
/* Sortable header — clickable, hover-darkens, active column shows the
   direction arrow in accent color. The arrow span sits inline so the
   click target is the whole th. */
.scout-th-sort {
    cursor: pointer;
    user-select: none;
}
.scout-th-sort:hover { color: var(--c-text-2); }
.scout-th-sort.is-active { color: var(--c-text); }
.scout-th-sort .scout-th-arrow {
    margin-left: 4px;
    color: var(--c-accent);
    font-size: 0.75em;
}
.scout-table td {
    padding: var(--sp-2) var(--sp-2);
    border-bottom: 1px solid #1a2333;
    color: var(--c-text-2);
    vertical-align: middle;
}
.scout-row.scout-status-rejected td,
.scout-row.scout-status-ignored td { opacity: 0.45; }
/* 'Joined' rows used to dim to 0.6 opacity; per user feedback, full
   visibility is more useful since these are guildmates and officers
   want to read their data clearly. Kept the ✓ glyph after the name
   (rule below) as a quiet 'in-guild' signal that doesn't muddy the
   row. */
/* Stub row — character was in the guild roster but Blizzard 404'd
   their profile. Row is dimmer + carries a "profile unavailable"
   badge next to the name so officers know why level/class/spec
   are blank instead of treating it as a render bug. */
.scout-row-stub td { opacity: 0.55; }
.scout-stub-badge {
    margin-left: 6px;
    color: var(--c-text-faint);
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 3px;
    padding: 1px 6px;
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    font-weight: 700;
    cursor: help;
}
.scout-row.scout-status-joined .scout-name::after {
    content: ' ✓';
    color: var(--c-accent);
}
.scout-name { font-weight: 600; text-decoration: none; }
.scout-name:hover { text-decoration: underline; }
.scout-guildless {
    color: var(--c-accent);
    font-weight: 600;
    text-transform: uppercase;
    font-size: var(--fs-xs);
    letter-spacing: var(--tracking-wide);
}
.scout-guilded {
    color: var(--c-text-3);
    font-style: italic;
}
.scout-status-select {
    /* Suppress the default dropdown chrome — native arrow renders
       black on dark backgrounds and was effectively invisible per
       user. Replace with a custom accent-colored SVG chevron via
       background-image. The 22 px right padding leaves room for the
       chevron without overlapping the option text. */
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background: var(--c-surface-2) url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='%2333E4DE' d='M5 6L0 0h10z'/%3E%3C/svg%3E") no-repeat right 6px center;
    border: 1px solid var(--c-border);
    border-radius: 3px;
    padding: 2px 22px 2px 6px;
    color: var(--c-text-2);
    font-size: var(--fs-xs);
    font-family: inherit;
    text-transform: capitalize;
}
.scout-status-select:focus { outline: none; border-color: var(--c-accent); }
@media (max-width: 700px) {
    .scout-table { font-size: var(--fs-xs); }
    /* Bump the chip padding on phones so the touch target's
       comfortably sized — the .scout-addon-actions column wrapper
       already handles the right-align + stacked layout, so the
       only thing the mobile override needs to do is widen the
       hit area. */
    .scout-addon-link {
        padding: 6px 10px;
    }
    /* Mobile: hide Race (5), Last login (7), Notes (9) — least
       actionable on a phone. Keep Name, Level, Class, Spec, Guild,
       Status, ✕. */
    .scout-table th:nth-child(5),
    .scout-table td:nth-child(5),
    .scout-table th:nth-child(7),
    .scout-table td:nth-child(7),
    .scout-table th:nth-child(9),
    .scout-table td:nth-child(9) { display: none; }
    /* Tighter cell padding on phones so the remaining 7 columns +
       Status dropdown fit in the panel width without clipping. */
    .scout-table th, .scout-table td { padding: 4px 6px; }
    .scout-guild-input { min-width: 0; flex: 1 1 100%; }
    /* Both Pending + Scout tables sit inside a `.at-panel` — let
       it scroll horizontally on phones when content can't compress
       further. Pending was already in this pattern; scout is now
       wrapped to match (per user "results table should be mobile
       friendly like the pending table, which imo works well"). */
    .at-panel { overflow-x: auto; padding-left: 8px; padding-right: 8px; }
}

/* About Vis Maior — sits at the bottom of the Join page below
   Recruiting + Contact. Carries an optional dimmed bg image
   (assets/about/vis-maior.jpg) layered behind a darker gradient
   overlay so the title + text stay readable regardless of what
   image is dropped in. Until an image is uploaded the dual-layer
   gradient still gives the card visual weight on top of the
   surface color. Same pattern as the progression page hero card. */
.about-card {
    /* Inline style sets the actual `background:` shorthand with the
       gradient + image + surface fallback. This rule supplies layout. */
    border: 1px solid #223049;
    border-radius: 10px;
    /* More vertical breathing room than recruit-card so the section
       feels like a "hero" closer rather than another data row. */
    padding: 36px 28px 32px 28px;
    margin-top: 16px;
    margin-bottom: 8px;
    position: relative;
    overflow: hidden;
    /* Subtle accent bar along the left edge — picks up the brand
       teal so the card reads as belonging to the same visual
       family as the brand strip + accent buttons elsewhere. */
    box-shadow: inset 4px 0 0 0 var(--c-accent);
}
.about-eyebrow {
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide, 0.18em);
    color: var(--c-accent);
    font-weight: 800;
    margin-bottom: 6px;
}
.about-heading {
    font-size: 28px;
    font-weight: 700;
    color: var(--c-text);
    letter-spacing: 0.01em;
    margin: 0 0 4px 0;
    line-height: 1.1;
    /* Subtle text shadow for readability over the image. */
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
/* Dictionary-style line under the heading: phonetic spelling +
   Latin meaning. Reads as a small etymological note rather than
   competing with the heading or lede. */
.about-pronounce {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 8px;
    margin: 0 0 12px 0;
    font-size: var(--fs-sm);
    color: var(--c-text-3);
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}
.about-ipa {
    font-style: italic;
    color: var(--c-text-2);
    /* Slightly mono-ish so the IPA-style brackets read as
       phonetic notation, not body copy. */
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: var(--fs-xs);
    letter-spacing: 0.01em;
}
.about-pronounce-sep {
    color: var(--c-text-faint);
    user-select: none;
}
.about-latin {
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide, 0.18em);
    font-weight: 700;
    color: var(--c-accent);
    padding: 1px 6px;
    border: 1px solid var(--c-accent);
    border-radius: 3px;
    line-height: 1.4;
}
.about-meaning {
    font-style: italic;
    color: var(--c-text-2);
}
.about-lede {
    font-size: var(--fs-md);
    line-height: 1.5;
    color: var(--c-text-2);
    margin: 0;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
    /* No wrap on desktop — keeps the lede as a single hero line.
       Mobile breakpoint (≤520px) below re-enables wrapping since
       the line genuinely won't fit on phones. */
    white-space: nowrap;
}
.about-lede strong {
    color: var(--c-text);
    font-weight: 700;
}
@media (max-width: 520px) {
    .about-card {
        padding: 28px 18px 24px 18px;
    }
    .about-heading { font-size: 22px; }
    /* Re-enable wrapping on phones — the single-line lede genuinely
       can't fit at narrow widths; force-fitting it would either
       trigger horizontal scroll on the card or shrink the text to
       unreadable. */
    .about-lede { white-space: normal; }
}
#schedule-root > .recruit-card:last-child,
#schedule-root > .sched-card:last-child { margin-bottom: 0; }
/* ── Raid Schedule card ──────────────────────────────── */
.sched-card {
    background: var(--c-surface);
    border: 1px solid #223049;
    border-radius: 8px;
    padding: 14px 16px 14px 16px;
    margin-bottom: 8px;
}
.sched-header {
    display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
    margin-bottom: 10px;
}
.sched-title { font-size: var(--fs-md); font-weight: 600; color: #aab4c2; letter-spacing: 0.04em; text-transform: uppercase; }
.sched-tz { font-size: var(--fs-xs); color: #7a8aa0; }
.sched-edit-btn { margin-left: auto; }
/* Recurring sections (Weekly / One-Time) sit side by side and only wrap
   to a new line when there's genuinely no room — so a small One-Time
   group (e.g. "Karazhan") fills the space to the right of the weekly
   columns instead of dropping to its own line. */
.sched-recurring { font-size: var(--fs-md); color: var(--c-text-2); display: flex; flex-wrap: wrap; gap: 10px 32px; align-items: flex-start; }
.sched-recurring-section { flex: 0 1 auto; min-width: 0; }
.sched-recurring-grid { display: flex; flex-wrap: wrap; gap: 6px 14px; }
.sched-recurring-row { flex: 0 1 auto; min-width: 260px; padding: 6px 14px 6px 0; border-right: 1px solid var(--c-border); }
.sched-recurring-row:last-child { border-right: none; padding-right: 0; }
.sched-recurring-row-proposed {
    /* Soft amber tint so a proposed slot reads as "still up for
       discussion" without overpowering the confirmed weekly slots
       sitting next to it. */
    background: rgba(212, 170, 68, 0.06);
    border-left: 2px solid rgba(212, 170, 68, 0.55);
    padding-left: 8px;
    border-radius: var(--r-sm);
}
.sched-recurring-day { margin-bottom: 6px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
/* PROPOSED badge — uppercase amber chip next to the day name. Matches
   the CALLOUT badge tone elsewhere in the app. */
.sched-proposed-badge {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    padding: 1px 6px;
    border-radius: var(--r-sm);
    border: 1px solid rgba(212, 170, 68, 0.65);
    color: rgb(212, 170, 68);
    background: rgba(212, 170, 68, 0.1);
}
/* CORE badge — same shape as PROPOSED but in VM accent so confirmed
   weekly slots read at a glance distinct from proposed ones. */
.sched-core-badge {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    padding: 1px 6px;
    border-radius: var(--r-sm);
    border: 1px solid rgba(51, 228, 222, 0.55);
    color: var(--c-accent);
    background: rgba(51, 228, 222, 0.1);
}
/* Small "Proposed" tag beside the team pill on a proposed night — carries the
   status now that the pill shows the team. Plain gold text, no border. */
.sched-proposed-tag {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: rgb(212, 170, 68);
    margin-left: 2px;
}
/* Interest cluster (count pill + sign-up button) on a proposed slot. */
.sched-recurring-interest {
    margin-top: 6px;
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}
.sched-interest-count {
    font-size: var(--fs-xs);
    color: var(--c-text-2);
    cursor: help;
}
/* Editor-side proposed checkbox + interest count chip. */
.sched-row-proposed {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: var(--fs-xs);
    color: var(--c-text-2);
    cursor: pointer;
    user-select: none;
    margin: 0 6px;
}
.sched-row-proposed input[type="checkbox"] { margin: 0; cursor: pointer; }
.sched-row-interest-count {
    font-size: var(--fs-xs);
    color: rgb(212, 170, 68);
    margin: 0 4px;
}
/* Label-group banner on the homepage schedule card. Quiet uppercase
   header that clusters slots sharing a label ("Progression Raids" /
   "Alt Raids"). Groups sit side-by-side via the parent flex row so
   the chrome stays compact when the schedule fits in one line; they
   wrap to the next line only when there's no room. Vertical divider
   between groups visually separates them since they're now inline. */
.sched-label-row {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 22px;
    align-items: flex-start;
}
.sched-label-group {
    flex: 0 1 auto;
    min-width: 0;
    /* Each group beyond the first gets a left border + padding so the
       visual separation reads even when groups sit on the same line. */
    border-left: 1px solid var(--c-border);
    padding-left: 14px;
}
.sched-label-group:first-child { border-left: 0; padding-left: 0; }
.sched-label-head {
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-2);
    font-weight: 700;
    padding: 0 0 4px;
    margin-bottom: 4px;
}
.sched-label-count {
    font-size: var(--fs-micro);
    color: var(--c-text-faint);
    font-weight: 600;
    margin-left: 6px;
    letter-spacing: 0;
    text-transform: none;
}
/* Inside each label group, slots flow inline horizontally just like
   the legacy grid did — no per-group line break, the row gap kicks in
   only when the actual width exceeds available space. */
.sched-label-group .sched-recurring-grid { gap: 6px 14px; }
/* On narrow viewports, drop the vertical divider in favor of a top
   divider so each group still reads as its own block when stacked,
   and force each group to span the full row width — content-sized
   pills (Phase 2 Progression / Alternate / Gruul Farm) felt cramped
   alongside each other on a phone. */
@media (max-width: 640px) {
    .sched-label-group {
        flex: 0 0 100%;
        max-width: 100%;
        border-left: 0;
        padding-left: 0;
        padding-top: 6px;
        border-top: 1px dashed var(--c-border);
    }
    .sched-label-group:first-child { border-top: 0; padding-top: 0; }
}
/* Editor-side label input — sized small but readable. */
#schedule-modal .sched-label-input {
    /* Match the dropdowns' height (same vertical padding + font) and stay
       compact — the label is short ("Team 1"). */
    font-size: var(--fs-sm);
    padding: 5px 10px;
    min-width: 0;
}
.sched-recurring-times { display: flex; flex-direction: column; gap: 2px; padding-left: 2px; }
@media (max-width: 480px) {
    .sched-recurring-grid { gap: 4px 0; }
    .sched-recurring-row { flex-basis: 100%; border-bottom: 1px dashed var(--c-border); }
    .sched-recurring-row:last-child { border-bottom: none; }
}
.sched-tz-row { display: flex; align-items: baseline; gap: 8px; font-size: var(--fs-sm); color: var(--c-text-3); }
.sched-tz-row.sched-tz-source { color: var(--c-text-2); font-weight: 600; }
.sched-tz-label { letter-spacing: .04em; }
.sched-tz-time { color: inherit; font-variant-numeric: tabular-nums; min-width: 140px; white-space: nowrap; }
.sched-day { color: var(--c-accent); font-weight: 600; }
.sched-time { color: var(--c-text-2); }
.sched-empty { font-size: var(--fs-sm); color: #aab4c4; font-style: italic; }
.sched-notes { font-size: var(--fs-sm); color: #e24b4a; margin-top: 10px; white-space: pre-wrap; }
.sched-subsection { margin-top: 16px; border-top: 1px solid var(--c-border); padding-top: 12px; }
.sched-subtitle { font-size: var(--fs-micro); font-weight: 700; letter-spacing: .08em; color: var(--c-text-faint); text-transform: uppercase; margin-bottom: 8px; display: block; }
.sched-subtitle-inline { display: inline; margin-right: 6px; margin-bottom: 0; }
.sched-raid-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 0; }
/* 3-col grid since the loot-row column was dropped — avail count
   slides into where the Export-TMB link used to render (col 3) per
   user request. Past raids already used a 3-col grid. */
.sched-raid-row { display: grid; grid-template-columns: 230px 140px 1fr; align-items: center; gap: 18px; padding: 10px; font-size: var(--fs-md); border-radius: 3px; min-height: 48px; box-sizing: border-box; }
.sched-raid-list .sched-raid-row:nth-child(odd) { background: #1a2540; }
/* justify-self:start so the span only takes its content width
   instead of stretching across the full 1fr column. Without this,
   the empty space to the right of "9/10 available" was a stretched
   span that picked up the avail's hover tooltip — read as
   "clickable white space" per user. */
.sched-raid-avail { color: var(--c-accent); font-size: var(--fs-xs); white-space: nowrap; cursor: default; }
/* Wrap avail + extras into a single grid cell so they sit inline
   instead of separate cells with the row's gap pushing them apart.
   justify-self:start preserves the previous left-anchored placement
   that .sched-raid-avail used to do solo. */
.sched-raid-avail-wrap {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    justify-self: start;
    white-space: nowrap;
}
/* Officer/GM-only "+N available" extras badge — sits next to the
   sched-raid-avail count. Confirm/success green (--c-good): these are
   spare bodies you CAN pull in, so it reads as "good news / room to
   fill". Distinct from the orange (still-filling) signed-up count. */
.sched-raid-extras {
    color: var(--c-good);
    font-size: var(--fs-xs);
    white-space: nowrap;
    cursor: default;
    opacity: 0.9;
}
/* "[N maybe]" tentative-RSVP badge — sits next to the avail count.
   Blue to match the Maybe RSVP button (Discord blurple), distinct
   from the cyan/red avail count and the warm extras badge. */
.sched-raid-maybe {
    color: var(--c-link);
    font-size: var(--fs-xs);
    white-space: nowrap;
    cursor: default;
}
/* "[N unavailable]" called-out badge — red, sits right of the maybe badge. */
.sched-raid-unavail {
    color: var(--c-bad);
    font-size: var(--fs-xs);
    white-space: nowrap;
    cursor: default;
}
/* "[N standby]" badge — subs for a full raid, sits right of unavailable. */
.sched-raid-standby {
    color: var(--c-warn);
    font-size: var(--fs-xs);
    white-space: nowrap;
    cursor: default;
}
/* Upcoming-raid col 3 wrapper — holds [N/M available] and the
   "JOIN THIS RAID" button side by side so the row stays a clean 3-col
   grid (the join button sits to the right of the avail count). */
.sched-raid-c3 { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.sched-join-btn { white-space: nowrap; }
/* Button-to-button gap matches the Raids Join bar (var(--sp-2) = 8px); the
   parent .sched-raid-c3's wider 12px gap still separates them from the count. */
.sched-join-btns { display: inline-flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
/* Center JOIN bar on the Raids toolbar — claims the middle of the
   subnav row and centers the button; collapses to nothing when empty
   (no active config or the viewer is already in the comp). */
/* Action buttons now sit at the LEFT of the raid subnav (where the Group
   Editor button used to be), not centered. */
#comp-join-bar { flex: 0 0 auto; display: inline-flex; justify-content: flex-start; align-items: center; gap: var(--sp-2); }
#comp-join-bar:empty { flex: 0 0 0; }
.comp-maybe-btn { white-space: nowrap; letter-spacing: .04em; }
/* Soft breathing glow around the JOIN button — a pulsing box-shadow
   halo (smooth, no rotation) to gently draw the eye. */
.comp-join-btn,
.sched-join-btn.btn-confirm { white-space: nowrap; letter-spacing: .04em; animation: comp-join-glow 2.4s ease-in-out infinite; }
@keyframes comp-join-glow {
    0%, 100% { box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-accent) 35%, transparent), 0 0 6px color-mix(in srgb, var(--c-accent) 30%, transparent); }
    50%      { box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-accent) 60%, transparent), 0 0 14px color-mix(in srgb, var(--c-accent) 60%, transparent); }
}
@media (prefers-reduced-motion: reduce) { .comp-join-btn, .sched-join-btn.btn-confirm { animation: none; } }
.sched-raid-date { color: var(--c-text-2); font-weight: 600; }
/* "July 14" stays the big bold date; "· Tuesday · 7:00 PM ST" is smaller/lighter.
   The "·" separators carry even spacing on both sides so the parts read evenly. */
.sched-raid-date-main { font-weight: 600; }
.sched-raid-date-sub { font-size: var(--fs-sm); font-weight: 400; color: var(--c-text-3); margin-left: 12px; }
.sched-raid-sep { color: var(--c-text-faint); margin: 0 6px; }
.sched-raid-time { white-space: nowrap; }
.sched-raid-name { color: var(--c-text-3); font-size: var(--fs-sm); justify-self: start; }
.sched-raid-list .sched-raid-link { text-decoration: none; cursor: pointer; }
.sched-raid-list .sched-raid-link:hover,
.sched-raid-list-past .sched-raid-link:hover { color: var(--c-accent); text-decoration: underline; }
.sched-raid-list-past .sched-raid-date { color: var(--c-text-2); font-weight: 600; }
.sched-raid-list-past .sched-raid-name { color: var(--c-text-3); }
.sched-raid-list-past .sched-raid-row { grid-template-columns: 230px 140px 1fr; gap: 18px; }
.sched-raid-done {
    display: inline-block;
    justify-self: start;
    font-size: var(--fs-micro);
    padding: 2px 7px;
    border-radius: 3px;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: 600;
    color: var(--c-accent);
    background: #12262a;
    border: 1px solid #2a5a60;
}
.sched-raid-name-wrap { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
.sched-raid-upcoming {
    display: inline-block;
    font-size: var(--fs-micro);
    padding: 2px 7px;
    border-radius: 3px;
    text-transform: uppercase;
    letter-spacing: 1px;
    font-weight: 600;
    color: var(--c-warn, #e6b07a);
    background: rgba(230, 176, 122, 0.10);
    border: 1px solid rgba(230, 176, 122, 0.45);
}
/* Inline-flex (was inline-grid with fixed columns) so cells size to
   their natural content and the link → EDIT gap is exactly the flex
   gap value (8px) regardless of which provider rendered the row.
   The previous grid layout's empty btn-slot column added an extra
   16px of dead space between the link and EDIT button on Vis Maior
   raids that had no first button to occupy column 2. */
.sched-sr { display: inline-flex; align-items: center; gap: 8px; font-size: var(--fs-sm); }
.sched-sr-link-cell, .sched-sr-token-cell { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
/* Empty button-slot placeholder used when the user isn't a loot
   manager (no EDIT button). Reserve the same vertical footprint a
   real .sched-sr-btn occupies so the row's height doesn't shrink
   for non-managers — keeps upcoming/past row heights consistent
   regardless of which buttons render. */
.sched-sr-btn-slot {
    display: inline-block;
    min-height: 26px;
    line-height: 26px;
}
.sched-sr-link { color: var(--c-accent); text-decoration: none; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; white-space: nowrap; }
.sched-sr-link:hover { text-decoration: underline; }
.sched-sr-empty { color: var(--c-text-faint); font-style: italic; text-transform: uppercase; letter-spacing: 0.05em; }
.sched-sr-edit, .sched-sr-btn {
    font-family: inherit;
    font-size: var(--fs-xs);
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    text-decoration: none;
    color: var(--c-accent);
    background: var(--c-surface);
    border: 1px solid #33E4DE66;
    padding: 5px 13px;
    border-radius: 4px;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
    text-align: center;
    /* Was width: 100% (sized to old grid column = ~64px). Now in
       inline-flex parent: width auto = sized to label content. The
       full-row stretch behavior is preserved on mobile via the
       phone-specific min-width rules elsewhere in this file. */
    box-sizing: border-box;
}
.sched-sr-edit:hover, .sched-sr-btn:hover { background: var(--c-accent); color: var(--c-bg); border-color: var(--c-accent); }
.sched-sr-token {
    background: var(--c-bg); border: 1px solid var(--c-border); color: var(--c-text-2);
    padding: 5px 13px; border-radius: 4px; font-size: var(--fs-xs); font-family: monospace;
    box-sizing: border-box; display: inline-flex; align-items: center;
}
/* Schedule editor modal — visuals come from admin-input / admin-select /
   tab-btn classes. Only layout lives here. 680px-wide box so the
   day | time | label | interest | remove | Proposed cluster fits on
   one row without line-wrapping. */
#schedule-modal .cfg-box { max-width: 900px; }
#schedule-modal .sched-row {
    display: grid;
    /* drag · day · time-cell · team(small) · × · Weekly · Proposed.
       Content-sized columns (no forced 1fr stretch) so the row stays compact
       and the Weekly/Proposed controls never bleed off the right edge. */
    grid-template-columns: auto minmax(90px, auto) auto minmax(64px, 120px) auto auto auto;
    gap: 8px;
    align-items: center;
    margin-bottom: 6px;
    border-top: 2px solid transparent;
    border-bottom: 2px solid transparent;
}
/* Start/end time cell (two pickers + a small "to") share one grid column;
   the pickers keep enough width so "10:00 PM" never truncates. */
#schedule-modal .sched-time-cell { display: inline-flex; align-items: center; gap: 6px; }
#schedule-modal .sched-time-cell .sched-time-input { min-width: 92px; }
#schedule-modal .sched-time-sep { color: var(--c-text-faint); font-size: var(--fs-xs); flex: 0 0 auto; }
#schedule-modal .sched-row.dragging { opacity: .4; }
#schedule-modal .sched-row.drag-over-top { border-top-color: var(--c-accent); }
#schedule-modal .sched-row.drag-over-bot { border-bottom-color: var(--c-accent); }
.sched-drag-handle {
    cursor: grab;
    color: var(--c-text-faint);
    padding: 3px 4px;
    font-size: var(--fs-base);
    line-height: 1;
    user-select: none;
}
.sched-drag-handle:active { cursor: grabbing; }
#schedule-modal .sched-row-weekly { white-space: nowrap; margin: 0; }
#schedule-modal #sched-tz { width: 100%; }
#schedule-modal .sched-add-btn { margin-top: 4px; }
/* Keep the proposed-checkbox label compact + on a single line so it
   doesn't squeeze the day/time fields. */
#schedule-modal .sched-row-proposed { white-space: nowrap; margin: 0; }
#schedule-modal .sched-row-interest-count { white-space: nowrap; margin: 0; }
/* Mobile: 6-col desktop row would blow past viewport width on
   phones. Compact 2-row layout per slot (3 if it carries an
   interest count):
     col 1 (1fr)            col 2 (auto)        col 3 (auto)
     row 1: Day              Time                ×
     row 2: Label (cols 1-2 span)                 Proposed
     row 3: (interest count, full-width right-aligned, if any)
   Each slot gets a dashed separator so they read as discrete
   cards instead of running together. */
@media (max-width: 600px) {
    #schedule-modal .sched-row {
        grid-template-columns: minmax(0, 1fr) auto auto;
        column-gap: 8px;
        row-gap: 6px;
        padding-bottom: 10px;
        margin-bottom: 10px;
        border-bottom: 1px dashed var(--c-border);
    }
    #schedule-modal .sched-row:last-child {
        border-bottom: 0;
        padding-bottom: 0;
        margin-bottom: 4px;
    }
    /* Drag handle is a pointer affordance — hide on touch layouts; the
       up/down array order still saves, officers just reorder on desktop. */
    #schedule-modal .sched-drag-handle { display: none; }
    #schedule-modal .sched-row .sched-day-sel    { grid-column: 1; grid-row: 1; min-width: 0; }
    #schedule-modal .sched-row .sched-time-input { grid-column: 2; grid-row: 1; }
    #schedule-modal .sched-row .sched-row-remove {
        grid-column: 3;
        grid-row: 1;
        justify-self: end;
    }
    #schedule-modal .sched-row .sched-label-input {
        grid-column: 1 / 3;
        grid-row: 2;
        min-width: 0;
    }
    #schedule-modal .sched-row .sched-row-proposed {
        grid-column: 3;
        grid-row: 2;
        justify-self: end;
        display: inline-flex;
        align-items: center;
        gap: 6px;
        white-space: nowrap;
    }
    #schedule-modal .sched-row .sched-row-weekly {
        grid-column: 1;
        grid-row: 3;
        justify-self: start;
        display: inline-flex;
        align-items: center;
        gap: 6px;
        white-space: nowrap;
    }
    #schedule-modal .sched-row .sched-row-interest-count {
        grid-column: 2 / -1;
        grid-row: 3;
        justify-self: end;
        font-size: var(--fs-xs);
    }
}
.recruit-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 10px;
    flex-wrap: wrap;
}
.recruit-title { font-size: var(--fs-md); font-weight: 600; color: #aab4c2; letter-spacing: 0.04em; text-transform: uppercase; }
/* Unified teal call-to-action button — used by Discord link, Manage, Edit Schedule,
 * Resist Gear, etc. Keeps padding/font/hover identical so the UI stops feeling
 * like a mismatched patchwork. */
.btn-teal,
.recruit-discord,
.sched-edit-btn {
    font-family: inherit;
    font-size: var(--fs-xs);
    line-height: 1;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    text-decoration: none;
    color: var(--c-accent);
    background: transparent;
    border: 1px solid #33E4DE66;
    padding: 6px 13px;
    border-radius: 4px;
    cursor: pointer;
    transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
    display: inline-flex;
    align-items: center;
    box-sizing: border-box;
}
.btn-teal:hover,
.recruit-discord:hover,
.sched-edit-btn:hover { background: var(--c-accent); color: var(--c-bg); border-color: var(--c-accent); }
.recruit-body {
    font-size: var(--fs-sm);
    color: #aab4c2;
    line-height: 1.5;
    margin-bottom: 12px;
}
.recruit-contacts {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
    gap: 8px;
}
.recruit-contact {
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: 6px;
    padding: 8px 10px;
}
.recruit-contact-name { font-size: var(--fs-md); font-weight: 600; line-height: 1.2; }
.recruit-contact-meta { font-size: var(--fs-micro); color: #7a8aa0; margin-top: 2px; letter-spacing: 0.02em; }
.recruit-contact-empty { font-size: var(--fs-xs); color: #7a8aa0; font-style: italic; }
.recruit-inline-link { color: var(--c-accent); text-decoration: none; border-bottom: 1px dotted #33E4DE66; }
.recruit-inline-link:hover { border-bottom-color: var(--c-accent); }

/* Admin / Backups card */
.admin-card {
    background: var(--c-surface);
    border: 1px solid #223049;
    border-radius: 8px;
    padding: 14px 16px 16px 16px;
}
.admin-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 8px;
}
.admin-title { font-size: var(--fs-md); font-weight: 600; color: #aab4c2; letter-spacing: 0.04em; text-transform: uppercase; }
.admin-body {
    font-size: var(--fs-sm);
    color: var(--c-text-3);
    line-height: 1.5;
    margin-bottom: 12px;
}
.admin-body code {
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: 3px;
    padding: 1px 5px;
    font-size: var(--fs-xs);
    color: var(--c-text-2);
}
.admin-list {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--c-border);
    border-radius: 6px;
    overflow: hidden;
    max-height: 280px;
    overflow-y: auto;
}
.admin-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
    padding: 7px 10px;
    font-size: var(--fs-sm);
    border-top: 1px solid var(--c-border);
}
.admin-row:first-child { border-top: none; }
.admin-row-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.admin-row-name { color: var(--c-text-2); font-family: 'SF Mono', Menlo, Consolas, monospace; font-size: var(--fs-xs); }
.admin-backup-tier {
    display: inline-block;
    font-family: inherit;
    font-size: 9px;
    font-weight: 700;
    letter-spacing: 0.08em;
    color: var(--c-accent);
    border: 1px solid #33E4DE55;
    background: #33E4DE14;
    padding: 1px 6px;
    border-radius: 3px;
    text-transform: uppercase;
    margin-left: 6px;
    vertical-align: middle;
}
.admin-row-meta { color: #7a8aa0; font-size: var(--fs-xs); }
.admin-row .admin-btn { font-size: var(--fs-xs); padding: 3px 10px; flex-shrink: 0; }
.admin-empty { font-size: var(--fs-xs); color: #7a8aa0; font-style: italic; padding: 10px; }
.admin-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin-bottom: 16px;
    /* Each admin-grid is a jump target for the page subnav; offset
       smooth-scroll so the section title sits below BOTH the sticky
       page header AND the sticky admin subnav (admin-subnav itself
       sticks at top:var(--sticky-header-h) and is ~50px tall). The
       extra 64px = subnav height + breathing room so the section
       title doesn't kiss the bottom edge of the subnav. */
    scroll-margin-top: calc(var(--sticky-header-h, 100px) + 64px);
}
.admin-grid:last-child { margin-bottom: 0; }
@media (max-width: 900px) { .admin-grid { grid-template-columns: 1fr; } }
/* Forced 2-column grid for admin-card pairs that share a row (e.g.
   Accounts | Site notice, Backups | Server info). The default
   .admin-grid uses :has(.admin-card-wide) to collapse to 1 column
   when a wide-flagged card is present — this variant overrides that
   so wide-flagged cards CAN sit half-width when explicitly paired.
   Mobile (≤900px) still stacks. */
.admin-grid-pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin-bottom: 16px;
    /* Same scroll-margin offset as .admin-grid — accounts for both
       the sticky page header and the sticky admin subnav above. */
    scroll-margin-top: calc(var(--sticky-header-h, 100px) + 64px);
}
.admin-grid-pair .admin-card-wide {
    grid-column: auto;
    max-width: none;
}
@media (max-width: 900px) { .admin-grid-pair { grid-template-columns: 1fr; } }
/* Each card in an .admin-grid-pair sits inside a .admin-pair-cell
   wrapper that carries the jump-target id. The wrapper is the grid
   item; the card inside fills the cell. This shape keeps the
   .admin-card-wide rule's `grid-column: 1 / -1` from firing (since
   the wide card is no longer a direct grid child) so the 2-column
   layout holds. flex-column + cards-stretch lets pairs of differing
   content heights stay aligned to the cell. */
.admin-pair-cell {
    display: flex;
    flex-direction: column;
    scroll-margin-top: calc(var(--sticky-header-h, 100px) + 64px);
    min-width: 0;
}
.admin-pair-cell > .admin-card { flex: 1; }

/* `.admin-grid-pair-fit` modifier (used on Backups + Server info):
   don't auto-stretch cells to equal heights. Backups stays at its
   natural (typically shorter) height — no dead white space below
   its content. Server info gets capped via JS to match backups'
   height with internal scroll on `.server-info-body`. */
.admin-grid-pair-fit { align-items: start; }
.admin-grid-pair-fit .admin-pair-cell > .admin-card { flex: 0 1 auto; }
.admin-grid-pair-fit #admin-jump-server .admin-card {
    /* JS sets max-height inline based on backups' measured height.
       overflow:hidden contains the scroll inside the body section
       so the card border stays clean. */
    overflow: hidden;
}
.admin-grid-pair-fit #admin-jump-server .server-info-body {
    /* JS sets max-height inline; this is the scrolling viewport
       inside the server info card. The overflow: auto kicks in
       only when the body's content exceeds the JS-set max-height.
       Dark bg + border match the Backups card's `.admin-pw-list`
       so the area visually reads as "this is the scrollable
       content region inside the card" rather than blending into
       the card surface. */
    overflow-y: auto;
    background: #0b1220;
    border: 1px solid var(--c-border);
    border-radius: 4px;
    padding: var(--sp-3);
}

/* Quick-jump subnav at the top of the admin page. Sticky just below
   the page header so the user can jump between sections regardless
   of scroll depth — same pattern as the wishlist sticky toolbar.
   IntersectionObserver-driven `.is-stuck` toggles the box-shadow
   on/off so the shadow only shows when the bar is actually pinned
   (clean look at top-of-page scroll). */
.admin-subnav {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
    margin-bottom: var(--sp-4);
    /* Symmetric top/bottom padding so the link text reads visually
       centered in the bar. The 1px bottom border doesn't count
       toward "padding" optically — making top + bottom equal in the
       padding declaration produces equal visible whitespace above
       and below the text. */
    padding: var(--sp-2) 0;
    border-bottom: 1px solid var(--c-border);
    font-size: var(--fs-sm);
    position: sticky;
    top: var(--sticky-header-h, 100px);
    z-index: 5;
    background: var(--c-bg);
    /* Negative top margin neutralizes the padding-top so the bar
       still sits in normal layout flow without an extra gap above. */
    margin-top: calc(-1 * var(--sp-2));
    box-shadow: 0 6px 8px -4px rgba(0, 0, 0, 0);
    transition: box-shadow 0.15s ease, padding 0.15s ease;
}
.admin-subnav.is-stuck {
    box-shadow: 0 6px 8px -4px rgba(0, 0, 0, 0.55);
    /* Slightly more vertical padding when pinned so the links
       breathe within their floating chrome. Top + bottom equal
       (both var(--sp-3)) so the text stays visually centered. */
    padding: var(--sp-3) 0;
}
.admin-jump-link {
    color: var(--c-text-2);
    text-decoration: none;
    border-bottom: 1px dotted transparent;
    padding: 2px 0;
    cursor: pointer;
    transition: color 0.12s, border-color 0.12s;
}
.admin-jump-link:hover {
    color: var(--c-accent);
    border-bottom-color: var(--c-accent);
}
.admin-subnav-sep {
    color: var(--c-text-faint);
    user-select: none;
}
@media (max-width: 520px) {
    .admin-subnav-sep { display: none; }
    .admin-subnav { gap: 8px 14px; }
}
/* Wide variant — single full-width column inside an admin-grid. Used for
   diagnostic cards (Server info) where the table content is denser than
   the half-width admin-card layout can absorb cleanly. */
.admin-grid:has(.admin-card-wide) { grid-template-columns: 1fr; }
.admin-card-wide {
    grid-column: 1 / -1;
}
/* Feedback inbox card — GM-only list of submissions from the footer
   "Feedback" link. Each entry: small header line (category pill +
   name/anonymous + timestamp + ipHash) and a multi-line message
   below. Unread entries get a soft accent left border + a small
   accent dot in the header for at-a-glance scanning. */
.admin-feedback-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: var(--sp-3);
    max-height: 480px;
    overflow-y: auto;
}
.fb-entry {
    padding: 8px 12px;
    border: 1px solid var(--c-border);
    border-radius: var(--r-sm);
    background: var(--c-surface);
}
.fb-entry-unread {
    border-left: 3px solid var(--c-accent);
    background: rgba(51, 228, 222, 0.04);
}
.fb-meta {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: var(--fs-xs);
    color: var(--c-text-faint);
    margin-bottom: 4px;
    flex-wrap: wrap;
}
.fb-unread-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--c-accent);
    flex-shrink: 0;
}
.fb-cat-pill {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    padding: 1px 6px;
    border-radius: var(--r-sm);
    border: 1px solid currentColor;
}
.fb-cat-bug     { color: var(--c-bad); }
.fb-cat-feature { color: var(--c-good); }
.fb-cat-ux      { color: var(--c-os); }
.fb-cat-other   { color: var(--c-text-2); }
.fb-name { color: var(--c-text-2); font-weight: 600; }
.fb-anon { color: var(--c-text-faint); font-style: italic; }
.fb-ts { font-variant-numeric: tabular-nums; }
.fb-iphash { font-family: monospace; opacity: 0.6; cursor: help; }
.fb-message {
    font-size: var(--fs-sm);
    color: var(--c-text-2);
    white-space: pre-wrap;
    word-break: break-word;
    line-height: 1.4;
}
.admin-card-meta {
    font-size: var(--fs-xs);
    color: var(--c-text-faint);
    margin-left: 8px;
}
/* Tools card — list of standalone /tools/*.php links surfaced on the
   Admin tab so officers/GM don't have to remember the URLs. Each link
   is its own card-row with a label + a one-line blurb. Hover lifts the
   row slightly and tints the background so it reads as clickable. */
.admin-tool-list {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--sp-3);
    padding: 0 var(--sp-3) var(--sp-3);
}
.admin-tool-link {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 10px 14px;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    text-decoration: none;
    color: var(--c-text-2);
    transition: background 0.1s, border-color 0.1s, transform 0.05s;
}
.admin-tool-link:hover {
    background: var(--c-surface-2);
    border-color: var(--c-text-faint);
    transform: translateY(-1px);
}
.admin-tool-label {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: var(--fs-md);
    font-weight: 600;
    color: var(--c-text);
}
.admin-tool-arrow {
    color: var(--c-link);
    font-size: var(--fs-sm);
}
.admin-tool-blurb {
    font-size: var(--fs-xs);
    color: var(--c-text-3);
    line-height: 1.4;
}
/* CLI helpers list — same visual scale as web-tools but reads as
   reference info (no hover state, no pointer cursor, slightly muted
   border/background). Each row: monospace path + one-line blurb. */
.admin-tool-cli-list {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: var(--sp-2);
    padding: 0 var(--sp-3) var(--sp-3);
}
.admin-tool-cli {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 8px 12px;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    cursor: default;
    opacity: 0.85;
}
.admin-tool-cli-name code {
    font-family: ui-monospace, SFMono-Regular, monospace;
    font-size: var(--fs-xs);
    color: var(--c-text);
    background: var(--c-surface-2);
    padding: 1px 6px;
    border-radius: 3px;
}
.admin-tool-cli-blurb {
    font-size: var(--fs-xs);
    color: var(--c-text-3);
    line-height: 1.4;
}
.server-info-body { display: flex; flex-direction: column; gap: 16px; padding: 0 var(--sp-3); }
.server-info-section { display: flex; flex-direction: column; gap: 6px; }
.server-info-h4 {
    margin: 0;
    font-size: var(--fs-sm);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-faint);
    font-weight: 600;
}
.server-info-table {
    border-collapse: collapse;
    width: 100%;
    font-size: var(--fs-sm);
}
.server-info-table th {
    text-align: left;
    padding: 5px 12px 5px 0;
    font-weight: 500;
    color: var(--c-text-faint);
    width: 160px;
    vertical-align: top;
    white-space: nowrap;
}
.server-info-table td {
    padding: 5px 0;
    vertical-align: top;
    color: var(--c-text-2);
    word-break: break-word;
}
.server-info-table code {
    font-size: 12px;
    background: var(--c-surface);
    padding: 1px 5px;
    border-radius: 3px;
    color: var(--c-text-2);
}
.server-info-yes { color: var(--c-good); font-weight: 600; }
.server-info-no { color: var(--c-bad); font-weight: 600; }
.server-info-unknown { color: var(--c-warn); font-weight: 600; }
.server-info-details { margin-top: 4px; }
.server-info-details > summary {
    cursor: pointer;
    color: var(--c-text-faint);
    font-size: var(--fs-xs);
    padding: 4px 0;
    user-select: none;
}
.server-info-details > summary:hover { color: var(--c-text-2); }
.server-info-pillrow {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin: 4px 0 8px;
}
.server-info-pill {
    display: inline-block;
    padding: 2px 7px;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 999px;
    font-size: 11px;
    color: var(--c-text-3);
    font-family: ui-monospace, monospace;
}
.server-info-pill.on {
    border-color: var(--c-good);
    color: var(--c-good);
}
.server-info-pill.off {
    border-color: var(--c-bad);
    color: var(--c-bad);
    opacity: 0.6;
}
.server-info-files th { width: auto; padding-right: 16px; }
.server-info-files thead th {
    border-bottom: 1px solid var(--c-border);
    padding-bottom: 4px;
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    font-size: 10px;
}
.server-info-mtime { color: var(--c-text-faint); font-size: var(--fs-xs); }
.server-info-advice {
    margin-top: 6px;
    font-size: var(--fs-xs);
    line-height: 1.5;
    color: var(--c-text-3);
}
.server-info-advice code {
    font-size: 11px;
    background: var(--c-surface);
    padding: 1px 5px;
    border-radius: 3px;
    color: var(--c-text-2);
}
.admin-role-pill {
    display: inline-block;
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 2px 7px;
    border-radius: 3px;
    /* All rank tiers (GM / Officer / Core / Vanguard / Member /
       Alumni / guest) share the same gold pill. Per user — no need
       to color-code rank tiers; the label text alone is sufficient
       for differentiation, and the previous tier-specific colors
       (raider blue) clashed with class colors on the profile page. */
    color: #f2c94c;
    border: 1px solid #6a5a22;
    background: #2a2414;
    /* Don't let "GUILD MASTER" / "OFFICER ALT" / "LOOT MANAGER" break
       across lines inside the pill — keep each label as one chip. */
    white-space: nowrap;
}
/* Loot Manager — copper-amber matching the AR row HR badge + the
   roster money-bag icon. Only shown for raiders with the
   `lootManager` flag (gm / officer get loot rights inherently, no
   need to surface a redundant pill on their profile card). */
.admin-role-pill.loot-mgr { color: #e88c4d; border-color: rgba(232,140,77,0.45); background: rgba(232,140,77,0.10); }
.admin-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
.admin-label { font-size: var(--fs-xs); color: var(--c-text-3); letter-spacing: 0.04em; text-transform: uppercase; }
.admin-input, .admin-select {
    background: var(--c-bg);
    border: 1px solid #223049;
    color: var(--c-text);
    font-size: var(--fs-sm);
    line-height: 1.4;
    padding: 5px 13px;
    border-radius: 4px;
    outline: none;
    font-family: inherit;
    box-sizing: border-box;
}
.admin-select {
    padding-right: 28px;
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    cursor: pointer;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%23cfd9e6' d='M0 0l5 6 5-6z'/></svg>");
    background-repeat: no-repeat;
    background-position: right 10px center;
}
.admin-input:focus, .admin-select:focus { border-color: var(--c-accent); }
.admin-select:hover { border-color: var(--c-accent); cursor: pointer; }
.admin-btn-row { display: flex; gap: 8px; margin-top: 4px; flex-wrap: wrap; }
.admin-btn {
    background: transparent;
    border: 1px solid #2a3a5a;
    color: var(--c-text-2);
    font-size: var(--fs-sm);
    padding: 5px 12px;
    border-radius: 4px;
    cursor: pointer;
    font-family: inherit;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}
.admin-btn:hover { background: var(--c-border); }
.admin-btn.primary { color: var(--c-accent); border-color: #33E4DE44; }
.admin-btn.primary:hover { background: #12262a; }
.admin-btn.danger { color: var(--c-bad); border-color: #e8707044; }
.admin-btn.danger:hover { background: #2a1414; }
.admin-note { font-size: var(--fs-xs); color: #7a8aa0; margin-top: 4px; font-style: italic; }
.admin-pw-row {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 8px;
    align-items: center;
    padding: 7px 0;
    border-top: 1px solid var(--c-border);
}
.admin-pw-tier { display: inline-flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.admin-pw-tier .admin-pw-lastseen,
.admin-pw-tier .admin-pw-status { justify-self: auto; }
.admin-pw-row:first-child { border-top: none; }
.admin-pw-tier { font-size: var(--fs-sm); color: var(--c-text-2); font-weight: 600; }
.admin-subhead { font-size: var(--fs-xs); color: #aab4c2; letter-spacing: 0.04em; text-transform: uppercase; margin-top: 16px; margin-bottom: 4px; }
.admin-pw-list { max-height: 260px; overflow-y: auto; border: 1px solid var(--c-border); border-radius: 4px; background: #0b1220; }
/* Raider list specifically gets a taller max-height so the panel
   reaches the full height of the Backups card next to it in the
   admin grid (Backups caps at 456px). Officer + GM lists keep their
   compact 260px cap — they're typically short and look fine that way.
   The min-height ensures even a small raider roster doesn't leave
   the card visually collapsed. */
.admin-pw-list.admin-pw-list-raider {
    min-height: 200px;
    max-height: 360px;
}
.admin-pw-list .admin-pw-row { border-top: 1px solid var(--c-border); padding: 7px 10px; }
.admin-pw-list .admin-pw-row:first-child { border-top: none; }
/* Desktop: 4-column row — name | pills | meta | actions, all inline.
   The 1fr on the meta column eats slack so actions hug the right edge. */
#admin-backup-list .admin-pw-row {
    grid-template-columns: auto auto 1fr auto;
    align-items: center;
    gap: 8px;
}
#admin-backup-list .backup-row-name {
    color: var(--c-text-2);
    font-weight: 600;
    font-size: var(--fs-sm);
    white-space: nowrap;
}
#admin-backup-list .backup-row-pills {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
}
#admin-backup-list .backup-row-meta {
    color: var(--c-text-3);
    font-size: var(--fs-xs);
    white-space: nowrap;
}
#admin-backup-list .backup-row-actions {
    display: inline-flex;
    gap: 6px;
    justify-self: end;
}
/* Phone (≤520px): two rows.
     Row 1 — name (left) ─── Download/Restore (right)
     Row 2 — pills (left) ─── date · size (right) */
@media (max-width: 520px) {
    #admin-backup-list .admin-pw-row {
        grid-template-columns: 1fr auto;
        row-gap: 4px;
        align-items: center;
    }
    #admin-backup-list .admin-pw-row .backup-row-name {
        grid-column: 1; grid-row: 1; min-width: 0;
        overflow: hidden; text-overflow: ellipsis;
    }
    #admin-backup-list .admin-pw-row .backup-row-actions {
        grid-column: 2; grid-row: 1;
    }
    /* align-self: center on each row 2 child so the pills (which have
       padding+border vertical mass) and the meta text (no vertical
       chrome) sit on the same vertical center inside their shared row,
       not at the top edges of their respective cells. */
    #admin-backup-list .admin-pw-row .backup-row-pills {
        grid-column: 1; grid-row: 2; justify-self: start; align-self: center;
    }
    #admin-backup-list .admin-pw-row .backup-row-meta {
        grid-column: 2; grid-row: 2; justify-self: end; align-self: center; text-align: right;
    }
    /* Feedback inbox card on phones — admin header buttons (Refresh /
       Mark all read / Clear all) wrap to a new row underneath the
       title cluster; fb-entry padding tightens; fb-meta wraps cleanly
       so the iphash + timestamp don't overflow. */
    .admin-feedback-list { max-height: 60vh; }
    .admin-feedback-list .fb-entry {
        padding: 6px 8px;
    }
    .admin-feedback-list .fb-meta {
        gap: 6px;
        row-gap: 4px;
        font-size: var(--fs-micro);
    }
    .admin-feedback-list .fb-iphash {
        font-size: var(--fs-micro);
    }
    .admin-feedback-list .fb-message {
        font-size: var(--fs-xs);
    }
    /* The Feedback Inbox card's header (title + meta + buttons) is
       packed via .admin-header (flex). On phones, let the buttons
       wrap onto their own row instead of overflowing horizontally. */
    .admin-card .admin-header {
        flex-wrap: wrap;
        row-gap: 6px;
    }
}
/* Cap the backup list at ~12 rows so the card doesn't sprawl; overflow scrolls */
#admin-backup-list .admin-pw-list { max-height: 456px; }
.admin-pw-status { font-size: var(--fs-micro); padding: 2px 7px; border-radius: 3px; justify-self: start; display: inline-block; text-transform: uppercase; letter-spacing: 1px; font-weight: 600; }
.admin-pw-status.set   { color: var(--c-accent); background: #12262a; border: 1px solid #2a5a60; }
.admin-pw-status.unset { color: #7a8aa0; background: var(--c-border); border: 1px solid #2a3a5a; }
.admin-pw-lastseen { font-size: var(--fs-xs); color: var(--c-text-3); justify-self: start; font-style: italic; }
.admin-pw-lastseen.never { color: #7a8aa0; opacity: 0.7; }
.admin-pw-actions { display: inline-flex; gap: 6px; align-items: center; justify-self: end; flex-wrap: nowrap; }
.admin-version { font-size: var(--fs-sm); color: var(--c-text-3); }
.admin-version strong { color: var(--c-text); font-size: var(--fs-base); font-weight: 700; }
.admin-version-line { margin-bottom: 4px; }
.admin-version-stats { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--c-border); }
.vstats-loading { font-size: var(--fs-sm); color: var(--c-text-3); }
.vstats-header { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8px; }
.vstats-title { font-size: var(--fs-sm); font-weight: 700; color: var(--c-text-2); letter-spacing: 0.04em; text-transform: uppercase; }
.vstats-since { font-size: var(--fs-xs); color: var(--c-text-3); }
.vstats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 12px; }
.vstats-stat { background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: var(--r-sm); padding: 8px 10px; text-align: center; }
.vstats-num { font-size: var(--fs-lg); font-weight: 700; color: var(--c-text); line-height: 1.1; }
.vstats-label { font-size: var(--fs-xs); color: var(--c-text-3); margin-top: 3px; }
.vstats-chart { display: flex; align-items: flex-end; gap: 3px; height: 36px; padding: 4px 0; }
.vstats-bar { flex: 1; min-width: 0; background: var(--c-accent); opacity: 0.45; border-radius: 2px 2px 0 0; transition: opacity 0.15s; }
.vstats-bar:hover { opacity: 1; }
.vstats-bar.is-today { opacity: 0.9; }
.vstats-bar.is-today:hover { opacity: 1; }
.admin-version-author { margin-top: 6px; font-size: var(--fs-xs); color: var(--c-text-3); }
.admin-version-author a { color: var(--c-link); text-decoration: none; }
.admin-version-author a:hover { text-decoration: underline; }
/* Masked text input — looks like a password field but type="text" so password
   managers ignore it. Trade-off: no native a11y "password" semantics. */
input.fake-password {
    -webkit-text-security: disc;
    -moz-text-security: disc;
    text-security: disc;
    font-family: text-security-disc, -apple-system, sans-serif;
}

/* Avatar circles — used in the header and the Account welcome card. */
.avatar-circle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    overflow: hidden;
    flex-shrink: 0;
    color: #fff;
    font-weight: 700;
    text-transform: uppercase;
    background: #2a3648;
    border: 1px solid var(--c-border);
}
.avatar-circle img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar-circle.avatar-empty { background: #2a3648; }
#header-auth .ha-avatar { margin-right: 3px; cursor: pointer; }
#header-auth .ha-avatar:hover { box-shadow: 0 0 0 2px rgba(51, 228, 222, 0.5); }

.welcome-head { display: flex; align-items: center; gap: 14px; margin-bottom: 8px; flex-wrap: wrap; }
.welcome-edit-btns { margin-left: auto; align-self: center; flex-shrink: 0; display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
.welcome-edit-btn { flex-shrink: 0; }
@media (max-width: 600px) {
    /* On phones, drop the buttons below the avatar + name/pill cluster
       (was previously sharing row 1 + drifting behind the name). The
       100% basis forces them onto their own row; left-justify so they
       sit under the avatar instead of trailing right. */
    .welcome-edit-btns {
        flex-basis: 100%;
        margin-left: 0;
        margin-top: 10px;
        justify-content: flex-start;
        align-self: stretch;
    }
}
.welcome-head-text { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.welcome-head-text .welcome-role-line { margin-bottom: 2px; }
.welcome-head-text .admin-welcome { margin-bottom: 4px; }
.welcome-avatar-wrap { position: relative; display: inline-flex; flex-direction: column; align-items: center; gap: 6px; cursor: pointer; width: max-content; }
.welcome-avatar-wrap:hover .avatar-circle { box-shadow: 0 0 0 2px rgba(51, 228, 222, 0.5); }
.welcome-avatar-clear {
    position: absolute; top: -2px; right: -2px;
    width: 18px; height: 18px; padding: 0; box-sizing: border-box;
    font-size: 0; line-height: 0; cursor: pointer;
    border-radius: 50%; border: 1px solid rgba(232, 112, 112, 0.5);
    background: var(--c-surface-2); color: rgba(232, 112, 112, 0.85);
    opacity: 0; transition: opacity .12s ease;
}
.welcome-avatar-clear::before,
.welcome-avatar-clear::after {
    content: ""; position: absolute; top: 50%; left: 50%;
    width: 8px; height: 1.5px; background: currentColor; border-radius: 1px;
}
.welcome-avatar-clear::before { transform: translate(-50%, -50%) rotate(45deg); }
.welcome-avatar-clear::after  { transform: translate(-50%, -50%) rotate(-45deg); }
.welcome-avatar-wrap:hover .welcome-avatar-clear { opacity: 1; }
.welcome-avatar-clear:hover { color: #fff; background: var(--c-bad); border-color: var(--c-bad); }

.avatar-crop-modal {
    position: fixed; inset: 0; z-index: 220;
    background: rgba(0,0,0,0.55);
    align-items: flex-start; justify-content: center;
    padding: 6vh 16px;
}
.avatar-crop-modal .avatar-crop-card {
    background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: 8px;
    padding: 16px 18px;
    display: flex; flex-direction: column; gap: 12px; align-items: center;
    max-width: 340px; width: 100%;
}
.avatar-crop-modal .modal-header { display: flex; align-items: center; justify-content: space-between; width: 100%; }
.avatar-crop-modal .modal-title { font-size: var(--fs-xs); color: var(--c-accent); letter-spacing: 2px; text-transform: uppercase; }
.avatar-crop-modal .modal-close { background: none; border: none; color: var(--c-text-3); font-size: var(--fs-2xl); line-height: 1; cursor: pointer; padding: 0 4px; }
.avatar-crop-modal .modal-close:hover { color: var(--c-text); }
.avatar-crop-stage { position: relative; border-radius: var(--r-md); overflow: hidden; background: #000; user-select: none; touch-action: none; cursor: grab; }
.avatar-crop-stage:active { cursor: grabbing; }
.avatar-crop-canvas { display: block; width: 100%; height: 100%; }
.avatar-crop-mask { position: absolute; inset: 0; pointer-events: none; box-shadow: 0 0 0 9999px rgba(0,0,0,0.45) inset; border-radius: 50%; }
.avatar-crop-zoom { display: flex; align-items: center; gap: 10px; width: 100%; }
.avatar-crop-label { font-size: var(--fs-sm); color: var(--c-text-2); min-width: 40px; }
.avatar-crop-slider { flex: 1; }
/* Live zoom value pill on the right end of the slider so the user
   can see exactly what % they're at as they drag. tabular-nums keeps
   the width steady as the digit count changes (100% → 250% → 100%);
   a fixed min-width prevents the slider from resizing on update. */
.avatar-crop-zoom-value {
    font-size: var(--fs-sm);
    color: var(--c-text);
    font-variant-numeric: tabular-nums;
    min-width: 48px;
    text-align: right;
}
.avatar-crop-actions { display: flex; gap: 8px; justify-content: flex-end; width: 100%; }

.welcome-block { margin-bottom: 4px; }
.admin-welcome { font-size: var(--fs-xl); font-weight: 700; color: var(--c-text); margin-bottom: 4px; letter-spacing: .01em; }
.welcome-role-line { margin-bottom: 10px; }
.welcome-char-wrap { display: flex; gap: 18px; align-items: stretch; margin-bottom: 10px; padding: 8px 0; border-top: 1px solid var(--c-border); border-bottom: 1px solid var(--c-border); flex-wrap: wrap; }
.welcome-char { display: grid; grid-template-columns: auto 1fr; gap: 4px 14px; flex: 1 1 260px; min-width: 0; }
.welcome-char-row { display: contents; }
.welcome-char-key { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .08em; color: var(--c-text-faint); align-self: center; }
.welcome-char-val { font-size: var(--fs-sm); color: #cbd; }
.welcome-timeoff { flex: 0 1 220px; min-width: 160px; padding-left: 14px; border-left: 1px solid var(--c-border); display: flex; flex-direction: column; gap: 4px; }
.welcome-timeoff-key { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: .08em; color: var(--c-warn); margin-bottom: 2px; }
.welcome-timeoff-item { font-size: var(--fs-sm); color: #cbd; }
.welcome-timeoff-item.welcome-timeoff-rule { font-size: var(--fs-xs); color: var(--c-text-3); font-style: italic; }
@media (max-width: 640px) {
    .welcome-timeoff { padding-left: 0; border-left: 0; border-top: 1px solid var(--c-border); padding-top: 8px; flex: 1 1 100%; }
}

.flags-modal-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 14px; }
/* Segmented toggle: two tab-btns sharing a border, active half filled. */
.seg-toggle { display: inline-flex; }
.seg-toggle .seg-btn { border-radius: 0; }
.seg-toggle .seg-btn:first-child { border-top-left-radius: var(--r-sm); border-bottom-left-radius: var(--r-sm); }
.seg-toggle .seg-btn:last-child  { border-top-right-radius: var(--r-sm); border-bottom-right-radius: var(--r-sm); }
/* Every button after the first overlaps its left border with the
   previous button's right border via margin-left:-1px. Otherwise
   the two adjacent 1px borders stack to a visibly thicker (2px)
   line between segments. Earlier this only applied to :last-child,
   so a 3-segment toggle (TMB / Boosted / PlusOnes) had a thick
   seam between buttons 1↔2 even though 2↔3 was clean. */
.seg-toggle .seg-btn:not(:first-child) { margin-left: -1px; }
.seg-toggle .seg-btn:not(.is-active) { opacity: 0.55; }
.seg-toggle .seg-btn:not(.is-active):hover { opacity: 0.85; background: transparent; color: var(--c-accent); border-color: var(--c-accent); }
.seg-toggle .seg-btn.is-active,
.seg-toggle .seg-btn.is-active:hover { opacity: 1; background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); z-index: 1; position: relative; }

.tab-btn.flags-toggle { justify-content: flex-start; text-align: left; opacity: 0.55; }
/* OFF stays dim even while hovered — the canonical accent fill from
   .tab-btn:hover would otherwise make it indistinguishable from ON.
   Tiny opacity bump signals "still clickable" without flipping the state. */
.tab-btn.flags-toggle:hover { opacity: 0.75; background: transparent; color: var(--c-accent); border-color: var(--c-accent); }
.tab-btn.flags-toggle.is-on,
.tab-btn.flags-toggle.is-on:hover { opacity: 1; background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }
@media (max-width: 480px) {
    .flags-modal-grid { grid-template-columns: 1fr; }
}
.admin-welcome-perms { font-size: var(--fs-xs); color: var(--c-text-faint); margin-bottom: 10px; line-height: 1.5; }
.welcome-edit-hint { font-size: var(--fs-xs); color: var(--c-text-faint); margin-left: 4px; align-self: center; }
.welcome-edit-hint a { color: var(--c-link); text-decoration: none; border-bottom: 1px dotted #3a4a6a; }
.welcome-edit-hint a:hover { color: #9aaedf; }
@keyframes rowOwnFlash {
    0%   { background: rgba(51, 228, 222, 0.18); }
    100% { background: transparent; }
}
tr.row-own-flash { animation: rowOwnFlash 1.6s ease-out; }

.admin-version-whatsnew { display: inline-block; margin-left: 10px; font-size: var(--fs-sm); color: var(--c-link); text-decoration: none; border-bottom: 1px dotted #3a4a6a; }
.admin-version-whatsnew:hover { color: #9aaedf; border-bottom-color: var(--c-link); }

/* ── Backup tabs ─────────────────────────────────────────── */
.backup-tabs { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; }
.backup-tab {
    background: var(--c-surface-2); border: 1px solid var(--c-border); color: var(--c-text-3);
    font-family: inherit; font-size: var(--fs-xs); font-weight: 600; letter-spacing: .04em;
    padding: 5px 10px; border-radius: 4px; cursor: pointer; transition: all .15s;
}
.backup-tab:hover { border-color: var(--c-accent); color: #cfe; }
.backup-tab.active { background: #1a2a44; border-color: var(--c-accent); color: var(--c-accent); }
.backup-tab-count {
    display: inline-block; margin-left: 4px; padding: 0 5px;
    background: var(--c-surface); border-radius: 8px; font-size: var(--fs-micro); color: var(--c-text-3);
}
.backup-tab.active .backup-tab-count { background: var(--c-surface-2); color: var(--c-accent); }
.backup-tier-badge {
    font-size: var(--fs-micro); font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
    padding: 1px 6px; border-radius: 3px;
}
.backup-tier-hourly  { background: #1f3a2a; color: #7ed8a3; }
.backup-tier-daily   { background: #2a3149; color: #8fa6e6; }
.backup-tier-weekly  { background: #3a2a1f; color: #e6b07a; }
.backup-tier-monthly { background: #3a1f3a; color: #d68fe6; }
.backup-tier-yearly  { background: #3a1f1f; color: #e67a7a; }
.backup-mode-data    { background: #2a2438; color: #b59ce6; }

/* ── Changelog modal ─────────────────────────────────────── */
#changelog-modal {
    position: fixed; inset: 0; z-index: 200;
    background: rgba(0,0,0,0.55);
    display: none; align-items: flex-start; justify-content: center;
    padding: 6vh 16px;
}
.changelog-card {
    background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: 8px;
    padding: 18px 22px;
    max-width: 640px; width: 92%; max-height: 82vh;
    display: flex; flex-direction: column;
}
.changelog-card .modal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.changelog-card .modal-title { font-size: var(--fs-xs); color: var(--c-accent); letter-spacing: 2px; text-transform: uppercase; }
.changelog-card .modal-close { background: none; border: none; color: var(--c-text-3); font-size: var(--fs-2xl); line-height: 1; cursor: pointer; padding: 0 4px; }
.changelog-card .modal-close:hover { color: var(--c-text); }
.changelog-body { overflow-y: auto; padding: 4px 2px; }
.changelog-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0 10px; border-bottom: 1px solid var(--c-border); margin-bottom: 6px; }
.cl-chips, .cl-chips-right { display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* Simple / Detailed mode toggle in the toolbar. Two pill buttons —
   one of them is .is-on (active). Same shape as the type chips so
   the toolbar reads as a coherent control strip. */
.cl-mode-toggle { display: inline-flex; gap: 4px; }
.cl-mode-btn {
    font-size: var(--fs-xs);
    font-weight: 700;
    letter-spacing: .05em;
    text-transform: uppercase;
    padding: 4px 12px;
    border-radius: 999px;
    cursor: pointer;
    border: 1px solid var(--c-border);
    background: transparent;
    color: var(--c-text-3);
    transition: background .12s, color .12s, border-color .12s;
}
.cl-mode-btn:hover { color: var(--c-text); border-color: var(--c-text-3); }
.cl-mode-btn.is-on {
    background: rgba(51, 228, 222, 0.12);
    color: var(--c-accent);
    border-color: var(--c-accent);
}
/* Simple-mode row: version + date as the lead line, summary text
   below. No expand/collapse, no per-change badges. Purposefully quiet
   so a casual scroll-through reads at a glance. */
.cl-simple-row {
    padding: 10px 0;
    border-bottom: 1px solid var(--c-border);
}
.cl-simple-row:last-child { border-bottom: none; }
.cl-simple-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 4px;
}
.cl-simple-text {
    color: var(--c-text);
    font-size: var(--fs-sm);
    line-height: 1.55;
}
.cl-chip { font-size: var(--fs-micro); font-weight: 700; letter-spacing: .05em; text-transform: uppercase; padding: 3px 9px; border-radius: 999px; cursor: pointer; border: 1px solid var(--c-border); background: transparent; color: var(--c-text-3); transition: opacity .12s, background .12s, color .12s; }
.cl-chip:hover { color: var(--c-text); border-color: var(--c-text-3); }
.cl-chip.is-on { color: var(--c-bg); }
.cl-chip-type.cl-chip-added.is-on   { background: #7ed8a3; border-color: #7ed8a3; }
.cl-chip-type.cl-chip-changed.is-on { background: #8fa6e6; border-color: #8fa6e6; }
.cl-chip-type.cl-chip-fixed.is-on   { background: #e6b07a; border-color: #e6b07a; }
.cl-chip-type.cl-chip-removed.is-on { background: #e67a7a; border-color: #e67a7a; }
.cl-chip-type:not(.is-on) { opacity: 0.5; }
.cl-empty { padding: 18px 4px; color: var(--c-text-3); font-style: italic; font-size: var(--fs-sm); }

.changelog-version { padding: 4px 0; border-bottom: 1px solid #2a3142; }
.changelog-version:last-child { border-bottom: none; }
.changelog-version-head {
    display: flex; align-items: center; gap: 10px;
    width: 100%; padding: 8px 4px; cursor: pointer;
    background: transparent; border: none; color: inherit; text-align: left;
    border-radius: 4px;
}
.changelog-version-head:hover { background: rgba(255,255,255,0.03); }
.cl-caret { font-size: var(--fs-sm); color: var(--c-text-3); width: 12px; flex-shrink: 0; }
.cl-new-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--c-accent); flex-shrink: 0; box-shadow: 0 0 6px rgba(51,228,222,0.6); }
.changelog-version-num { font-size: 15px; font-weight: 700; color: var(--c-text); }
.changelog-version-date { font-size: var(--fs-xs); color: var(--c-text-3); }
.changelog-version-title { font-size: var(--fs-sm); color: #aab; font-style: italic; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cl-version-count { font-size: var(--fs-micro); color: var(--c-text-3); padding: 2px 7px; border-radius: 999px; background: var(--c-border); flex-shrink: 0; }
.changelog-list { list-style: none; margin: 0; padding: 4px 0 8px 22px; }
.changelog-list li { font-size: var(--fs-sm); color: #cbd; padding: 4px 0; line-height: 1.5; }
.changelog-badge { display: inline-block; font-size: var(--fs-micro); font-weight: 700; letter-spacing: .04em; text-transform: uppercase; padding: 1px 6px; border-radius: 3px; margin-right: 8px; vertical-align: middle; }
.changelog-badge-added   { background: #1f3a2a; color: #7ed8a3; }
.changelog-badge-changed { background: #2a3149; color: #8fa6e6; }
.changelog-badge-fixed   { background: #3a2a1f; color: #e6b07a; }
.changelog-badge-removed { background: #3a1f1f; color: #e67a7a; }

/* ── Role-based visibility ───────────────────────────────── */
/* .write-only shows for gm/officer only; .admin-only for gm only.
   .member-only shows for anyone signed in as a roster member
   (raider, officer, or gm) — hidden only for guests. Used by
   Roster's Edit-mode toggle, which raiders need too because they
   can edit their own row (gated per-row via editableRow). */
body.role-guest .write-only,
body.role-raider .write-only { display: none !important; }
body.role-guest .member-only { display: none !important; }
/* .loot-only shows for gm + officer + any raider whose roster row has the
   `lootManager` flag (toggled via the roster's money-bag icon). Gates
   Gargul TMB exports, per-raid TMB modals, AR HR controls, etc. */
body.role-guest .loot-only,
body.role-raider:not(.role-loot-manager) .loot-only { display: none !important; }
body:not(.role-gm) .admin-only { display: none !important; }

/* "Sign in with Discord" button + divider on the login form */
a.discord-signin-btn, a.discord-signin-btn:link, a.discord-signin-btn:visited { display:flex; align-items:center; justify-content:center; width:100%; box-sizing:border-box; background:#5865F2; border-color:#5865F2; color:#fff; text-decoration:none; margin-bottom:10px; }
a.discord-signin-btn:hover { background:#4752c4; border-color:#4752c4; color:#fff; }
/* Discord-blurple "Discord Announcement" button (Raids toolbar + the modal's
   confirm). Rides on .tab-btn for sizing; overrides colour to Discord's brand. */
.tab-btn.discord-announce-btn { display:inline-flex; align-items:center; gap:6px; background:#5865F2; border-color:#5865F2; color:#fff; }
.tab-btn.discord-announce-btn:hover:not(:disabled) { background:#4752c4; border-color:#4752c4; color:#fff; }
.tab-btn.discord-announce-btn:disabled { opacity:.6; }
.tab-btn.discord-announce-btn svg { flex-shrink:0; }
/* "Stand by" join-bar CTA — identical to the Join button: brand-teal
   (btn-confirm) outline + the same breathing glow. The "Clear stand by"
   set-state keeps the muted look (:not(.btn-muted) targets only the CTA). */
.tab-btn.comp-standby-btn:not(.btn-muted) { background: transparent; color: var(--c-accent); border-color: #33E4DE44; white-space: nowrap; letter-spacing: .04em; animation: comp-join-glow 2.4s ease-in-out infinite; }
.tab-btn.comp-standby-btn:not(.btn-muted):hover { background: var(--c-accent); border-color: var(--c-accent); color: var(--c-bg); }
@media (prefers-reduced-motion: reduce) { .tab-btn.comp-standby-btn:not(.btn-muted) { animation: none; } }
/* The muted "Clear maybe" button hovers in its own colour (blue) rather than
   the default btn-muted teal. "Clear stand by" is fine with the default teal
   (standby is teal now). */
.tab-btn.comp-maybe-btn.btn-muted:hover { background: var(--c-link); border-color: var(--c-link); color: var(--c-bg); }
/* "Actions" label to the left of the join-bar buttons. */
.raid-actions-label { font-size: var(--fs-xs); letter-spacing: .08em; text-transform: uppercase; color: var(--c-text-faint); font-weight: 700; margin-right: var(--sp-1); }
/* In-modal recurring-reminder lead-time chips (Announce raid modal). */
.dca-leads { display:flex; flex-wrap:wrap; gap:6px; }
.dca-lead { display:inline-flex; align-items:center; gap:5px; font-size:var(--fs-xs); color:var(--c-text-2); background:var(--c-surface-2); border:1px solid var(--c-border); border-radius:999px; padding:3px 10px; cursor:pointer; }
.dca-lead input { margin:0; }
.dca-lead-off { opacity:.4; }
/* "Configure Gargul" guide (Loot ▸ Gargul ▸ Setup, loot-master only) */
.gargul-guide { max-width: 900px; }
.gargul-guide-head h2 { margin: 0 0 6px; font-size: var(--fs-lg); }
.gargul-guide-lede { color: var(--c-text-3); font-size: var(--fs-sm); margin: 0 0 var(--sp-5); line-height: 1.5; }
.gargul-guide-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-5); }
.gargul-step { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-lg); padding: var(--sp-4) var(--sp-5); }
.gargul-step-head { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.gargul-step-num { flex: 0 0 auto; width: 24px; height: 24px; border-radius: 999px; background: var(--c-accent); color: #08211f; font-weight: 700; font-size: var(--fs-sm); display: inline-flex; align-items: center; justify-content: center; }
.gargul-step-title { font-size: var(--fs-md); color: var(--c-text); }
.gargul-step-body { color: var(--c-text-2); font-size: var(--fs-sm); line-height: 1.5; margin: 0 0 var(--sp-4); }
.gargul-shot { margin: 0; }
.gargul-guide-shot { display: block; max-width: 100%; height: auto; border: 1px solid var(--c-border); border-radius: var(--r-md); background: var(--c-surface-2); }
.gargul-shot figcaption { color: var(--c-text-faint); font-size: var(--fs-xs); margin-top: 6px; }
.gargul-guide-foot { color: var(--c-text-faint); font-size: var(--fs-xs); font-style: italic; margin-top: var(--sp-5); }
.admin-or { display:flex; align-items:center; gap:10px; margin:6px 0 12px; color:var(--c-text-3); font-size:var(--fs-xs); }
.admin-or::before, .admin-or::after { content:''; flex:1; height:1px; background:var(--c-border); }
/* Recruiting nudge under the login form — visitors who hit "Join this raid"
   land here signed out; point the ones who aren't members yet at the Join page. */
.admin-join-nudge { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--c-border); font-size: var(--fs-sm); color: var(--c-text-2); line-height: 1.5; }
.admin-join-nudge-title { font-weight: 700; font-size: var(--fs-md); color: var(--c-accent); margin-bottom: 3px; }
.admin-join-nudge a { color: var(--c-accent); text-decoration: none; border-bottom: 1px dotted #33E4DE66; }
.admin-join-nudge a:hover { border-bottom-color: var(--c-accent); }
/* While auth_status is still in flight we don't yet know the viewer's
   real role, so hide the app to avoid flashing write-UI that a stale
   client-side AUTH (sessionStorage) might render. Revealed by
   refreshAuthStatus() on first response. */
body.auth-pending { visibility: hidden; }
/* Highlight the signed-in player's own row so they immediately know
   where to edit their info. */
#roster-tbody tr.row-own > td {
    background: var(--c-surface);
    box-shadow: inset 2px 0 0 #4a7fb8;
}
#roster-tbody tr.row-own > td:first-child { box-shadow: inset 3px 0 0 #4a7fb8; }
/* GM's roster row is locked for officers — they can view but not change it. */
body:not(.role-gm) #roster-tbody tr.row-gm-locked select,
body:not(.role-gm) #roster-tbody tr.row-gm-locked input,
body:not(.role-gm) #roster-tbody tr.row-gm-locked button,
body:not(.role-gm) #roster-tbody tr.row-gm-locked .editable { pointer-events: none; opacity: 0.65; }
/* On the roster page, raiders can only edit their own row. Guests edit nothing.
   Disable inputs/selects/buttons in non-editable rows to communicate this. */
body.role-raider #roster-tbody tr:not(.row-own) select,
body.role-raider #roster-tbody tr:not(.row-own) input,
body.role-raider #roster-tbody tr:not(.row-own) button,
body.role-raider #roster-tbody tr:not(.row-own) .editable { pointer-events: none; opacity: 0.75; }
body.role-guest #roster-tbody tr select,
body.role-guest #roster-tbody tr input,
body.role-guest #roster-tbody tr button,
body.role-guest #roster-tbody tr .editable { pointer-events: none; opacity: 0.75; }
/* Buffs table: guests/raiders can't edit any buff cells either. */
body.role-guest #buffs-tbody tr select,
body.role-guest #buffs-tbody tr input,
body.role-guest #buffs-tbody tr .editable,
body.role-raider #buffs-tbody tr select,
body.role-raider #buffs-tbody tr input,
body.role-raider #buffs-tbody tr .editable { pointer-events: none; opacity: 0.75; }
/* When a <select> can't be edited (guest, or raider on a foreign row / buffs /
   roster rows they don't own), strip the input chrome so the element renders
   as plain text — no border, no background, no chevron. The admin page is
   excluded: its login-form selects are real dropdowns and need their chrome. */
body.role-guest #tab-roster select:not(.admin-select):not(.config-select),
body.role-guest #tab-buffs select:not(.admin-select):not(.config-select),
body.role-guest #tab-comp select:not(.admin-select):not(.config-select),
body.role-guest #tab-coverage select:not(.admin-select):not(.config-select),
body.role-guest #tab-attendance select:not(.admin-select):not(.config-select),
body.role-raider #roster-tbody tr:not(.row-own) select:not(.admin-select):not(.config-select),
body.role-raider #buffs-tbody tr select:not(.admin-select):not(.config-select) {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background: transparent;
    background-image: none;
    border: 0;
    box-shadow: none;
    padding: 0;
    color: inherit;
    opacity: 1;
}
body.role-guest #tab-roster select::-ms-expand,
body.role-guest #tab-buffs select::-ms-expand,
body.role-guest #tab-comp select::-ms-expand,
body.role-guest #tab-coverage select::-ms-expand,
body.role-guest #tab-attendance select::-ms-expand,
body.role-raider #roster-tbody tr:not(.row-own) select::-ms-expand,
body.role-raider #buffs-tbody tr select::-ms-expand { display: none; }
.prog-raid {
    background: var(--c-surface);
    border: 1px solid #223049;
    border-radius: 8px;
    padding: 14px 16px 16px 16px;
    position: relative;
}
/* Hover-revealed edit / clear buttons on the .prog-overall and
   .prog-raid cards (writer-only — non-writer renderers omit the markup
   entirely). Sit in the top-right corner; show only on hover so they
   don't compete with the boss tiles below for visual attention. */
.prog-bg-actions {
    position: absolute;
    top: 8px;
    right: 8px;
    display: flex;
    gap: 4px;
    opacity: 0;
    transition: opacity 0.15s;
    z-index: 2;
}
.prog-overall:hover .prog-bg-actions,
.prog-raid:hover .prog-bg-actions { opacity: 1; }
/* About-card mirrors the prog-bg-actions hover-reveal pattern so
   officers can swap or clear the background image right in place.
   Reuses .prog-bg-edit for the button styling (no need to invent
   new chrome). */
.about-bg-actions {
    position: absolute;
    bottom: 8px;
    right: 8px;
    display: flex;
    gap: 4px;
    opacity: 0;
    transition: opacity 0.15s;
    z-index: 2;
}
.about-card:hover .about-bg-actions { opacity: 1; }
.prog-bg-edit {
    width: 26px;
    height: 26px;
    padding: 0;
    border-radius: 4px;
    background: rgba(15, 22, 35, 0.85);
    border: 1px solid var(--c-border);
    color: var(--c-text-2);
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.prog-bg-edit:hover { background: var(--c-accent); color: var(--c-bg); border-color: var(--c-accent); }
.prog-raid-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 8px;
}
.prog-raid-title { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.prog-tier {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: 0.05em;
    color: #7a8aa0;
    background: var(--c-border);
    border: 1px solid #2a3a5a;
    padding: 2px 6px;
    border-radius: 3px;
}
.prog-raid-name { font-size: 15px; font-weight: 600; color: var(--c-text); }
.prog-raid-count { font-size: var(--fs-xs); color: var(--c-text-3); letter-spacing: 0.03em; }
.prog-raid-count-mobile, .prog-raid-actions-mobile { display: none; }
.prog-raid-title-row { display: inline-flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.prog-raid-bar {
    height: 4px;
    background: var(--c-border);
    border-radius: 2px;
    overflow: hidden;
    margin-bottom: 12px;
}
.prog-raid-fill {
    height: 100%;
    background: var(--c-accent);
    transition: width 0.25s ease;
}
.prog-boss-grid {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}
.prog-boss-grid .prog-boss { flex: 0 0 96px; }
.prog-boss {
    cursor: pointer;
    text-align: center;
    user-select: none;
    padding: 6px 4px;
    border-radius: 6px;
    transition: background 0.15s ease;
}
.prog-boss:hover { background: var(--c-border); }
.prog-portrait-wrap {
    position: relative;
    /* Sizes path: 56 → 67 (+20%) → 60 (fit-11-on-one-row reduction)
       → 68 (room-to-grow bump now that the row had whitespace right
       of Nightbane). .prog-boss flex-basis path 84 → 100 → 88 → 96. */
    width: 68px;
    height: 68px;
    margin: 0 auto 6px auto;
    border-radius: 6px;
    overflow: hidden;
    background: var(--c-border);
    border: 2px solid #3a4a6a;
    box-sizing: border-box;
}
.prog-portrait {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    /* Brightness path: 0.55 → 0.8 → 0.96 → 1.15 → 1.04 (last step is
       a 10% step-down per user since 1.15 was washing out highlights).
       Values >1 brighten beyond source; grayscale stays at 100% so
       alive bosses still read as desaturated vs the .is-dead full-color
       treatment. */
    filter: grayscale(100%) brightness(1.04);
    transition: filter 0.2s ease;
}
.prog-boss.is-dead .prog-portrait-wrap {
    border-color: var(--c-accent);
    box-shadow: 0 0 0 1px #33E4DE44;
}
.prog-boss.is-dead .prog-portrait {
    filter: none;
}
.prog-portrait-edit {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 18px;
    height: 18px;
    padding: 0;
    border-radius: 50%;
    border: 1px solid var(--c-border);
    background: rgba(0, 0, 0, 0.6);
    color: var(--c-text-2);
    font-size: 11px;
    line-height: 1;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.15s ease, color 0.15s ease, border-color 0.15s ease;
    z-index: 2;
}
.prog-portrait-wrap:hover .prog-portrait-edit { opacity: 1; }
.prog-portrait-edit:hover { color: var(--c-accent); border-color: var(--c-accent); }
.prog-name {
    font-size: var(--fs-xs);
    color: #aab4c2;
    line-height: 1.25;
    min-height: 28px;
}
.prog-boss.is-dead .prog-name { color: var(--c-text); }
.prog-note {
    font-size: 9px;
    color: #7a8aa0;
    font-style: italic;
    margin-top: 2px;
}
.prog-video-badge {
    position: absolute;
    bottom: 2px;
    right: 2px;
    width: 16px;
    height: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 9px;
    color: #fff;
    background: rgba(200, 40, 40, 0.88);
    border-radius: 3px;
    text-decoration: none;
    line-height: 1;
    border: 1px solid rgba(0,0,0,0.35);
}
.prog-video-badge:hover { background: rgba(230, 60, 60, 1); }
.prog-resist-badge {
    position: absolute;
    top: 2px;
    left: 2px;
    width: 18px;
    height: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-xs);
    color: var(--c-bg);
    background: rgba(51, 228, 222, 0.92);
    border-radius: 3px;
    text-decoration: none;
    line-height: 1;
    border: 1px solid rgba(0,0,0,0.35);
    cursor: pointer;
}
.prog-resist-badge:hover { background: rgba(51, 228, 222, 1); }
/* Loot badge — sits at top-right of the boss portrait when there's
   non-voided award history for that boss. Number reads at-a-glance
   ("how many drops have we awarded off this boss?"). Click opens the
   per-boss loot modal. Treasure-chest yellow so it doesn't collide
   with resist-cyan or video-red. */
.prog-loot-badge {
    position: absolute;
    bottom: 2px;
    left: 2px;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-xs);
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--c-bg);
    background: rgba(232, 200, 84, 0.92);
    border-radius: 3px;
    text-decoration: none;
    line-height: 1;
    border: 1px solid rgba(0,0,0,0.35);
    cursor: pointer;
}
.prog-loot-badge:hover { background: rgba(232, 200, 84, 1); }
.prog-resources {
    margin-top: 14px;
    padding-top: 10px;
    border-top: 1px solid #223049;
}
.resources-modal-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    border-bottom: 1px solid #223049;
    margin: 6px 0 10px 0;
    padding-bottom: 6px;
}
.resources-modal-tab {
    background: transparent;
    color: var(--c-text-3);
    border: 1px solid #223049;
    padding: 5px 10px;
    border-radius: 4px;
    font-size: var(--fs-xs);
    letter-spacing: 0.03em;
    cursor: pointer;
    font-family: inherit;
    white-space: nowrap;
}
.resources-modal-tab:hover { color: var(--c-text-2); border-color: #33E4DE55; }
.resources-modal-tab.active { color: var(--c-bg); background: var(--c-accent); border-color: var(--c-accent); font-weight: 600; }
.resources-modal-tab.has-content::after { content: ' •'; color: var(--c-accent); }
.resources-modal-tab.active.has-content::after { color: var(--c-bg); }
.resources-modal-body { overflow: auto; flex: 1 1 auto; padding-right: 4px; }
.prog-resources-toggle {
    background: transparent;
    color: var(--c-text-3);
    border: 0;
    padding: 4px 0;
    font-size: var(--fs-xs);
    letter-spacing: 0.03em;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.prog-resources-toggle:hover { color: var(--c-text-2); }
/* Anchor variant of the toggle (Dungeon Journal link) — kill the
   default underline so it reads as a button. The empty modifier
   dims the label when the journal has no notes/videos yet. */
a.prog-resources-toggle { text-decoration: none; }
.prog-resources-toggle-empty { opacity: 0.55; font-style: italic; }
.prog-resources-toggle-empty:hover { opacity: 0.85; }
.prog-resources-caret { font-size: var(--fs-micro); }
.prog-resources-body { margin-top: 10px; }
.prog-resources-label {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: 0.05em;
    color: #7a8aa0;
    text-transform: uppercase;
    margin-bottom: 6px;
}
.prog-resources-notes-view {
    font-size: var(--fs-md);
    color: var(--c-text-2);
    line-height: 1.55;
    white-space: pre-wrap;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 5px;
    padding: 10px 12px;
}
.prog-resources-notes-edit {
    width: 100%;
    box-sizing: border-box;
    font-size: var(--fs-md);
    color: var(--c-text);
    background: var(--c-surface-2);
    border: 1px solid #2a3a5a;
    border-radius: 5px;
    padding: 9px 11px;
    font-family: inherit;
    line-height: 1.55;
    resize: vertical;
    min-height: 70px;
}
.prog-resources-fields {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 12px 16px;
}
.prog-resources-fields .prog-resources-field.full {
    grid-column: 1 / -1;
}
@media (max-width: 820px) {
    .prog-resources-fields { grid-template-columns: 1fr; }
}
.prog-resources-field { display: flex; flex-direction: column; }
.prog-resources-field-label {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: 0.08em;
    color: #8ca0be;
    text-transform: uppercase;
    margin-bottom: 5px;
}
.prog-resources-sections { display: flex; flex-direction: column; gap: 10px; }
.prog-resources-section-label {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: 0.08em;
    color: var(--c-accent);
    text-transform: uppercase;
    margin-bottom: 4px;
}
.prog-resources-raidwide {
    margin-bottom: 14px;
    padding: 10px 12px;
    background: var(--c-surface);
    border-left: 3px solid var(--c-accent);
    border-radius: 4px;
}
.prog-resources-raidwide-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 6px;
}
.prog-resources-raidwide-label {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: 0.08em;
    color: var(--c-accent);
    text-transform: uppercase;
}
.prog-resources-raidwide-dismiss {
    background: transparent;
    border: 1px solid #2a3a5a;
    color: #9aa8bb;
    border-radius: 4px;
    width: 22px;
    height: 22px;
    line-height: 1;
    font-size: var(--fs-base);
    cursor: pointer;
    padding: 0;
}
.prog-resources-raidwide-dismiss:hover { background: var(--c-border); color: var(--c-text); }
.prog-resources-raidwide-edit {
    margin-bottom: 14px;
    padding: 10px 12px;
    background: var(--c-surface);
    border-left: 3px solid var(--c-accent);
    border-radius: 4px;
    display: flex;
    flex-direction: column;
}
.prog-resources-preset {
    margin-top: 6px;
    margin-bottom: 10px;
    display: flex;
    justify-content: flex-end;
}
.prog-resources-preset-btn {
    font-size: var(--fs-xs);
    color: var(--c-accent);
    background: transparent;
    border: 1px dashed var(--c-accent);
    border-radius: 4px;
    padding: 4px 10px;
    cursor: pointer;
}
.prog-resources-preset-btn:hover { background: var(--c-surface); }
.prog-resources-empty {
    font-size: var(--fs-xs);
    color: #6b7788;
    font-style: italic;
}
/* Trash editor (CC + mobs + packs editor inside the Trash Mobs tab) */
.prog-resources-trash-summary { list-style: none; padding: 0; margin: 8px 0 0; display: flex; gap: 16px; font-size: var(--fs-sm); color: var(--c-text-2); }
.prog-resources-trash-summary li { padding: 4px 10px; background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: var(--r-sm); }
.trash-editor { display: flex; flex-direction: column; gap: var(--sp-3); margin-top: var(--sp-3); }
.trash-edit-section { background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: var(--r-md); padding: var(--sp-3); }
.trash-edit-section[open] { padding-bottom: var(--sp-3); }
.trash-edit-section > summary { cursor: pointer; font-size: var(--fs-md); font-weight: 600; color: var(--c-text); list-style: none; padding: 4px 0; user-select: none; }
.trash-edit-section > summary::before { content: '▸'; margin-right: 8px; color: var(--c-text-faint); transition: transform .15s; display: inline-block; }
.trash-edit-section[open] > summary::before { transform: rotate(90deg); }
.trash-edit-section > summary::-webkit-details-marker { display: none; }
.trash-section-count { font-size: var(--fs-xs); color: var(--c-text-3); font-weight: 400; margin-left: 6px; padding: 1px 8px; background: var(--c-surface); border: 1px solid var(--c-border); border-radius: 999px; }
.trash-section-hint { font-size: var(--fs-xs); color: var(--c-text-3); margin: 4px 0 8px; }
.trash-rows { display: flex; flex-direction: column; gap: var(--sp-2); margin: var(--sp-2) 0; }
.trash-empty-hint { font-size: var(--fs-xs); color: var(--c-text-faint); font-style: italic; padding: 4px 0; }
/* Generic row card */
.trash-cc-row, .trash-mob-row, .trash-pack-row {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-sm);
    padding: 8px 10px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.trash-cc-row-head, .trash-mob-row-head, .trash-pack-row-head {
    display: flex;
    gap: 6px;
    align-items: center;
}
.trash-cc-row-head input, .trash-mob-row-head input, .trash-pack-row-head input,
.trash-cc-row-head select {
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    color: var(--c-text);
    padding: 4px 8px;
    border-radius: var(--r-sm);
    font-family: inherit;
    font-size: var(--fs-sm);
    flex: 1;
    min-width: 0;
}
.trash-cc-row .trash-cc-class { flex: 0 0 auto; min-width: 100px; font-weight: 700; }
.trash-mob-row .trash-mob-warn { flex: 0 0 auto; min-width: 130px; color: var(--c-bad); font-weight: 700; }
.trash-cc-row textarea, .trash-mob-row textarea, .trash-pack-row textarea {
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    color: var(--c-text);
    padding: 6px 8px;
    border-radius: var(--r-sm);
    font-family: inherit;
    font-size: var(--fs-sm);
    width: 100%;
    box-sizing: border-box;
    resize: vertical;
}
.trash-row-rm {
    background: transparent;
    border: 1px solid var(--c-border);
    color: var(--c-text-faint);
    cursor: pointer;
    width: 26px;
    height: 26px;
    border-radius: var(--r-sm);
    font-size: var(--fs-md);
    line-height: 1;
    flex-shrink: 0;
}
.trash-row-rm:hover { background: var(--c-bad); color: #fff; border-color: var(--c-bad); }
/* Pack mob-checkbox chips */
.trash-pack-mob-list { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 0; }
.trash-pack-mob-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 8px;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: var(--r-sm);
    font-size: var(--fs-xs);
    cursor: pointer;
    user-select: none;
}
.trash-pack-mob-chip input { margin: 0; }
.trash-pack-mob-chip:has(input:checked) { background: rgba(51, 228, 222, 0.12); border-color: var(--c-accent); color: var(--c-accent); }
.prog-resources-boss {
    padding: 24px 0;
    border-top: 1px solid var(--c-border);
}
.prog-resources-boss:first-child { border-top: 0; padding-top: 4px; }
.prog-resources-boss-name {
    font-size: var(--fs-md);
    font-weight: 700;
    color: var(--c-text);
    margin-bottom: 10px;
}
.prog-resources-video-link {
    display: inline-block;
    margin-top: 6px;
    font-size: var(--fs-sm);
    color: var(--c-accent);
    text-decoration: none;
}
.prog-resources-video-link:hover { text-decoration: underline; }
.prog-resources-video-field {
    margin-top: 12px;
    display: flex;
    flex-direction: column;
}
.prog-resources-video-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.prog-resources-video-row {
    display: flex;
    gap: 6px;
    align-items: stretch;
}
.prog-resources-video-input {
    flex: 1;
    box-sizing: border-box;
    font-size: var(--fs-sm);
    color: var(--c-text);
    background: var(--c-surface-2);
    border: 1px solid #2a3a5a;
    border-radius: 4px;
    padding: 6px 10px;
    font-family: inherit;
}
.prog-resources-video-remove {
    background: transparent;
    border: 1px solid #2a3a5a;
    color: #9aa8bb;
    border-radius: 4px;
    width: 28px;
    font-size: var(--fs-base);
    cursor: pointer;
    padding: 0;
    line-height: 1;
}
.prog-resources-video-remove:hover { background: var(--c-border); color: var(--c-text); }
.prog-resources-video-add {
    align-self: flex-start;
    margin-top: 8px;
    font-size: var(--fs-xs);
    color: var(--c-accent);
    background: transparent;
    border: 1px dashed var(--c-accent);
    border-radius: 4px;
    padding: 4px 10px;
    cursor: pointer;
}
.prog-resources-video-add:hover { background: var(--c-surface); }
.prog-resources-video-links {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin-top: 6px;
}
.prog-resources-actions {
    margin-top: 10px;
    display: flex;
    gap: 8px;
}
.prog-resources-btn {
    font-size: var(--fs-xs);
    color: var(--c-text-2);
    background: var(--c-border);
    border: 1px solid #2a3a5a;
    border-radius: 4px;
    padding: 5px 10px;
    cursor: pointer;
}
.prog-resources-btn:hover { background: #22304c; }
.prog-resources-btn.primary {
    background: var(--c-accent);
    color: #0a1020;
    border-color: var(--c-accent);
    font-weight: 600;
}
.prog-resources-btn.primary:hover { background: #5FEDE8; }

/* ── Responsive: tablet (≤820px) ─────────────────────────────── */
@media (max-width: 820px) {
    .prog-raid-grid { grid-template-columns: 1fr; }
    .prog-raid { flex: 1 1 100% !important; }
    .admin-grid { grid-template-columns: 1fr; }
    .sched-card .sched-raid-row { grid-template-columns: 210px 1fr auto auto; gap: 14px; }
    .coverage-groups { grid-template-columns: repeat(2, 1fr); }
    .cfg-box { max-width: 92vw; min-width: 0; }
    #schedule-modal .cfg-box,
    #softres-modal .cfg-box,
    #away-modal .cfg-box,
    #flags-modal .cfg-box,
    #notes-modal .cfg-box { max-width: 92vw; }
    .pug-box { max-width: 92vw; min-width: 0; }
    .config-select { min-width: 140px; max-width: 60vw; }
    /* Resist modal: let the tall body scroll, let wide tables scroll
       sideways instead of blowing past the viewport. */
    #ssc-resist-modal { align-items: flex-start; padding: 16px 0; overflow-y: auto; }
    #ssc-resist-modal .pug-box { max-height: none; padding: 14px; width: 94vw; }
    #ssc-resist-modal .pug-box { overflow-x: hidden; }
    .resist-table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }
    .resist-table-scroll table { font-size: var(--fs-xs) !important; width: max-content; min-width: 100%; }
    .resist-table-scroll th, .resist-table-scroll td,
    .resist-table-scroll td *, .resist-table-scroll th * { font-size: var(--fs-xs) !important; white-space: nowrap; }
    #ssc-resist-body input[type="text"], #ssc-resist-body select { min-width: 130px; }
    #ssc-resist-body input[type="number"] { min-width: 50px; }
}

/* Desktop/all-viewport rule: table overflow is handled by its own wrapper, not the modal. */
#ssc-resist-modal .pug-box { overflow-x: hidden; }
.resist-table-scroll { overflow-x: auto; max-width: 100%; }
.resist-table-scroll table { width: max-content; min-width: 100%; }

/* ── Responsive: tablet-only (521px–820px) ──────────────────────
   At tablet widths, collapse the SR cell on the schedule's upcoming
   raid rows down to just the link (drop the COPY URL / EDIT / token
   buttons). Otherwise the SR would wrap to its own row, making the
   upcoming cells visibly taller than past cells (which never had SR
   content). Writers can still tap the raid name to open the config
   modal and edit from there. The phone rule (≤520px) has its own
   multi-row layout that explicitly positions the buttons + token,
   so we scope this to (min-width: 521px) to avoid stomping on it. */
@media (min-width: 521px) and (max-width: 820px) {
    .sched-card .sched-raid-row .sched-sr {
        grid-template-columns: auto;
        gap: 0;
        font-size: var(--fs-xs);
    }
    .sched-card .sched-raid-row .sched-sr-btn,
    .sched-card .sched-raid-row .sched-sr-btn-slot,
    .sched-card .sched-raid-row .sched-sr-token-cell { display: none; }
}

/* ── Responsive: phone (≤520px) ──────────────────────────────── */
@media (max-width: 520px) {
    /* Body padding zeroed horizontally on phones — gives every content
       section the full viewport width minus card padding. Vertical
       padding (4px top/bottom) stays so the header doesn't kiss the
       browser chrome. The header-wrap negative margin matches so the
       white header card still extends edge-to-edge. */
    body { padding: 4px 0; font-size: var(--fs-md); }
    /* Header pill flush against the brand strip — drop the 4px
       top padding (was creating a visible gap between the strip's
       glow and the white pill on mobile). Margin-top:-4px still
       negates the body's 4px top padding so the wrap edge sits
       at viewport y=0. */
    .header-wrap { margin: -4px 0 8px 0; padding: 0 8px 0; }
    /* Long-name avatar hide: when the signed-in user's character
       name has 10+ chars (e.g. Iveseenworst), the avatar circle
       crowds the logo on the multi-row mobile header. Hide the
       avatar specifically — name + Sign out remain so the user can
       still navigate / sign out. JS toggles body.long-username via
       renderHeaderAuth(). Short names (Achtungg, Bearlf) keep the
       avatar visible. */
    body.long-username .ha-avatar,
    body.long-username #header-auth > a:has(.ha-avatar) {
        display: none;
    }
    /* Mobile body padding is 4px 0 (zero horizontal), so the desktop
       -0.75rem margin would over-extend past the viewport. Match
       the header-wrap's mobile inset instead. */
    /* margin-bottom 4px (not 8px) so the notice's bottom edge sits
       flush against the white header card. Header-wrap on mobile
       carries margin-top:-4px to neutralize body's 4px top padding;
       paired with the notice's positive 4px margin-bottom they net
       to 0, eliminating the visible page-bg strip the user reported
       between the orange fade-out and the header. (8px was leaving
       a 4px dark gap there.) */
    .site-notice { margin: -4px 0 4px 0; padding: 11px 14px; }
    /* Mobile layout: [NOTICE pill] [text — left-aligned, takes
       remaining width, line-clamped to 2 lines] [CTA — pushed right
       via margin-left:auto]. Drop the centered alignment used on
       desktop; with limited width, left-aligned text + right-aligned
       button reads as a clear "label · content · action" row. The
       line-clamp on the message keeps a long string from pushing the
       row 4+ lines tall — overflow ellipsizes after line 2. */
    .site-notice .site-notice-inner {
        justify-content: flex-start;
        text-align: left;
        flex-wrap: nowrap;
        gap: 8px;
    }
    /* Drop the NOTICE pill on mobile — severity is already telegraphed
       by the brand strip's tint above the banner, and on a narrow row
       the badge competes with the message + CTA for horizontal space. */
    .site-notice .site-notice-badge { display: none; }
    .site-notice .site-notice-message {
        /* flex:1 1 0 + min-width:0 lets the message shrink below its
           intrinsic content width, which is what enables the
           line-clamp ellipsis to actually fire. webkit-box + line-clamp
           is still the only cross-browser-ish way to get hard 2-line
           cap with ellipsis. */
        flex: 1 1 0;
        min-width: 0;
        text-align: left;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
        font-size: var(--fs-sm);
        line-height: 1.35;
    }
    .site-notice .site-notice-cta { margin-left: auto; }
    /* Drop the carousel dots on mobile entirely — every px of the
       narrow row matters, and users get the same affordance via the
       swipe gesture wired up in setupNoticeSwipe(). The banner
       advertises swipeability via touch-action:pan-y and a subtle
       grab cursor. */
    .site-notice .site-notice-dots { display: none; }
    .site-notice { touch-action: pan-y; cursor: grab; }
    .site-notice:active { cursor: grabbing; }
    /* Card horizontal padding tightened on phones — 16px each side →
       8px each side reclaims another 16px of usable content width
       across the schedule / progression / attendance / recruit /
       admin cards. Combined with the body padding cut, ~24px more
       horizontal room per content section than the desktop padding
       provided when carried over to mobile. */
    .sched-card,
    .prog-overall,
    .recruit-card,
    .admin-card,
    .at-section,
    .at-panel { padding-left: 8px; padding-right: 8px; }
    /* Tab content wrappers: 8px horizontal padding on phones so the
       content cards' outer edges align with the header pill + the
       View As impersonation card. Both of those sit at x=8..vw-8
       because .header-wrap has padding:4px 8px 0 — matching that
       inset on the content wrappers means cards line up with the
       header chrome above them instead of running 8px wider on each
       side. Was 0/0 (let cards run edge-to-edge), then user feedback:
       cards looked wider than the header, content area should match
       the header + View As width. Cards still have their own ~8px
       internal padding for text inside, so visible item rows still
       have breathing room from the card border. */
    .tab-content-wrap,
    .loot-tab-wrap { padding-left: 8px; padding-right: 8px; }
    /* Raid Builder mobile inset is bigger than the loot/join 8px
       baseline. Same CSS rule produced visibly different widths in
       practice — loot's wishlist content has its own internal
       padding (`.wl-raid { padding: var(--sp-3) var(--sp-4) }` etc.)
       and the rendered chip rows leave natural empty space on the
       right, so the 8px wrap inset reads wider visually. Raid's
       subnav extras stretches edge-to-edge with no internal padding,
       so at 8px the cluster felt full-width. var(--sp-6) (24px,
       matching the desktop padding) gives the raid wrap a more
       generous visible inset so its outer edges read narrower than
       the header pill above it. User went 8px (too wide) → 16px
       (closer) → not enough → 24px feels right. */
    .raid-tab-wrap { padding-left: var(--sp-6); padding-right: var(--sp-6); }
    /* Raid Builder subnav on phones — invert desktop order so config
       controls (Select Raid + buttons) sit on row 1, and the sub-tab
       buttons (Group Editor / Buff Coverage / Buff Editor) drop to
       row 2 BELOW a horizontal divider line. Per user feedback: this
       reads more naturally on a narrow viewport since config controls
       are the primary action and the sub-tab switch is secondary.
       The horizontal divider is the config-controls row's
       border-bottom; inline subnav dividers (the vertical 1px bars)
       hide on mobile since they don't belong in a stacked layout. */
    .raid-subnav {
        flex-direction: column;
        align-items: stretch;
        /* Three rows + one mid-list divider on mobile:
             row 1 = config controls (Select Raid + +New / Edit / Delete)
             row 2 = comp-sr-bar      (Loot System: Vis Maior / Softres…)
             ─── divider ───
             row 3 = sub-tab buttons  (Group Editor / Buff Coverage / Buff Editor)
           Divider lives on row 2's bottom border (negative-margin'd
           edge-to-edge so it spans the full subnav width regardless
           of the comp-sr-bar's natural content width). */
        gap: var(--sp-2);
        /* Bumped from sp-3 → sp-5 per user — more breathing room
           between the sub-tab buttons (last subnav row) and the
           comp tab content below them. */
        margin-bottom: var(--sp-5);
        border-bottom: none;
        padding-bottom: 0;
    }
    .raid-subnav .raid-subnav-divider { display: none; }
    .raid-subnav-extras#raid-config-controls {
        order: 1;
        margin-left: 0;
        flex-wrap: wrap;
    }
    /* Hide Copy URL button on mobile — the share-link affordance
       isn't critical on a phone and the row reads cleaner with just
       Select Raid + +New / Edit / Delete. */
    .raid-subnav-extras#raid-config-controls #copy-link-btn {
        display: none !important;
    }
    /* Force Delete (.btn-danger) to the END of the config-controls
       row even if the source order rearranges or extras prepend new
       buttons. order:99 puts it after every default-order child. */
    .raid-subnav-extras#raid-config-controls .tab-btn.btn-danger {
        order: 99;
    }
    .raid-subnav #comp-sr-bar {
        order: 2;
        /* Reset the inline `display: inline-flex; gap: 8px` from
           the partial so the row breathes and centers properly on
           mobile. flex with justify-content:center horizontally
           centers the "Loot System:" label + provider name +
           buttons cluster. row-gap 8px gives space when the row
           wraps to multiple lines on narrow viewports; column-gap
           14px puts a little air between the label, provider, and
           buttons. align-items:center keeps button + text baselines
           aligned even at different intrinsic heights. */
        display: flex !important;
        flex-wrap: wrap;
        justify-content: center;
        align-items: center;
        gap: 8px 14px;
        padding: var(--sp-2) 0 var(--sp-3) 0;
        border-bottom: 1px solid var(--c-border);
    }
    /* Empty comp-sr-bar (no active config) collapses to nothing —
       kill the padding + border so a placeholder row doesn't
       leave an orphan divider line above the sub-tabs. */
    .raid-subnav #comp-sr-bar:empty {
        padding: 0;
        border-bottom: none;
        display: none !important;
    }
    .raid-subnav .raid-subnav-tabs {
        order: 3;
        /* 2×2 grid with EQUAL-WIDTH buttons. 1fr × 1fr columns +
           buttons stretching to fill their cell make all four tabs
           the same width regardless of label length, so "Buff
           Coverage" lines up under "Group Editor" instead of
           hanging asymmetrically wider. The grid itself is capped
           via max-width and centered so it doesn't span edge-to-
           edge on tablet widths. Per user — without uniform widths
           the 2×2 read as crooked. */
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: var(--sp-2);
        max-width: 360px;
        margin-left: auto;
        margin-right: auto;
        /* Match the comp-sr-bar's padding-bottom above the divider
           (sp-3 = 12px) on this side too — without explicit padding
           the tab buttons would only get the parent's flex `gap`
           (sp-2 = 8px) which read as visibly tighter than the space
           above the divider. */
        padding-top: var(--sp-3);
    }
    .raid-subnav .raid-subnav-tabs > .tab-btn {
        width: 100%;
    }
    /* Header splits: white card holds only logo/nav + sign-in on row 1;
       tabs + config controls drop below it on transparent rows. */
    .header {
        display: grid;
        grid-template-columns: 1fr auto auto;
        gap: 0;
        background: transparent;
        border: none;
        padding: 0;
        position: relative;
    }
    .header > .mobile-nav-toggle {
        grid-column: 2; grid-row: 1;
        align-self: stretch;
        background: #ffffff;
        border: 1px solid #dde1e7;
        border-left: none; border-right: none;
        border-radius: 0;
        /* Tightened from 6px 10px → 5px 8px (saves ~4px horizontal,
           freeing space for longer character names in the auth
           pill on the right). */
        padding: 5px 8px;
        margin: 0 -1px;
    }
    /* Row 1 = the white card. Paint white + border on the two row-1 children
       with no gap between them so they form one continuous pill. */
    .header > .logo-wrap {
        grid-column: 1; grid-row: 1;
        align-self: stretch;
        background: #ffffff;
        border: 1px solid #dde1e7;
        border-right: none;
        border-radius: 8px 0 0 8px;
        /* Tighter logo-wrap padding (10 → 6 right) shaves another
           4px so 9-char names like Greybusch fit without crowding. */
        padding: 6px 6px 6px 10px;
        min-width: 0;
        gap: 6px;
        align-items: center;
    }
    .header > #header-auth {
        grid-column: 3; grid-row: 1;
        align-self: stretch;
        background: #ffffff;
        border: 1px solid #dde1e7;
        border-left: none;
        border-radius: 0 8px 8px 0;
        /* padding-right matches #header-impersonation (10px) so the
           LOGOUT button's right edge lines up with the View As
           dropdown's right edge below it. padding-left tightened to
           7px (away from the avatar) since there's no symmetry
           constraint there — Logo pill is on that side, not an
           aligning element. */
        padding: 6px 10px 6px 7px;
        margin-left: -1px;
        display: flex;
        align-items: center;
    }
    /* Shrink the avatar slightly + drop its right margin on mobile
       so the name has more room. ha-avatar is rendered at 26×26 by
       avatarHtml(); CSS-scaling it down to 22 saves 4px width and
       another 3px margin = 7px back to the name. */
    #header-auth .ha-avatar {
        width: 22px !important;
        height: 22px !important;
        margin-right: 0;
    }
    /* Tighten the gap inside #header-auth (avatar → ha-user → sign
       out) so the cluster compacts without changing per-element
       sizes further. */
    .header > #header-auth { gap: 6px; }
    .header > .tabs {
        grid-column: 1 / -1;
        grid-row: 2;
        margin: 8px 0 0 !important;
        justify-content: flex-start;
        flex-wrap: wrap;
        gap: 6px;
    }
    .header > #header-sep { display: none; }
    .header > #config-controls {
        grid-column: 1 / -1;
        grid-row: 3;
        flex-wrap: wrap;
        gap: 6px;
        margin-top: 4px;
    }
    /* Kill the inline vertical separators inside config-controls */
    .header > #config-controls > div[style*="width:1px"] { display: none; }
    /* "View as" impersonation picker — on mobile, drop it onto its
       own row below the header card (where it actually fits) instead
       of trying to squeeze in alongside the logo + auth pill. Only
       renders for GM / officer; raiders + guests still get
       :empty { display: none }. Dark surface matches the rest of the
       page chrome (was white pill) so it reads as app content, not
       header brand chrome. */
    .header > #header-impersonation {
        grid-column: 1 / -1;
        grid-row: 4;
        margin-top: 6px;
        margin-left: 0;
        padding: 6px 10px;
        background: var(--c-surface);
        border: 1px solid var(--c-border);
        border-radius: 8px;
        flex-wrap: wrap;
        color: var(--c-text-2);
    }
    .header > #header-impersonation .ha-imp-label {
        color: var(--c-text-faint);
    }
    .header > #header-impersonation .ha-imp-select {
        flex: 1 1 auto;
        max-width: none;
        background-color: var(--c-bg);
        color: var(--c-text);
        border-color: var(--c-border);
        /* Dark background on mobile → light caret. */
        background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%23cfd9e6' d='M0 0l5 6 5-6z'/></svg>");
    }
    .header > #header-impersonation .ha-imp-select:hover {
        background-color: var(--c-surface-2);
    }
    .header > #header-impersonation .ha-imp-select.is-active {
        background-color: rgba(232, 140, 77, 0.18);
        border-color: rgba(232, 140, 77, 0.55);
        color: #f0a86c;
    }

    .logo-img { height: 48px; }
    .subtitle { display: none; }
    .config-select {
        min-width: 0; max-width: none; flex: 1 1 auto;
        font-size: var(--fs-sm); padding: 4px 28px 4px 10px;
        background-color: var(--c-bg);
        color: var(--c-text);
        border-color: #223049;
        background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%23cfd9e6' d='M0 0l5 6 5-6z'/></svg>");
    }
    .config-select option { color: var(--c-text); background: var(--c-bg); }

    /* Comp / Raids */
    .comp-grid { grid-template-columns: 1fr; gap: 8px; }
    .group-card { min-width: 0; }
    /* Hide inactive groups on mobile (only the active ones for the raid size) */
    .group-card.group-disabled { display: none; }
    /* Softres toolbar bar wraps to its own line instead of crowding the Raid/Buffs row */
    .buff-vis-bar { flex-wrap: wrap; row-gap: 6px; }
    #comp-sr-bar {
        margin-left: 0 !important;
        flex-basis: 100%;
        order: 99;
        flex-wrap: wrap;
    }

    /* Coverage */
    .coverage-groups { grid-template-columns: 1fr; }
    .coverage-top { grid-template-columns: 1fr !important; }
    #missing-list { grid-template-columns: 1fr; }

    /* Progression */
    .prog-phase-grid { grid-template-columns: repeat(2, 1fr); }
    .prog-boss-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .prog-raid-grid { grid-template-columns: 1fr; gap: 12px; }
    .prog-overall-header { flex-direction: column; align-items: flex-start; gap: 2px; }
    .prog-raid-header { flex-direction: row; align-items: flex-start; gap: 10px; }
    .prog-raid-title { flex: 1 1 auto; flex-direction: column; align-items: flex-start; gap: 2px; }
    .prog-raid-count-desktop { display: none !important; }
    .prog-raid-count-mobile { display: inline; }
    .prog-raid-actions-mobile { display: flex; align-items: flex-start; }
    .recruit-contacts { grid-template-columns: 1fr; }
    /* Keep "Contact Us" left and "Join our Discord" right on row 1 even
       on phones — was previously stacked column on mobile per user, now
       reverted so the discord CTA pins top-right alongside the title. */
    .recruit-header { flex-direction: row; align-items: center; gap: 8px; }
    .recruit-header > *:not(.recruit-title) { align-self: auto; }

    /* Schedule card rows on mobile — 2-column grid. The Loot row
       controls (Export TMB + EDIT button) were dropped per user
       since they cluttered the schedule. What's left:
         col 1 (left)              col 2 (right)
         row 1: date               [N/M available — vertically
         row 2: raid name           centered, spans both rows]
       Avail spans both rows with align-self:center so it reads as
       centered between the date + raid-name lines on the left
       (instead of sticking to row 1 or row 2). */
    .sched-card .sched-raid-row {
        display: grid;
        grid-template-columns: 1fr auto;
        grid-template-rows: auto auto;
        column-gap: 12px;
        row-gap: 4px;
        padding: 10px 12px;
        border-bottom: 1px solid var(--c-border);
        align-items: center;
    }
    .sched-card .sched-raid-row:last-child { border-bottom: 0; }
    .sched-card .sched-raid-date { grid-column: 1; grid-row: 1; min-width: 0; }
    .sched-card .sched-raid-row .sched-raid-name { grid-column: 1; grid-row: 2; justify-self: start; min-width: 0; }
    .sched-card .sched-raid-row .sched-raid-avail {
        grid-column: 2;
        grid-row: 1 / 3;
        align-self: center;
        justify-self: end;
    }

    /* Past raids list — Completed pill spans both rows on the right
       and vertically centers against the date + raid name block on
       the left (per user feedback that the pill read as misaligned
       when locked to row 1 only).
       col 1 (left)            col 2 (right)
       row 1: date             [Completed pill — vertically
       row 2: raid name         centered, spans both rows]            */
    .sched-card .sched-raid-list-past .sched-raid-row {
        grid-template-columns: 1fr auto;
        grid-template-rows: auto auto;
        column-gap: 12px;
        row-gap: 2px;
        align-items: center;
    }
    .sched-card .sched-raid-list-past .sched-raid-row .sched-raid-date { grid-column: 1; grid-row: 1; }
    .sched-card .sched-raid-list-past .sched-raid-row .sched-raid-name { grid-column: 1; grid-row: 2; justify-self: start; }
    .sched-card .sched-raid-list-past .sched-raid-row .sched-raid-done {
        grid-column: 2;
        grid-row: 1 / 3;
        align-self: center;
        justify-self: end;
    }

    /* Modals & cards */
    .cfg-box { padding: 16px 14px; min-width: 0; }
    .pug-box { padding: 14px 16px; min-width: 0; }

    /* Tables: let them scroll horizontally instead of breaking layout */
    table { font-size: var(--fs-xs); }
    .tab-panel { overflow-x: auto; }
    /* Loot tab uses a responsive grid (no wide tables), so let it stay
       overflow:visible — `overflow-x: auto` silently sets overflow-y too,
       which would break `position: sticky` on the loot filter strip. */
    #tab-loot { overflow: visible; }
    /* Same exception for the unified Raid Builder tab. The Group
       Editor + Buff Coverage views use responsive grids (no wide
       tables), and the .raid-subnav doesn't need horizontal scroll
       either — overflow-x:auto on the parent was visibly affecting
       how the inner wrap's mobile padding rendered, making content
       run wider than the loot tab at the same viewport. Matches
       #tab-loot behavior. */
    #tab-raidbuilder { overflow: visible; }
    /* Admin tab needs overflow:visible too — its .admin-subnav uses
       position:sticky to stay pinned at the top of long pages, and
       overflow-x:auto on the parent breaks sticky positioning
       (sticky needs a non-overflow ancestor as its scrollport).
       No wide tables on the admin page so dropping the auto-scroll
       has no downside. */
    #tab-admin { overflow: visible; }

    /* Buttons & dropdowns: tighten */
    .tab-btn { padding: 5px 10px; font-size: var(--fs-xs); letter-spacing: 0.5px; }

    /* Roster tab: tighten cell padding and let the table scroll horizontally
       so full values (Protection, Tank, Warrior, etc.) stay readable. */
    #tab-roster .at-table { width: auto; min-width: 100%; }
    #tab-roster .at-table th,
    #tab-roster .at-table td { padding: 6px 6px; font-size: var(--fs-sm); white-space: nowrap; }
    #tab-roster .at-panel { overflow-x: auto; }
    /* Attendance audit rows on phones: the desktop grid (130 90 1fr 140)
       totals 360+ px which overflows a 320-360 px viewport, pushing the
       "X below T" cell off-screen. Restructure to 2 rows:
         row 1: [rank label] [meets/total]   ─── [X below T]
         row 2: [progress bar — full width]
       The bar gets its own row so it has real horizontal room to read,
       and the "X below T" text right-aligns next to the meets/total
       chip on row 1 instead of getting clipped. !important needed to
       override the inline grid-template-columns. */
    #rank-audit-body .rank-audit-row {
        grid-template-columns: auto auto 1fr !important;
        column-gap: 8px !important;
        row-gap: 4px !important;
    }
    #rank-audit-body .rank-audit-row .ra-label   { grid-column: 1; grid-row: 1; }
    #rank-audit-body .rank-audit-row .ra-counts  { grid-column: 2; grid-row: 1; }
    #rank-audit-body .rank-audit-row .ra-below   { grid-column: 3; grid-row: 1; justify-self: end; }
    #rank-audit-body .rank-audit-row .ra-bar     { grid-column: 1 / -1; grid-row: 2; }

    /* Attendance leaderboard on phones: drop the Class column (col 3).
       The player name in col 2 is already class-coloured so the class
       label was redundant chrome that was forcing the table to overflow
       horizontally on narrow viewports. Tighten cell padding + font
       so the remaining four columns (Rank · Player · Points · Last)
       fit cleanly on a 320-360px viewport. */
    #tab-attendance .at-table th:nth-child(3),
    #tab-attendance .at-table td:nth-child(3) { display: none; }
    #tab-attendance .at-table th,
    #tab-attendance .at-table td { padding: 6px 8px; font-size: var(--fs-sm); }
    #tab-attendance .at-panel { overflow-x: auto; }
    #tab-roster .roster-select { width: auto; min-width: 0; }

    /* Roster summary: full-width sections on mobile so Players card matches Roles/Classes */
    #roster-summary .rsm-section { width: 100%; flex: 1 1 100% !important; }

    /* Attendance recorder: 2-column grid on narrow viewports so 25m's 5 groups
       don't turn into a super-tall stack */
    .at-rec-groups {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 10px;
    }
    .at-rec-group { min-width: 0; }

    /* Modal text inputs full-width */
    .cfg-box input, .cfg-box select, .cfg-box textarea { font-size: var(--fs-base); }

    /* Attendance / roster headings wrap */
    h1, h2, h3 { word-wrap: break-word; }
}

/* ── Loot tab ──────────────────────────────────────────────── */
/* `.tab-content-wrap` is the shared content container — same width +
   padding the Loot tab has used. Other tabs (Attendance, Schedule, …)
   opt in by wrapping their content in it so the slimmer-than-header
   look matches across the app.
   Layout uses flex-column with gap so sibling sections sit at
   consistent vertical spacing AND hidden children (e.g. .write-only
   `display: none` for non-writers) drop out completely — no empty
   margin gap remains above the next visible section. */
.tab-content-wrap {
    padding: var(--sp-5) var(--sp-6) 0;
    max-width: 1280px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
}
.loot-tab-wrap { padding: var(--sp-5) var(--sp-6) 0; max-width: 1280px; margin: 0 auto; }

/* ── Pug-loot reservation public page ─────────────────────────
   Activated by `?pug=<token>`. The whole page is replaced with this
   minimal picker — no nav, no header, just the loot list. The page
   has its own boxed card so it reads as "this is a one-off task,
   not a full app". Same dark surface palette as the rest of the
   app for visual continuity in case the pug is the same person who
   sees the public site too. */
.pug-reserve-page {
    min-height: 100vh;
    background: var(--c-bg);
    color: var(--c-text);
    font-family: 'Trebuchet MS', sans-serif;
    box-sizing: border-box;
    /* Disable browser scroll-anchoring on the pug page. The sticky
       hero collapses 200px → ~56px when stuck, which shrinks the
       document above the user's scroll position. Without this,
       browsers compensate by adjusting scrollY automatically, which
       moves the IntersectionObserver sentinel back across the
       threshold and re-triggers the toggle — exactly the jitter the
       user reported. With anchoring off, the document just shifts
       and scrollY stays put. */
    overflow-anchor: none;
}
.pug-reserve-page * { overflow-anchor: none; }
.pug-reserve-frame {
    max-width: 880px;
    margin: 0 auto;
    padding: var(--sp-5) var(--sp-6);
    box-sizing: border-box;
}
.pug-reserve-card {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 8px;
    padding: 22px 26px 26px;
}
/* Hero banner above the picker. Uses the matching raid's bg image
   from /assets/raids/<slug>.jpg with a strong dark gradient overlay
   so the title + name + counter stay readable. Mirrors the journal
   raid-banner so the pug page reads as part of the same site. The
   inline style on the element fills in the actual `background:`
   declaration; the rules here just shape the box. */
.pug-hero {
    border-radius: 8px;
    border: 1px solid var(--c-border);
    overflow: hidden;
    /* Bottom padding leaves room for the absolutely-positioned
       .pug-hero-help line to sit at the bottom-left of the banner. */
    padding: 22px 28px 56px;
    margin-bottom: var(--sp-4);
    min-height: 200px;
    display: block;
    box-sizing: border-box;
    position: sticky;
    /* Stay below the page's brand-strip glow (body::before is 2px
       fixed-position at top) plus a comfortable gap so the sticky
       banner doesn't visually fight with it. */
    /* Sit flush at viewport top (covers viewport rows 0+). The
       brand-strip glow (body::before) has a higher z-index so it
       paints ABOVE the banner — picker rows can't bleed through
       the 2px brand-strip area because the banner now covers it,
       and the glow renders on top of the banner top edge. */
    top: 0;
    z-index: 100;
    /* Hint the compositor we'll be animating padding + min-height +
       title size; together with the cubic-bezier this smooths the
       expand/collapse from the scroll listener. */
    will-change: padding, min-height;
    transition: padding 280ms cubic-bezier(0.22, 0.61, 0.36, 1),
                min-height 280ms cubic-bezier(0.22, 0.61, 0.36, 1),
                box-shadow 280ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.pug-hero-help {
    position: absolute;
    bottom: 14px;
    left: 28px;
    right: 220px; /* leave space for the counter pinned top-right */
    margin: 0;
    color: var(--c-text-2);
    font-size: var(--fs-sm);
    text-shadow: 0 1px 3px rgba(0,0,0,.5);
    transition: opacity 0.18s ease, max-height 0.22s ease;
}
/* Compacted state — applied by the scroll listener once the user
   scrolls past the hero's natural position. Keeps just the title +
   counter visible so the sticky banner doesn't dominate the
   viewport while the user is deep in the picker. Content inside the
   pug-hero-inner gets vertically centered (flex column with
   justify-content: center) so the title + name row read as a
   single tight band rather than top-aligned with empty space below. */
.pug-hero.pug-hero-stuck {
    padding: 8px 28px;
    min-height: 56px;
    display: flex;
    align-items: center;
    /* Drop shadow appears ONLY when the banner is stuck — lifts the
       compact header off the picker content scrolling beneath.
       Doubled-up shadow (close inner + softer outer) so it stays
       visible even at the moments when the banner moves up by 2-3px
       per scroll frame and the eye loses the soft outer alone. */
    box-shadow:
        0 2px 4px rgba(0, 0, 0, 0.6),
        0 8px 22px rgba(0, 0, 0, 0.55);
}
.pug-hero.pug-hero-stuck .pug-hero-inner {
    display: flex;
    flex-direction: column;
    justify-content: center;
    flex: 1;
    min-width: 0;
}
.pug-hero.pug-hero-stuck .pug-hero-eyebrow { display: none; }
.pug-hero.pug-hero-stuck .pug-hero-help { opacity: 0; pointer-events: none; max-height: 0; overflow: hidden; }
.pug-hero.pug-hero-stuck .pug-hero-title { font-size: var(--fs-lg); margin: 0 0 2px; }
.pug-hero.pug-hero-stuck .pug-hero-name-row { gap: 8px; margin: 0; }
.pug-hero.pug-hero-stuck .pug-header-name { font-size: var(--fs-sm); }
.pug-hero.pug-hero-stuck .pug-class-pill { font-size: var(--fs-micro); padding: 1px 6px; }
/* Smoother shared transition curve for the inner pieces (title size,
   name-row spacing, etc.) so they scale down in unison with the
   container's padding/min-height transition above. */
.pug-hero .pug-hero-title,
.pug-hero .pug-hero-name-row,
.pug-hero .pug-header-name,
.pug-hero .pug-class-pill,
.pug-hero .pug-hero-eyebrow,
.pug-hero .pug-hero-help {
    transition: font-size 280ms cubic-bezier(0.22, 0.61, 0.36, 1),
                padding 280ms cubic-bezier(0.22, 0.61, 0.36, 1),
                margin 280ms cubic-bezier(0.22, 0.61, 0.36, 1),
                opacity 200ms ease,
                max-height 280ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
@media (max-width: 540px) {
    /* Match the mobile hero padding (left: 16px) so the help line
       aligns with the eyebrow / title / name above it. Was sitting
       at 28px which made it visibly indented from everything else. */
    .pug-hero-help { left: 16px; right: 16px; bottom: 12px; font-size: var(--fs-xs); }
}
/* Counter chip pinned to the top-right of the hero banner so it
   reads as "X/Y reserved" right where the user expects to see
   their reservation count. The hero is sticky → counter rides
   along for the scroll. */
.pug-hero-counter-pin {
    position: absolute;
    top: 14px;
    right: 14px;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 6px 12px;
    background: rgba(15, 22, 35, 0.75);
    border: 1px solid var(--c-accent);
    border-radius: 6px;
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    z-index: 2;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
}
.pug-hero-counter-pin .pug-reserved-label {
    color: var(--c-accent);
    text-transform: uppercase;
    letter-spacing: .8px;
    font-weight: 700;
    font-size: var(--fs-micro);
    text-shadow: 0 1px 3px rgba(0,0,0,.5);
}
.pug-hero-counter-pin .pug-reserved-counts {
    display: inline-flex;
    gap: 6px;
    flex-wrap: wrap;
    font-size: var(--fs-xs);
    color: var(--c-text-2);
}
.pug-hero-counter-pin .pug-counter-raid {
    background: transparent;
    border: 0;
    padding: 0;
}
.pug-hero-counter-pin .pug-counter-raid-name {
    color: var(--c-text-faint);
    font-size: var(--fs-micro);
}
.pug-hero-inner { position: relative; z-index: 1; }
.pug-hero-eyebrow {
    color: var(--c-accent);
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: 1.2px;
    font-weight: 700;
    margin-bottom: 6px;
    text-shadow: 0 1px 4px rgba(0,0,0,.6);
}
.pug-hero-title {
    margin: 0 0 var(--sp-3);
    font-size: var(--fs-2xl);
    color: var(--c-text);
    line-height: 1.2;
    text-shadow: 0 2px 10px rgba(0,0,0,.7);
}
.pug-hero-name-row {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    flex-wrap: wrap;
    margin-bottom: var(--sp-2);
}
.pug-hero-name-row .pug-header-name {
    color: var(--cls, var(--c-text));
    font-weight: 700;
    font-size: var(--fs-lg);
    text-shadow: 0 1px 6px rgba(0,0,0,.6);
}
.pug-hero-counter-row {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-2);
    margin-bottom: var(--sp-2);
}
.pug-hero-counter-row .pug-counter-raid {
    background: rgba(15,22,35,.78);
    border-color: var(--c-border);
}
.pug-hero-help {
    margin: var(--sp-3) 0 0;
    color: var(--c-text-2);
    font-size: var(--fs-sm);
    text-shadow: 0 1px 3px rgba(0,0,0,.5);
}
.pug-hero-help code {
    background: rgba(15,22,35,.85);
    padding: 1px 5px;
    border-radius: 3px;
    color: var(--c-accent);
    border: 1px solid var(--c-border);
}
.pug-reserve-footer {
    text-align: center;
    margin-top: var(--sp-5);
    color: var(--c-text-faint);
    font-size: var(--fs-xs);
}
.pug-reserve-footer .pug-reserve-home {
    color: var(--c-accent);
    text-decoration: none;
    border-bottom: 1px dotted currentColor;
}
.pug-reserve-footer .pug-reserve-home:hover { opacity: .8; border-bottom-style: solid; }

/* Brand-strip glow on the pug page reflects loot completion state.
   The body class is set/refreshed by _pugReserveRender on every
   render. !important is needed because the default body::before
   gradient is fully specified above. Animation pulses the glow
   alpha while progress is incomplete (empty / partial); goes solid
   green once all reservation slots are filled. */
@keyframes pugGlowPulse {
    0%, 100% { opacity: 0.55; }
    50% { opacity: 1; }
}
body.pug-loot-empty::before {
    background: linear-gradient(
        90deg,
        transparent 0%,
        rgba(232, 112, 112, 0.45) 5%,
        rgba(232, 112, 112, 0.95) 30%,
        rgba(255, 140, 140, 1.0) 50%,
        rgba(232, 112, 112, 0.95) 70%,
        rgba(232, 112, 112, 0.45) 95%,
        transparent 100%
    ) !important;
    box-shadow: 0 0 18px 3px rgba(232, 112, 112, 0.55) !important;
    animation: pugGlowPulse 1.4s ease-in-out infinite;
}
body.pug-loot-partial::before {
    background: linear-gradient(
        90deg,
        transparent 0%,
        rgba(212, 170, 68, 0.45) 5%,
        rgba(212, 170, 68, 0.95) 30%,
        rgba(245, 195, 90, 1.0) 50%,
        rgba(212, 170, 68, 0.95) 70%,
        rgba(212, 170, 68, 0.45) 95%,
        transparent 100%
    ) !important;
    box-shadow: 0 0 18px 3px rgba(212, 170, 68, 0.55) !important;
    animation: pugGlowPulse 1.8s ease-in-out infinite;
}
body.pug-loot-full::before {
    /* VM brand green (--c-accent #33E4DE) — same teal/green that
       carries every other "go" cue across the app. Solid ribbon at
       full picks; no pulse. */
    background: linear-gradient(
        90deg,
        transparent 0%,
        rgba(51, 228, 222, 0.45) 5%,
        rgba(51, 228, 222, 0.95) 30%,
        rgba(102, 237, 232, 1.0) 50%,
        rgba(51, 228, 222, 0.95) 70%,
        rgba(51, 228, 222, 0.45) 95%,
        transparent 100%
    ) !important;
    box-shadow: 0 0 18px 3px rgba(51, 228, 222, 0.55) !important;
    animation: none;
    opacity: 1;
}

/* Header — title left, per-raid pick counter right-aligned. */
.pug-header-top {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--sp-4);
    flex-wrap: wrap;
}
.pug-header-title {
    margin: 0;
    font-size: var(--fs-xl);
    line-height: 1.25;
    color: var(--c-text);
}
.pug-header-counter {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    flex-wrap: wrap;
    padding-top: 4px;
    color: var(--c-text-2);
    font-size: var(--fs-sm);
    white-space: nowrap;
}
.pug-counter-raid {
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 4px;
    padding: 4px 10px;
    line-height: 1.2;
}
.pug-counter-raid-name {
    color: var(--c-text-faint);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    font-size: var(--fs-xs);
    margin-right: 4px;
}
.pug-counter-raid strong { color: var(--c-accent); font-weight: 700; }
/* Sub-header — pug name + class pill on a row, both class-coloured. */
.pug-header-sub {
    margin-top: 8px;
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}
.pug-header-name {
    color: var(--cls, var(--c-text));
    font-weight: 600;
    font-size: var(--fs-base);
}
.pug-class-pill {
    color: var(--cls, var(--c-text-2));
    border: 1px solid var(--cls, var(--c-border));
    background: rgba(255, 255, 255, 0.03);
    border-radius: 999px;
    padding: 1px 10px;
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-weight: 600;
}
.pug-header-help {
    margin: 12px 0 0;
    color: var(--c-text-faint);
    font-size: var(--fs-sm);
}
.pug-section { margin-top: var(--sp-5); }
.pug-section + .pug-section { margin-top: var(--sp-6); }
/* Pug-page section header. Boss portrait + raid/boss labels + an
   optional per-raid counter pill. Mirrors the wishlist editor's
   .wl-boss-head pattern so the pug page reads as part of the same
   loot-pick UI family. */
.pug-section-title {
    margin-bottom: 8px;
    display: flex;
    align-items: center;
    gap: 12px;
}
/* Boss portrait — same dimensions + radius as the wishlist editor
   so the artwork looks identical wherever it appears. */
.pug-section-thumb-wrap { flex-shrink: 0; line-height: 0; }
.pug-section-thumb-wrap .wl-boss-thumb {
    width: 44px;
    height: 44px;
    border-radius: var(--r-md);
    object-fit: cover;
    border: 1px solid var(--c-border);
    background: var(--c-surface-2);
}
.pug-section-titles {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    line-height: 1.2;
}
.pug-section-raid {
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-weight: 700;
    color: var(--c-accent);
}
.pug-section-boss {
    font-size: var(--fs-md);
    color: var(--c-text);
    font-weight: 600;
    text-transform: none;
    letter-spacing: 0;
}
.pug-section-count {
    margin-left: auto;
    color: var(--c-text-2);
    font-size: var(--fs-xs);
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 999px;
    padding: 1px 8px;
}

/* App-update banner — shown when the deployed app.js mtime moves past
   what the open tab booted with. Sticks to the bottom-right corner so
   it's noticeable without blocking interaction. */
#app-update-banner {
    position: fixed;
    bottom: 18px;
    right: 18px;
    z-index: 9999;
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--c-surface);
    border: 1px solid var(--c-accent);
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
    border-radius: 6px;
    padding: 12px 16px;
    color: var(--c-text);
    font-size: var(--fs-sm);
    max-width: calc(100vw - 36px);
}
#app-update-banner span { color: var(--c-text); }
@media (max-width: 520px) {
    #app-update-banner { left: 12px; right: 12px; bottom: 12px; flex-wrap: wrap; }
    /* Roster toolbar divider sits between "+ Add Player" and the search
       input on desktop. On phones the row is flex-wrap:wrap and the
       search has wrapped to line 2 — leaving the divider stranded on
       line 1 next to the button with nothing to divide. Hide it. */
    .roster-toolbar-divider { display: none; }
}

/* Quick-check straggler pill — flagged when one or more comp members
   haven't reserved any loot for the active raid. Same warm-amber treatment
   used elsewhere in the app for "needs attention" callouts. */
.qc-no-picks-pill {
    font-size: var(--fs-xs);
    color: var(--c-warn);
    background: rgba(230, 176, 122, 0.10);
    border: 1px solid rgba(230, 176, 122, 0.45);
    padding: 3px 10px;
    border-radius: 999px;
    cursor: default;
    white-space: nowrap;
}
.pug-section-rows { display: flex; flex-direction: column; gap: 4px; }
/* Item cell holds the name + an inline contested "(X)" bubble on the same
   row. The name ellipsizes; the bubble never shrinks. */
.pug-row-item { display: flex; align-items: center; gap: 8px; }
.pug-row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Pick button + the contested "(N MS/OS)" bubbles share the right-hand cell,
   with the bubbles sitting to the RIGHT of the Pick button. */
.pug-row-actions { display: inline-flex; align-items: center; gap: 8px; justify-self: end; }
.pug-contest-pill { flex-shrink: 0; cursor: default; }
.pug-row {
    display: grid;
    grid-template-columns: 1fr 110px auto;
    align-items: center;
    gap: 10px;
    padding: 6px 10px;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 4px;
    transition: border-color 0.1s;
}
.pug-row.picked {
    border-color: var(--c-accent);
    background: rgba(51, 228, 222, 0.08);
}
.pug-row.won { border-color: var(--c-good); background: rgba(120, 200, 120, 0.10); }
.pug-won-badge { margin-left: 8px; font-size: var(--fs-xs); font-weight: 600; color: var(--c-good); white-space: nowrap; }
.pug-locked-banner {
    margin: 0 0 14px; padding: 10px 12px; border-radius: 6px;
    background: rgba(232, 140, 77, 0.12); border: 1px solid var(--c-warn);
    color: var(--c-text-2); font-size: var(--fs-sm);
}
.pug-row-item { min-width: 0; }
.pug-row-slot {
    font-size: var(--fs-xs);
    color: var(--c-text-3);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    text-align: right;
}
@media (max-width: 540px) {
    .pug-reserve-frame { padding: var(--sp-3); }
    .pug-reserve-card { padding: 16px 14px 18px; }
    .pug-hero { padding: 18px 16px 16px; min-height: 160px; }
    .pug-hero-title { font-size: var(--fs-xl); }
    .pug-row { grid-template-columns: 1fr auto; }
    .pug-row-slot { display: none; }
    /* Counter pill on phones — drop the "Loot " prefix and the
       per-raid name so it reads tight: "RESERVED 0/3". */
    .pug-hero-counter-pin .pug-reserved-label-loot { display: none; }
    .pug-hero-counter-pin .pug-counter-raid-name { display: none; }
    /* Recruit head on phones — keep "Join Vis Maior" left and the
       Discord button right on a single row. nowrap + min-width: 0
       on the h3 so a long title shrinks rather than wrapping the
       button onto a second line. */
    .pug-recruit-head { flex-wrap: nowrap; gap: var(--sp-2); }
    .pug-recruit-head h3 { flex: 1 1 auto; min-width: 0; font-size: var(--fs-base); }
    .pug-recruit-head .pug-recruit-cta { flex: 0 0 auto; }
}

/* Recruitment soft-sell at the bottom of the pug-loot reservation page.
   Sits below the picker so the pug sees it after they've made their
   picks. Subtle accent border so it reads as guild-branded without
   shouting. */
.pug-recruit-card {
    margin-top: var(--sp-6);
    padding: 18px 22px;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-left: 3px solid var(--c-accent);
    border-radius: 6px;
}
.pug-recruit-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-3);
    flex-wrap: wrap;
    margin-bottom: 10px;
}
.pug-recruit-head h3 { margin: 0; font-size: var(--fs-lg); color: var(--c-text); }
.pug-recruit-cta {
    background: var(--c-accent);
    color: var(--c-bg);
    text-decoration: none;
    font-weight: 600;
    padding: 6px 14px;
    border-radius: 4px;
    font-size: var(--fs-sm);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    transition: background 0.1s;
}
.pug-recruit-cta:hover { background: #5fede8; }
/* Two CTAs side-by-side: Vis Maior Guild Homepage + Join our Discord.
   The home button is muted (outline) so the Discord button still reads
   as the primary action. */
.pug-recruit-ctas { display: inline-flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.pug-recruit-cta-home {
    background: transparent;
    color: var(--c-accent);
    border: 1px solid var(--c-accent);
}
.pug-recruit-cta-home:hover { background: var(--c-accent); color: var(--c-bg); }

/* Pre-raid info card — sits above the recruit block. Heads-up about
   the Gargul addon + a few raid-night logistics. Distinct visual
   from the recruit card (no bg image, just the surface card with an
   accent-edge stripe) so the two read as separate concerns. Left
   border picks up the Gargul logo's purple so the card visually
   "owns" the addon callout. */
.pug-raidinfo-card {
    margin-top: var(--sp-6);
    padding: 18px 22px;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-left: 3px solid #6d4ba0;
    border-radius: 6px;
}
/* Center-align logo + title block vertically so the [G] tile sits
   on the same baseline as the heading + subtitle, not floating up
   relative to them. */
.pug-raidinfo-head {
    display: flex;
    align-items: center;
    gap: 14px;
    margin-bottom: 12px;
}
.pug-raidinfo-head h3 {
    margin: 0;
    font-size: var(--fs-lg);
    color: var(--c-text);
}
.pug-raidinfo-sub {
    margin: 4px 0 0;
    font-size: var(--fs-sm);
    color: var(--c-text-3);
}
/* Gargul addon logo — real artwork from Curseforge CDN. Rounded
   tile sized to match the heading height; subtle purple border to
   tie into the card's left stripe. */
.pug-gargul-logo {
    flex-shrink: 0;
    width: 48px;
    height: 48px;
    border-radius: 8px;
    border: 1px solid #6d4ba0;
    object-fit: cover;
    display: block;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
.pug-raidinfo-list {
    margin: 0;
    padding-left: 20px;
    color: var(--c-text-2);
    font-size: var(--fs-sm);
    line-height: 1.6;
}
.pug-raidinfo-list li { margin-bottom: 8px; }
.pug-raidinfo-list li:last-child { margin-bottom: 0; }
.pug-raidinfo-list strong { color: var(--c-text); }
.pug-raidinfo-list a { color: var(--c-accent); border-bottom: 1px dotted currentColor; text-decoration: none; }
.pug-raidinfo-list a:hover { border-bottom-style: solid; }
.pug-raidinfo-list code {
    background: rgba(15, 22, 35, 0.6);
    padding: 1px 5px;
    border-radius: 3px;
    color: var(--c-accent);
    border: 1px solid var(--c-border);
    font-size: var(--fs-xs);
}
.pug-recruit-body { color: var(--c-text-2); font-size: var(--fs-sm); margin: 0; line-height: 1.55; }
.pug-recruit-body p:first-child { margin-top: 0; }
.pug-recruit-list { margin: 8px 0 0; padding-left: 22px; line-height: 1.7; color: var(--c-text-2); font-size: var(--fs-sm); }
.pug-recruit-foot {
    margin-top: 12px;
    padding-top: 10px;
    border-top: 1px solid var(--c-border);
    font-size: var(--fs-xs);
    color: var(--c-text-3);
    line-height: 1.7;
}
.pug-recruit-contacts {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 12px;
    margin-left: 4px;
}
.pug-recruit-contact { white-space: nowrap; }
/* Hide the audit/timeoff row entirely for guests — both children are
   hidden in that role (audit via .write-only, timeoff via JS), so the
   wrapper would otherwise contribute a doubled gap above the next
   visible section in the parent's flex-column layout. */
body.role-guest #tab-attendance .at-section-row { display: none; }
.loot-subnav { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin-bottom: var(--sp-5); border-bottom: 1px solid var(--c-border); padding-bottom: var(--sp-3); }
/* Roster tab wrap — matches .loot-tab-wrap / .raid-tab-wrap so the
   sub-nav sits aligned with the centered page content (max-width
   1280, auto-margin, sp-5 top + sp-6 horizontal inset). Without this,
   .roster-subnav would render flush to the viewport edges and read
   as detached from the table below. */
.roster-tab-wrap {
    padding: var(--sp-5) var(--sp-6) 0;
    max-width: 1280px;
    margin: 0 auto;
}
/* Inner .tab-content-wrap inside the roster-tab-wrap zeroes its own
   padding/max-width to avoid double-applying — same trick the
   raid-tab-wrap uses for its inner partials. */
.roster-tab-wrap .tab-content-wrap {
    padding: 0;
    max-width: none;
    margin: 0;
}
/* Roster page sub-nav — same shape as loot-subnav (Roster / Pending /
   Scout). Sub-views are display:none until the matching .roster-sub
   gets the .active class via showRosterSub(). Pending + Scout sub-tab
   buttons carry .write-only so they're hidden for guests/raiders. */
.roster-subnav { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; margin-bottom: var(--sp-4); border-bottom: 1px solid var(--c-border); padding-bottom: var(--sp-3); }
.roster-sub { display: none; }
.roster-sub.active { display: block; }
/* Pending card — list of Vis Maior characters Blizzard's API reports
   are in the guild but the local roster doesn't have yet. Visually a
   sibling of the existing roster table; same .at-panel chrome inside
   so the table styling is consistent. */
.pending-card {
    background: var(--c-surface);
    border: 1px solid #223049;
    border-radius: 8px;
    padding: var(--sp-4);
    margin-bottom: var(--sp-3);
}
.pending-title {
    font-size: var(--fs-md);
    font-weight: 700;
    margin: 0 0 var(--sp-2) 0;
    color: var(--c-text);
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
}
.pending-count {
    background: var(--c-accent);
    color: var(--c-bg);
    font-size: var(--fs-xs);
    font-weight: 700;
    padding: 2px 8px;
    border-radius: 999px;
}
.pending-lede {
    color: var(--c-text-3);
    font-size: var(--fs-sm);
    margin: 0 0 var(--sp-3) 0;
    line-height: 1.55;
}
.pending-name { font-weight: 600; }
.pending-rank { color: var(--c-text-faint); font-size: var(--fs-xs); }
/* Slot at the right end of the subnav for per-sub-view extras (e.g.
   the Editing char picker on My Wishlist). margin-left:auto pushes it
   away from the sub-tab buttons. Empty when unused — no visual weight. */
.loot-subnav-extras { display: inline-flex; align-items: center; gap: var(--sp-2); margin-left: auto; }
/* On mobile, move the .loot-subnav-extras (Editing dropdown) BELOW
   the subnav's horizontal divider line. Take a full-width row,
   move the divider from the subnav's bottom border to the extras'
   top border. Sub-tab buttons (row 1) center; extras dropdown
   (row 2) right-aligns within its full-width row. Desktop layout
   (extras pinned to the right end of the same row as the buttons)
   is unchanged. */
@media (max-width: 520px) {
    .loot-subnav {
        border-bottom: none;
        padding-bottom: 0;
        /* Left-align the row-1 buttons (Wishlist / History / Gargul
           Export / Import) on phones — the user prefers the buttons
           anchored to the viewport's left edge across all four sub
           pages rather than centered. */
        justify-content: flex-start;
    }
    .loot-subnav-extras {
        flex-basis: 100%;
        margin-left: 0;
        /* margin-top adds breathing room ABOVE the divider line
           (which is the extras' border-top). With the parent's
           gap:var(--sp-2) of 8px + this 10px margin = 18px
           between the buttons row and the divider — matches the
           raid builder mobile spacing per user feedback. */
        margin-top: var(--sp-3);
        padding-top: var(--sp-3);
        border-top: 1px solid var(--c-border);
        /* Right-align the Editing dropdown within its full-width
           row — feels like a "floating right" anchor below the
           divider rather than centering with the buttons above. */
        justify-content: flex-end;
    }
}
/* Char picker (admin-select) inside the subnav extras slot has to match
   btn-small's height; otherwise the whole subnav row gets taller on the
   wishlist sub-view (where the picker exists) than on All Reservations
   (where the slot is empty), and the buttons visibly shift up/down a
   few px when switching between sub-tabs. fs-xs + 3px vertical padding
   matches .tab-btn.btn-small exactly. */
.loot-subnav-extras .admin-select {
    font-size: var(--fs-xs);
    padding: 3px 28px 3px 10px;
    line-height: 1.4;
}
/* Vertical divider between subnav groups (e.g. Wishlist views | Gargul
   integration). Same look as the filter strip's seg-divider. */
.loot-subnav-divider {
    display: inline-block;
    width: 1px;
    height: 22px;
    background: var(--c-border);
    align-self: center;
    margin: 0 var(--sp-1);
}
/* Group label preceding a cluster of related subnav buttons (e.g. the
   "Gargul" cluster: Export / Import / History). Quiet so it reads as
   a header, not a button. */
.loot-subnav-group-label {
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-faint);
    font-weight: 700;
    align-self: center;
}
.loot-sub { display: none; }
.loot-sub.active { display: block; }
.loot-sub-stub { color: var(--c-text-faint); font-size: var(--fs-sm); padding: var(--sp-5); text-align: center; background: var(--c-surface-2); border: 1px dashed var(--c-border); border-radius: var(--r-md); }
.loot-empty { color: var(--c-text-faint); padding: var(--sp-5); text-align: center; }
.loot-section-label { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--tracking-wide); color: var(--c-text-faint); margin-right: var(--sp-2); }
.loot-chartag { font-weight: 600; color: var(--c-text); font-size: var(--fs-md); }

.wl-header { display: flex; justify-content: space-between; align-items: center; gap: var(--sp-4); margin-bottom: var(--sp-3); flex-wrap: wrap; }
.wl-header-left  { display: flex; align-items: center; gap: var(--sp-2); }
.wl-header-right { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
/* Wishlist search input — same shape as the All Reservations one so the two
   views feel like siblings. Width caps so the header layout doesn't blow out. */
/* Search row sits below the filter strip and looks like its sibling —
   same layout (label + sticky bar) but a single wide input instead of
   button cluster. Stickies just below the filter strip via dynamic
   height vars so the search never overlaps the filter (filter
   buttons wrap on narrow viewports — static offset would drift). */
.wl-search-row {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
    background: var(--c-bg);
    padding: var(--sp-2) 0;
    /* Sticky behavior is now handled by the parent .wl-sticky-toolbar
       so the filter + search stick as ONE cohesive cluster — without
       the wrapper, the search scrolled independently and only "joined"
       the filter once it reached its own sticky offset. */
}
.wl-search { flex: 1; min-width: 0; }
/* Expand/Collapse-all accordion toggle moved out of the filter strip
   (it isn't a filter — it's accordion control) and onto the search
   row's right edge. Sits flush with the input + auto-margin-left so
   it sticks to the row's far end regardless of the input's width. */
.wl-accordion-toggle { flex-shrink: 0; }

/* "Visibility" cluster sits in its own row above the wishlist sections,
   right under the help text. Compact — just the label + the three
   PUBLIC/GUILD/PRIVATE buttons with a real gap between them.
   Right-aligned so it tucks neatly to the edge above Phase 1, away
   from the bulk of the help/search content on the left. */
/* Wishlist visibility (Public / Guild / Private) toggle UI was
   retired per user. Selectors kept intentionally empty so any
   stragglers in the DOM (cached older renders) just no-op rather
   than throw layout errors; full removal is safe once a deploy
   has rolled. */
/* Per-row BiS pill — compact rank tag colored by category (BIS orange,
   tank-flavored sub-tags in their respective hues, Alt grey-blue).
   Sits inside .wl-slot next to the slot/type chip so the row reads
   "<icon> Item Name | SLOT TYPE BIS·P1" left-to-right. */
.wl-bis-pill {
    display: inline-flex; align-items: center;
    /* No margin-left — the parent .wl-slot already provides gap: 6px
       between flex children. Earlier 6px margin-left on top of that
       6px gap doubled up to ~12px, pushing the BiS pill visibly far
       from the slot/type chip cluster. */
    font-size: var(--fs-micro);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding: 1px 6px;
    border: 1px solid currentColor;
    border-radius: var(--r-sm);
    background: rgba(255, 255, 255, 0.02);
    cursor: help;
    white-space: nowrap;
    flex-shrink: 0;
}
.wl-bis-pill.wl-bis-bis  { color: #ff7a45; }   /* BIS — warm orange-red */
.wl-bis-pill.wl-bis-thrt { color: #e8c85a; }   /* tank threat-flavor */
.wl-bis-pill.wl-bis-mit  { color: #5b9bd5; }   /* tank mitigation-flavor */
.wl-bis-pill.wl-bis-stam { color: #a37dd0; }   /* tank stam-flavor */
.wl-bis-pill.wl-bis-alt  { color: #8a98a8; }   /* Alt — muted grey-blue */
/* BiS filter button — when active, color in the BiS-pill orange so it
   visually announces "BiS gating is on" alongside whatever else is
   active on the main armor/state filter axis. */
/* Each active-state chip carries its OWN identity-color tint background —
   like every other filter button (MS/OS/plate/…). Without it, hover fell
   through to .tab-btn:hover's solid --c-accent teal, which bled under the
   orange/yellow/green text and border ("the colour showing through"). */
.wl-armor-seg .wl-armor-bis.is-active { color: #ff7a45; border-color: rgba(255,122,69,0.6); background: rgba(255,122,69,0.16); }
/* CONTESTED filter chip — uses the warn color to mirror the
   "this item is contested" pill on each row, so the toggle's active
   state visually matches the rows it surfaces. */
.wl-armor-seg .wl-armor-contested.is-active { color: var(--c-warn); border-color: rgba(212,170,68,0.6); background: rgba(212,170,68,0.16); }
.wl-armor-seg .wl-armor-uncontested.is-active { color: var(--c-good); border-color: rgba(29,158,117,0.6); background: rgba(29,158,117,0.16); }
/* Conflict state — applied to filter buttons that would produce an
   empty intersection given the currently-selected filter on the
   OTHER axis (e.g. Trophy + MS). Greys the button out + a not-
   allowed cursor so it visually reads as "you can't pick this
   right now"; clicking still resolves via auto-clear in
   setLootArmorFilter. */
.wl-armor-seg .seg-btn.is-conflict {
    opacity: 0.35;
    cursor: not-allowed;
    color: var(--c-text-faint);
    border-color: var(--c-border);
    background: transparent;
}
.wl-armor-seg .seg-btn.is-conflict:hover {
    opacity: 0.45;
    color: var(--c-text-faint);
    border-color: var(--c-border);
    background: transparent;
}
.wl-help { color: var(--c-text-3); font-size: var(--fs-sm); margin: 0 0 var(--sp-5); line-height: 1.5; }
.wl-help code { background: var(--c-surface-2); padding: 1px 5px; border-radius: 3px; }
/* Anchors inside the help block — pull from the app's link token
   instead of letting the browser render purple-default visited
   links (the CurseForge / Gargul link was reading as bright purple). */
.wl-help a { color: var(--c-link); text-decoration: none; border-bottom: 1px dotted rgba(114, 137, 218, 0.5); }
.wl-help a:hover { color: #9aaedf; border-bottom-color: var(--c-link); }
.wl-help a:visited { color: var(--c-link); }
/* Inline "Sign in" link in the wishlist guest empty state. Inherits
   the surrounding .loot-empty color (var(--c-text-faint)) rather than
   the indigo --c-link used elsewhere — the link sits inside body
   copy and shouldn't read as a separate color from the text it's
   embedded in. Dotted underline preserves the affordance. */
.loot-empty-signin { color: inherit; text-decoration: none; border-bottom: 1px dotted currentColor; cursor: pointer; opacity: 0.85; }
.loot-empty-signin:hover { color: var(--c-text); opacity: 1; }
.loot-empty-signin:visited { color: inherit; }
/* Help toggle — callout-style button right-aligned above the phases
   so it grabs attention as the "click me if confused" affordance
   without competing with the filter strip / search row. Closed by
   default; expands inline below the button on click. */
/* Desktop: trim the help-row's top margin slightly on wishlist so
   it reads with the same gap as Import's help button. Wishlist's
   parent (#loot-wishlist-root) is `display: flex; flex-direction:
   column`, which suppresses margin collapse — so the base 20px
   sits as-is and feels heavier than Import's plain block flow
   (where the 20px collapses against the previous element's
   margin-bottom). The mobile @media block below overrides this
   to 4px on phones. */
#loot-wishlist-root .wl-help-row { margin-top: 14px; }
.wl-help-row {
    display: flex;
    /* Center the callout button (used on Wishlist / Loot Import /
       Loot Export). Was right-aligned (justify-content: flex-end);
       per user, center reads better. Bottom margin bumped from
       sp-3 (10px) to sp-5 (18px) so the button has clear breathing
       room before the next section. flex-wrap + gap so the "Get Gargul"
       button sits beside the help toggle when closed and wraps cleanly
       below once the toggle expands to full width. */
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    justify-content: center;
    margin: 20px 0 var(--sp-5) 0;
}
.wl-help-toggle {
    display: inline-block;
    max-width: 100%;
}
.wl-help-toggle[open] {
    display: block;
    width: 100%;
}
.wl-help-toggle > summary {
    list-style: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 16px;
    border-radius: var(--r-md);
    border: 1px solid var(--c-accent);
    background: rgba(51, 228, 222, 0.08);
    color: var(--c-accent);
    font-size: var(--fs-sm);
    font-weight: 700;
    user-select: none;
    transition: background 0.12s, border-color 0.12s, transform 0.12s;
    box-shadow: 0 0 0 0 rgba(51, 228, 222, 0);
}
.wl-help-toggle[open] > summary {
    /* Stay centered when expanded so the button doesn't drift right
       while the help panel below it takes the full width. */
    margin: 0 auto;
    display: flex;
    width: max-content;
}
.wl-help-toggle > summary::-webkit-details-marker { display: none; }
.wl-help-toggle-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--c-accent);
    color: var(--c-bg);
    font-size: var(--fs-xs);
    font-weight: 800;
    line-height: 1;
    flex-shrink: 0;
}
.wl-help-toggle > summary:hover {
    background: rgba(51, 228, 222, 0.16);
    border-color: var(--c-accent);
    box-shadow: 0 0 0 3px rgba(51, 228, 222, 0.15);
}
.wl-help-toggle > .wl-help {
    /* sp-3 (10px) felt slightly tight between the button and the
       expanded panel — bump by 3px so the panel reads as a clearly
       separated reveal rather than visually attached to the button.
       Per user feedback. */
    margin-top: calc(var(--sp-3) + 3px);
    padding: var(--sp-4);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    background: var(--c-surface);
    /* Mobile containment: long <code> blocks (e.g. /gargul tmb) and
       any unbroken strings should wrap rather than push content past
       the panel's right edge. Without this, the help panel renders
       with text bleeding outside its border on narrow viewports. */
    box-sizing: border-box;
    max-width: 100%;
    overflow-wrap: anywhere;
}
/* The <details> wrapper itself needs to behave as a proper flex item
   on mobile (parent .wl-help-row is display:flex). Without the
   min-width:0 + box-sizing fix, when [open] makes it `display: block;
   width: 100%`, intrinsic content widths can push it wider than the
   parent's available space — pulling the help panel past the
   viewport edge. */
.wl-help-toggle {
    box-sizing: border-box;
    min-width: 0;
}
.wl-help-toggle[open] {
    box-sizing: border-box;
    max-width: 100%;
}
/* Inline <code> snippets in help copy should also wrap on narrow
   viewports rather than force-extend the line. */
.wl-help code {
    overflow-wrap: anywhere;
    word-break: break-word;
}
/* Restructured help block — quick lead line, scannable button list
   with mini-pills matching the actual button colors, separate "rolls"
   paragraph, and an <details>-collapsed edge-cases section. Replaces
   the old wall-of-text paragraph that was hard to digest. */
.wl-help-lead { margin: 0 0 var(--sp-3); }
.wl-help-buttons {
    list-style: none;
    margin: 0 0 var(--sp-3);
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}
.wl-help-buttons li {
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
    line-height: 1.4;
}
/* Wraps the bold label + description text into ONE flex item so
   they wrap as a normal inline-text block instead of breaking into
   separate flex columns. Without this wrapper, <strong> + adjacent
   text become independent flex items in the parent flex row, each
   sized to its natural width — produced the awkward "Off- / spec"
   column the user flagged on narrow viewports. */
.wl-help-text {
    flex: 1 1 0;
    min-width: 0;
}
.wl-help-pill {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    padding: 1px 7px;
    border-radius: var(--r-sm);
    border: 1px solid currentColor;
    flex-shrink: 0;
    min-width: 36px;
    text-align: center;
    background: var(--c-surface-2);
}
.wl-help-pill-ms     { color: var(--c-good); }
.wl-help-pill-os     { color: var(--c-os); }
/* WANT / HAVE / PASS pills render in the neutral text-faint shade —
   user wanted only MS and OS colored in the example list since those
   are the ones with meaningful color semantics on the rows. */
.wl-help-pill-want   { color: var(--c-text-faint); }
.wl-help-pill-have   { color: var(--c-text-faint); }
.wl-help-pill-pass   { color: var(--c-text-faint); }
.wl-help-rolls { margin: 0 0 var(--sp-2); }
/* Retired "Boosted Rolls" explainer — a muted, collapsed sub-toggle inside
   the loot help so the old system stays documented without cluttering it. */
.wl-help-legacy { margin-top: var(--sp-3); border-top: 1px solid var(--c-border); padding-top: var(--sp-3); }
.wl-help-legacy-summary { cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-xs); color: var(--c-text-faint); font-weight: 600; user-select: none; }
.wl-help-legacy-summary::-webkit-details-marker { display: none; }
.wl-help-legacy-summary::after { content: '▸'; color: var(--c-text-faint); transition: transform 0.12s; }
.wl-help-legacy[open] > .wl-help-legacy-summary::after { transform: rotate(90deg); }
.wl-help-legacy-tag { text-transform: uppercase; letter-spacing: 0.06em; font-size: 9px; font-weight: 700; color: var(--c-text-faint); border: 1px solid var(--c-border); border-radius: 3px; padding: 1px 5px; }
.wl-help-legacy-body { margin-top: var(--sp-2); color: var(--c-text-faint); font-size: var(--fs-xs); line-height: 1.5; }
.wl-help-legacy-body p { margin: 0 0 var(--sp-2); }
/* "Get Gargul" button (Loot help row + Join loot card) + its modal. */
/* Sized to match the "How does the loot system work?" toggle it sits beside,
   and colour-matched to the Gargul icon (its #36393f gray background) so the
   logo blends into the button — same brand-button pattern as the Discord btn. */
.wl-gargul-btn { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; font-size: var(--fs-sm); font-weight: 700; border-radius: var(--r-md); text-transform: none; letter-spacing: 0; }
.tab-btn.wl-gargul-btn { background: #36393f; border-color: #454951; color: #fff; }
/* Hover: brighten the WHOLE button uniformly (filter) rather than swapping the
   background. The icon carries its own #36393f gray baked into the image, so a
   background-only change left a mismatched darker rectangle around the logo;
   brightening scales the button bg and the icon's gray by the same factor, so
   they stay blended and there's no gray patch. */
.tab-btn.wl-gargul-btn:hover:not(:disabled) { background: #36393f; border-color: #454951; color: #fff; filter: brightness(1.22); }
.wl-gargul-btn-icon { width: 18px; height: 18px; border-radius: 3px; flex-shrink: 0; margin-left: -2px; }
.gargul-modal { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-md); padding: 18px; max-width: 460px; width: 100%; box-shadow: 0 10px 40px rgba(0,0,0,.5); }
.gargul-modal-h3 { margin: 0 0 12px; font-size: var(--fs-md); }
.gargul-modal-actions { display: flex; justify-content: flex-end; margin-top: 16px; }
.gargul-info { display: flex; gap: 14px; align-items: flex-start; }
.gargul-info-logo { width: 64px; height: 64px; border-radius: var(--r-md); flex-shrink: 0; }
.gargul-info-body { flex: 1; min-width: 0; font-size: var(--fs-sm); color: var(--c-text-2); line-height: 1.5; }
.gargul-info-body p { margin: 0 0 10px; }
.gargul-info-body code { background: var(--c-surface-2); padding: 1px 5px; border-radius: 3px; }
.gargul-info-body a.tab-btn { text-decoration: none; }
/* Join page "How loot works" card reuses the wishlist help styling. */
.join-loot-card .join-loot-help { margin-bottom: 0; }
@media (max-width: 480px) { .gargul-info { flex-direction: column; } .gargul-info-logo { width: 48px; height: 48px; } }
.wl-help-edge { margin-top: var(--sp-2); }
.wl-help-edge > summary { cursor: pointer; color: var(--c-text-faint); font-size: var(--fs-xs); padding: 2px 0; }
.wl-help-edge > summary:hover { color: var(--c-text-2); }
.wl-help-edge-list { list-style: disc; padding-left: 18px; margin: 4px 0 0; color: var(--c-text-faint); font-size: var(--fs-xs); }
.wl-help-edge-list li { margin-bottom: 2px; }

.wl-sections { display: flex; flex-direction: column; gap: var(--sp-5); }
.wl-phase { display: flex; flex-direction: column; gap: var(--sp-3); }
.wl-phase-head { font-size: var(--fs-md); color: var(--c-text-faint); text-transform: uppercase; letter-spacing: 0.06em; margin: 0 0 2px; padding-bottom: 7px; border-bottom: 1px solid var(--c-border); display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.wl-phase-label { flex-shrink: 0; color: var(--c-accent); }
.wl-phase-counts { margin-left: auto; }
/* Active (currently-progged) phase — floated to the top + flagged. */
/* Both the "active" pill and the "Set active" button share the same pill shape
   so they read as one slot next to the phase label. */
/* Phase status label (driven by the Progression active phase) — replaces the
   old Set active button. ACTIVE in brand teal; COMPLETED / UPCOMING muted. */
.wl-phase-active > .wl-phase-head { border-bottom-color: color-mix(in srgb, var(--c-accent) 55%, var(--c-border)); }
.wl-phase-active > .wl-phase-head .wl-phase-label { color: var(--c-accent); }
/* Full-width labeled section divider between the Active / Completed / Upcoming
   phase groups — mirrors the Progression page's .prog-group-divider. */
.wl-group-divider { display: flex; align-items: center; gap: 12px; margin: var(--sp-6) 2px var(--sp-4); font-size: var(--fs-xs); font-weight: 700; letter-spacing: 0.10em; text-transform: uppercase; color: var(--c-text-faint); }
.wl-group-divider:first-child { margin-top: 0; }
.wl-group-divider > span { flex: 0 0 auto; }
.wl-group-divider::before,
.wl-group-divider::after { content: ""; flex: 1 1 auto; height: 1px; background: var(--c-border); }
.wl-group-divider.wl-group-active { color: var(--c-accent); }
.wl-group-divider.wl-group-active::before,
.wl-group-divider.wl-group-active::after { background: rgba(51, 228, 222, 0.35); }
.wl-phase-raids { display: flex; flex-direction: column; gap: var(--sp-3); }
.wl-raid { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-lg); padding: var(--sp-3) var(--sp-4); }
.wl-raid[open] { padding-bottom: var(--sp-4); }
.wl-raid-name { cursor: pointer; font-size: var(--fs-lg); color: var(--c-accent); margin: 0; padding: 4px 0; list-style: none; }
.wl-raid-name::-webkit-details-marker { display: none; }
.wl-raid-name::before { content: '▸ '; color: var(--c-text-faint); font-size: var(--fs-sm); }
.wl-raid[open] .wl-raid-name::before { content: '▾ '; }
.wl-raid-meta { color: var(--c-text-faint); font-size: var(--fs-xs); font-weight: normal; margin-left: var(--sp-2); }
.wl-boss { margin-top: var(--sp-3); padding: var(--sp-3) var(--sp-3) var(--sp-3); border-top: 1px solid var(--c-border-2); border-radius: var(--r-md); }
.wl-boss:first-of-type { border-top: 0; padding-top: var(--sp-2); }
.wl-boss-name { color: var(--c-text); font-size: var(--fs-md); margin: 0; }
.wl-boss-head { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-2); }
/* Clickable head opens the boss-loot modal — same data the progression
   page exposes. Cursor + subtle hover so it reads as interactive
   without screaming "BUTTON". Outline on focus for keyboard nav. */
.wl-boss-head-clickable { cursor: pointer; border-radius: var(--r-md); padding: 2px; margin: -2px -2px var(--sp-2); transition: background 0.1s; }
.wl-boss-head-clickable:hover { background: rgba(255,255,255,0.03); }
.wl-boss-head-clickable:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
/* Wrapper around the boss thumbnail so the .prog-loot-badge can
   absolute-position over it. Sized to match the thumb so the
   absolute coordinates land in the right place. */
.wl-boss-thumb-wrap { position: relative; flex-shrink: 0; line-height: 0; }
.wl-boss-thumb { width: 44px; height: 44px; border-radius: var(--r-md); object-fit: cover; border: 1px solid var(--c-border); flex-shrink: 0; background: var(--c-surface-2); }
.wl-boss-thumb-zone { display: flex; align-items: center; justify-content: center; font-size: var(--fs-xs); font-weight: 600; color: var(--c-text-faint); text-transform: uppercase; letter-spacing: 0.04em; border-style: dashed; }
/* Group MS + OS (or the single WANT trophy button) for layout. */
.wl-want-group { display: inline-flex; gap: 4px; }
/* Vertical divider between row clusters (slot pill | WANT | HAVE/PASS).
   No own margin — `.wl-actions` already supplies a 4px gap between
   siblings, and the divider was doubling the breathing room around
   itself with its own margin (gap on the right of the slot-pill
   divider was reading wider than the gaps between regular buttons). */
.wl-actions-divider {
    display: inline-block;
    width: 1px;
    height: 18px;
    background: var(--c-border);
    margin: 0;
    align-self: center;
}
.wl-boss-titlewrap { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.wl-boss-meta { font-size: var(--fs-xs); color: var(--c-text-faint); }
.wl-boss-picked { color: var(--c-accent); font-weight: 600; }
.wl-raid-title { font-weight: 600; }
.wl-raid-counts { display: inline-flex; gap: 6px; margin-left: var(--sp-2); vertical-align: middle; }
/* When a raid section is collapsed, hide its per-raid completion chips —
   only the higher-level per-phase totals (rendered outside any <details>)
   read at-a-glance from the collapsed view. The phase chips already give
   the big picture; the per-raid % chips just add noise until expanded. */
details.wl-raid:not([open]) > summary .wl-raid-counts { display: none; }
.wl-chip { font-size: var(--fs-micro); font-weight: 700; letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 2px 7px; border-radius: 999px; border: 1px solid var(--c-border); background: var(--c-surface-2); color: var(--c-text-3); }
.wl-chip-ms     { color: var(--c-ms);      border-color: var(--c-ms); }
.wl-chip-os     { color: var(--c-os);      border-color: var(--c-os); }
.wl-chip-shared { color: var(--c-special); border-color: var(--c-special); }
.wl-chip-have   { color: var(--c-text-faint); border-color: var(--c-border); }
.wl-chip-pct    { color: var(--c-accent);     border-color: var(--c-accent); }
.wl-boss-have   { color: var(--c-text-faint); }
/* 2-column layout (used to be 3) — gives each row enough breathing room for
   the slot, the armor / weapon chip, the MS / OS / WANT / HAVE buttons, and
   the wowhead-rendered item name to all sit on one line without squeezing. */
/* (.wl-editor-row was used briefly to host the Editing/Viewing picker
   on its own row inside the wishlist body, below the loot-subnav. It
   was removed when the picker moved back into .loot-subnav-extras —
   keeping it out of the body avoids the "missing bg" gap that appeared
   during scroll between the body row scrolling away and the sticky
   toolbar pinning. The class no longer ships any markup; if reintroduced
   later, restore the rule from git history.) */
.wl-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(440px, 1fr)); gap: 6px; }
/* Phones (≤520px): drop the 440px floor so columns can shrink to fit a
   320-360 px viewport. Without this the minmax(440px, 1fr) kept the
   grid 440 px wide minimum and rows extended past the right edge of
   the screen. wl-item already ellipsizes long names via min-width:0. */
@media (max-width: 520px) {
    .wl-grid { grid-template-columns: minmax(0, 1fr); }
    /* Don't sticky the wishlist toolbar on phones — the cluster is
       too tall (filter chips + search row) for it to be useful pinned;
       it just eats vertical real estate on narrow screens. Drop the
       sticky positioning + shadow so the toolbar scrolls away with
       the rest of the page.
       !important needed because the desktop .wl-sticky-toolbar rule
       sits later in the file (line 4374) and would otherwise win the
       cascade tie at this specificity. */
    .wl-sticky-toolbar {
        position: static !important;
        box-shadow: none !important;
        padding-top: 0 !important;
        margin: 0 0 var(--sp-3) 0 !important;
    }
    /* Hide secondary chrome on the wishlist row to save horizontal
       room for the item name + action buttons:
         - .wl-slot     = "Neck" / "Plate" / "Cloth" / etc. slot+armor chip
         - .wl-contest-pill = "vs N" reserver-count pill
       Both are nice-to-have on desktop but redundant context on a
       phone where every pixel of row width counts toward fitting the
       item name and the MS / OS / WANT / HAVE / PASS button cluster. */
    .wl-row > .wl-slot,
    .wl-row .wl-contest-pill { display: none; }
}
.wl-row { display: flex; align-items: center; gap: var(--sp-3); background: var(--c-surface-2); border: 1px solid var(--c-border); border-radius: var(--r-md); padding: 6px 10px; }
/* Selected wishlist rows: bucket color set inline by
   _renderWishlistEditorInner (MS = green / OS = blue / Trophy = purple).
   Keeps the generic fallback in case neither inline style nor a class
   override is applied. */
.wl-row.on-pick { border-color: var(--c-accent); }
/* Dual-bucket selection (tier tokens with BOTH MS + OS picked) —
   bold slanted candy-stripe in MS green and OS blue. Trick: a thick
   transparent border lets a repeating-linear-gradient at 135° show
   through the border-box layer, while the inner padding-box stays
   the row's surface color. Border-radius is preserved (no
   border-image weirdness). 3px thickness reads bolder than the 1px
   solid stroke on single-bucket picks; 16px stripe period gives
   ~half a dozen visible stripes around a typical row instead of the
   browser's default tiny dashes. */
.wl-row.on-pick.dual-stroke {
    border: 1px solid transparent;
    background: linear-gradient(var(--c-surface-2), var(--c-surface-2)) padding-box,
                repeating-linear-gradient(135deg,
                    var(--c-ms) 0 12px,
                    var(--c-os) 12px 24px) border-box;
}
/* All three "out of competition" states (HAVE / PASS / wrong-class) share
   the opacity: 0.4 dim AND a line-through on the item name. The combo
   reads as "crossed off the list — out of contention." Wrong-class also
   swaps the button cluster for a single .wl-disabled-pill since none of
   the action buttons would do anything for those rows. */
.wl-row.on-have,
.wl-row.on-skip,
.wl-row.cant-use { opacity: 0.4; }
.wl-row.on-have .wl-item,
.wl-row.on-skip .wl-item,
.wl-row.cant-use .wl-item { text-decoration: line-through; }
.wl-disabled-pill,
.wl-contest-pill {
    font-size: var(--fs-micro);
    font-weight: 700;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    padding: 3px 10px;
    border-radius: 999px;
    border: 1px solid var(--c-border);
    background: var(--c-surface-2);
    color: var(--c-text-faint);
    align-self: center;
    white-space: nowrap;
}
/* Contest pill variants — used in Quick-check player view to flag
   whether a player's reservation is rolled-against (contested),
   uncontested, or moot (already owned). Hover surfaces the
   contestants tooltip when there are any. */
.wl-contest-pill { cursor: default; min-width: 36px; text-align: center; }
.wl-contest-pill.contested { color: var(--c-warn); border-color: var(--c-warn); }
.wl-contest-pill.uncontested { color: var(--c-good); border-color: var(--c-good); }

/* Quick-check player view — items laid out in a 2-column grid (collapses
   to 1 column on narrow viewports). Each row is its own 3-segment grid:
   item icon+name on the left, slot label+TIER pill centered, contested
   /uncontested status pill right-aligned. The 1fr / auto / 1fr column
   template keeps the slot pill visually centered with equal gutters
   regardless of item-name length. */
.qc-list {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 4px 12px;
    margin-top: var(--sp-2);
}
@media (max-width: 800px) {
    .qc-list { grid-template-columns: 1fr; }
}
.qc-row {
    display: grid;
    grid-template-columns: 1fr auto auto;
    align-items: center;
    gap: var(--sp-3);
    padding: 6px 10px;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    min-width: 0;
}
.qc-row.is-owned { opacity: 0.4; }
.qc-row.is-owned .wl-item { text-decoration: line-through; }
/* `justify-self: start` keeps the anchor sized to its content (icon +
   name) instead of stretching to fill the 1fr track. Wowhead's
   power.js positions tooltips relative to the anchor's bounding box,
   so a stretched anchor would put the tooltip far past the visible
   text. Same fix as `.wl-item { display: inline-flex }` in the
   wishlist editor — the grid context needs the explicit override. */
.qc-row > .wl-item {
    min-width: 0;
    justify-self: start;
    /* Long item names (e.g. "Glaive of the Defender", "Bracers of
       the Forgotten Conqueror") were spilling out of the qc-row
       grid track on narrow viewports because .wl-item is inline-
       flex with no overflow handling. Constrain to its grid track
       width and ellipsize the inner text node — the anchor itself
       stays clickable; the icon + start of the name remain visible. */
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* The anchor's children (icon img + text node) need their own
   handling — the anchor is inline-flex which means inline children
   don't naturally truncate. Force the last child (the name) to
   shrink + ellipsize. */
.qc-row > .wl-item > * + * {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
/* With the outer 2-column grid, each row is half the page width — slot
   pill + status pill both right-align so they cluster at the row's end
   (instead of slot-centered, which left awkward dead space when the
   item name was short). */
.qc-row > .wl-slot { justify-self: end; }
.qc-row > .wl-actions { justify-self: end; }
.tab-btn.btn-have-on,
.tab-btn.btn-pass-on { background: var(--c-text-faint); color: var(--c-bg); border-color: var(--c-text-faint); }
/* `.wl-item` sizes to its content (icon + name) so Wowhead's power.js
   tooltip positions next to the actual link, not the wide row gutter
   that a `flex: 1` anchor would create. The slot pill + action area on
   the right are pushed to the row's end via `.wl-row > .wl-slot {
   margin-left: auto }` instead of relying on the anchor stretching.
   Row order: [item] ........ [slot/type chip] [actions] | [contest].

   `justify-self: start` is critical inside CSS Grid containers (e.g.
   .profile-loot-row's `92px 1fr max-content` template). Grid items
   default to `justify-self: stretch`, which would make the anchor span
   the entire `1fr` column — every pixel of empty space to the right of
   the name would become clickable AND Wowhead's tooltip would position
   off the right edge of the wide bounding box instead of next to the
   visible icon+name. The anchor's intrinsic `inline-flex` size doesn't
   override grid's stretch alignment; this does.

   `align-self: center` keeps it vertically centered when a grid row
   has `align-items: center`. */
.wl-item { display: inline-flex; align-items: center; gap: 9px; text-decoration: none; font-size: var(--fs-sm); font-weight: 600; min-width: 0; justify-self: start; align-self: center; }
.wl-row > .wl-slot { margin-left: auto; }
.wl-item ins { width: 24px !important; height: 24px !important; background-size: 24px 24px !important; border-radius: 3px; display: inline-block; flex-shrink: 0; }
/* Native <img> icon (loading="lazy" defers off-screen fetches). Same
   visual size + radius as the legacy <ins> background-image so existing
   row layouts don't shift. */
.wl-item-icon { width: 24px; height: 24px; border-radius: 3px; flex-shrink: 0; display: inline-block; }
/* The inline-rendered name span (wlItemAnchor() output) — let it shrink
   gracefully on narrow rows without pushing the row's other children
   off-screen. */
.wl-item > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wl-slot { font-size: var(--fs-micro); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--c-text-faint); white-space: nowrap; display: inline-flex; align-items: center; gap: 4px; }
/* Armor type badge (plate / mail / leather / cloth). Sits inside .wl-slot so
   it tracks alongside the slot label. Color-coded so the gear type pops at a
   glance. Uses currentColor borders for low-noise look. */
.wl-armor {
    display: inline-block;
    font-size: 9px; line-height: 1;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    padding: 2px 5px;
    border: 1px solid currentColor;
    border-radius: var(--r-sm);
    font-weight: 700;
    opacity: 0.85;
}
/* Pill color groups (per user). Three clusters:
   - Armor (plate/mail/leather/cloth) + Back share a neutral white so
     the eye reads them as one "wearable armor" family — Back groups
     visually with armor in the filter strip too.
   - Jewelry (neck/finger/trinket) shares the gold/yellow warn color.
   - Weapon is red (--c-bad), the held-in-hand cluster.
   - Trophy reuses the special purple — same hue as the trophy/shared
     row chip throughout the app. */
/* Warm dusty brown — distinct from the cool gray slot label next to it
   (.wl-slot is --c-text-faint = #6a7d96, blue-ish gray) and the
   colored item name to its right. Reads as "armor" cluster without
   competing with the bucket buttons or the quality-colored title. */
.wl-armor-plate,
.wl-armor-mail,
.wl-armor-leather,
.wl-armor-cloth,
.wl-armor-back    { color: #b08e6b; }
.wl-armor-neck,
.wl-armor-finger,
.wl-armor-trinket,
.wl-armor-relic,
.wl-armor-libram  { color: var(--c-warn); }
.wl-armor-weapon  { color: var(--c-bad); }
.wl-armor-trophy  { color: var(--c-special); }
/* Tier — same warm copper/amber as the Tier filter button. */
.wl-armor-tier    { color: #e88c4d; }
/* Pattern — crafting recipes (Pattern / Plans / Design / Formula /
   Recipe). Soft cyan-blue to read as profession-related rather than
   gear. Quest = green-ish (attunement chains). Bag = muted gold. */
.wl-armor-pattern { color: #67c4d6; }
.wl-armor-quest   { color: #69c97a; }
.wl-armor-bag     { color: #c8a86a; }
/* MS / OS bucket-state filters use the same accents as the bucket
   buttons themselves so the strip-button reads as the same concept. */
.wl-armor-ms      { color: var(--c-ms); }
.wl-armor-os      { color: var(--c-os); }
/* Armor filter strip: small segmented toggle above the wl-sections.
   Sticks just below the page header while scrolling so users can keep
   filtering as the loot list scrolls underneath. `--sticky-header-h` is
   measured by app.js (trackStickyHeaderHeight) and updates on resize. The
   100px fallback covers the brief pre-measure window on first paint. */
/* Two-row filter cluster — gear-category row on top, state filters
   below. Sticky as one unit so both rows stay pinned together when
   scrolling the wishlist body. Each .wl-armor-row is a horizontal
   flex of label + segmented buttons. */
.wl-armor-filter {
    display: flex;
    flex-direction: column;
    /* +4px between FILTER ITEMS and FILTER STATE rows so the two
       filter clusters read as distinct lines instead of running into
       each other (was 4px, bumped to 8px per user feedback). */
    gap: 8px;
    background: var(--c-bg);
    /* Sticky now handled by parent .wl-sticky-toolbar — keep the
       padding so the buttons don't kiss the cluster's outer edges. */
    padding: var(--sp-2) 0;
}
/* Sticky wrapper around the wishlist filter strip + search row. Both
   stick as ONE cohesive cluster so the search no longer scrolls
   independently before the filter joins it (the previous staggered
   sticky offsets caused that "search moves alone, then filter slides
   up below it" behavior the user flagged).
   - padding-top above the filter row gives breathing room when the
     cluster anchors against the page header.
   - box-shadow under the bottom edge (the search row) reads as a soft
     "this surface is floating above the scrolling content" cue. */
.wl-sticky-toolbar {
    position: sticky;
    top: var(--sticky-header-h, 100px);
    z-index: 5;
    background: var(--c-bg);
    padding-top: var(--sp-2);
    margin: calc(-1 * var(--sp-2)) 0 var(--sp-3) 0;
    /* Shadow is gated on .is-stuck (added via IntersectionObserver
       once the toolbar starts pinning against the page header). At
       the natural top-of-page scroll position the toolbar isn't
       sticky yet — no shadow there. The transition smooths the
       fade-in as the user scrolls. */
    box-shadow: 0 6px 8px -4px rgba(0, 0, 0, 0);
    transition: box-shadow 0.15s ease;
}
.wl-sticky-toolbar.is-stuck {
    box-shadow: 0 6px 8px -4px rgba(0, 0, 0, 0.55);
}
.wl-armor-row {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    flex-wrap: wrap;
}
/* Pin both row labels ("Filter Items" / "Filter State") to the same
   width so the first filter button starts at the same X on each row.
   Without this, font-rendering on uppercase letters (M wider than A,
   etc.) leaves the rows visibly misaligned even when label lengths
   look identical in source. */
.wl-armor-row > .loot-section-label {
    min-width: 90px;
    flex-shrink: 0;
}
.wl-armor-seg { display: inline-flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.wl-armor-seg .seg-btn { min-width: 56px; }
/* Group divider between filter clusters (All | Armors | Jewelry | Weapon).
   Thin vertical line that matches the header subnav divider color. */
.wl-armor-seg .seg-divider {
    display: inline-block;
    width: 1px;
    height: 18px;
    background: var(--c-border);
    margin: 0 var(--sp-1);
    align-self: center;
}
/* Active-state filter buttons match the pill cluster colors:
   - Plate/Mail/Leather/Cloth/Back share white (armor cluster)
   - Neck/Finger/Trinket share gold (jewelry cluster)
   - Weapon is red (--c-bad)
   - Trophy is purple (--c-special) */
.wl-armor-seg .seg-btn.is-active.wl-armor-plate,
.wl-armor-seg .seg-btn.is-active.wl-armor-mail,
.wl-armor-seg .seg-btn.is-active.wl-armor-leather,
.wl-armor-seg .seg-btn.is-active.wl-armor-cloth,
.wl-armor-seg .seg-btn.is-active.wl-armor-back    { color: #b08e6b; border-color: #b08e6b; background: rgba(176,142,107,0.16); }
.wl-armor-seg .seg-btn.is-active.wl-armor-neck,
.wl-armor-seg .seg-btn.is-active.wl-armor-finger,
.wl-armor-seg .seg-btn.is-active.wl-armor-trinket,
.wl-armor-seg .seg-btn.is-active.wl-armor-relic,
.wl-armor-seg .seg-btn.is-active.wl-armor-libram   { color: var(--c-warn); border-color: var(--c-warn); background: rgba(212,170,68,0.16); }
.wl-armor-seg .seg-btn.is-active.wl-armor-weapon  { color: var(--c-bad); border-color: var(--c-bad); background: rgba(232,112,112,0.16); }
.wl-armor-seg .seg-btn.is-active.wl-armor-trophy  { color: var(--c-special); border-color: var(--c-special); background: rgba(168,144,216,0.16); }
/* Tier — warm copper/amber, distinct from jewelry yellow + weapon red. */
.wl-armor-seg .seg-btn.is-active.wl-armor-tier    { color: #e88c4d; border-color: #e88c4d; background: rgba(232,140,77,0.16); }
/* Pattern / Quest / Bag filters mirror the pill colors. */
.wl-armor-seg .seg-btn.is-active.wl-armor-pattern { color: #67c4d6; border-color: #67c4d6; background: rgba(103,196,214,0.16); }
.wl-armor-seg .seg-btn.is-active.wl-armor-quest   { color: #69c97a; border-color: #69c97a; background: rgba(105,201,122,0.16); }
.wl-armor-seg .seg-btn.is-active.wl-armor-bag     { color: #c8a86a; border-color: #c8a86a; background: rgba(200,168,106,0.16); }
/* MS / OS bucket-state filters — accents borrowed from the bucket
   buttons themselves (--c-ms green, --c-os blue) so the strip-button
   matches the row-button color users already associate with each
   bucket. */
.wl-armor-seg .seg-btn.is-active.wl-armor-ms      { color: var(--c-ms); border-color: var(--c-ms); background: color-mix(in srgb, var(--c-ms) 16%, transparent); }
.wl-armor-seg .seg-btn.is-active.wl-armor-os      { color: var(--c-os); border-color: var(--c-os); background: color-mix(in srgb, var(--c-os) 16%, transparent); }
/* HAVE / PASS personal-state filters — same neutral cool gray for
   both, since they belong to the same "out of competition" cluster
   visually. */
.wl-armor-seg .seg-btn.is-active.wl-armor-have,
.wl-armor-seg .seg-btn.is-active.wl-armor-pass    { color: var(--c-text-faint); border-color: var(--c-text-faint); background: rgba(106,125,150,0.16); }
.wl-actions { display: flex; gap: 4px; }
.wl-actions .tab-btn { min-width: 38px; padding: 3px 8px; font-size: var(--fs-xs); }

/* All Reservations sub-view. The toolbar shares .wl-search-row layout
   with the wishlist's search row so both sub-tabs start at the same Y
   offset (filter strip first, search row directly below) — no shift
   when switching between sub-tabs. */
.ar-toolbar .admin-select { min-width: 180px; }
.ar-toolbar .admin-input  { min-width: 220px; flex: 1; }
.ar-results { display: flex; flex-direction: column; gap: var(--sp-5); }
/* Native webkit search-clear "X" is dark-on-dark by default — invert it so it
   reads against our dark surface. Applies to all .admin-input[type=search]. */
.admin-input[type="search"]::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
    height: 14px; width: 14px;
    background: var(--c-text-faint);
    -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M3 3l10 10M13 3L3 13' stroke='black' stroke-width='2' stroke-linecap='round'/></svg>") no-repeat center / contain;
            mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path d='M3 3l10 10M13 3L3 13' stroke='black' stroke-width='2' stroke-linecap='round'/></svg>") no-repeat center / contain;
    cursor: pointer;
}
.admin-input[type="search"]::-webkit-search-cancel-button:hover { background: var(--c-text); }
.ar-summary { color: var(--c-text-faint); font-size: var(--fs-sm); margin-left: auto; }
/* Two-column layout shared across every row in the boss section so
   the reserver columns line up no matter how long an item name gets.
   Subgrid lets each .ar-row inherit the parent .ar-list's column
   tracks while keeping its own border + background + padding.
   First track is `var(--ar-item-col-width)` — pinned by JS
   (`_alignArItemColumns`) to the widest .ar-item across the WHOLE
   view so columns align across boss + raid sections (not just within
   one boss). Falls back to `max-content` on first paint before the
   measurement runs. Second track `1fr` (= remainder for MS/OS/
   Trophies columns). */
.ar-list { display: grid; grid-template-columns: var(--ar-item-col-width, max-content) 1fr; row-gap: 8px; column-gap: var(--sp-6); }
/* `align-items: end` sits .ar-item (single-line item info + dropdown) on
   the SAME baseline as .ar-cols' value row (—  / Achtungg). Labels (MS /
   OS) float above. Without this the row used align-items: center, which
   left the item name floating awkwardly between the label row and the
   value row of .ar-cols. */
.ar-row {
    display: grid;
    grid-template-columns: subgrid;
    grid-column: 1 / -1;
    align-items: end;
    background: var(--c-surface-2); border: 1px solid var(--c-border);
    border-radius: var(--r-md); padding: 10px 14px;
}
.ar-item { display: flex; align-items: center; gap: 8px; }
/* Extra breathing room between the item name and the slot label
   (CHEST / TRINKET / etc.) — flush against each other reads cramped
   when the item name is long. Adds 8px on top of the 8px flex gap
   for a total of 16px between name and slot, without affecting any
   of the other intra-row spacings. */
.ar-item .wl-slot { margin-left: 8px; }
/* Item-name anchor inside .ar-item must never truncate — the parent grid
   track is sized to fit the WIDEST item across the whole view (see
   _alignArItemColumns), so every row's name has the room it needs.
   Override the wishlist-editor's `.wl-item > span { overflow: hidden;
   text-overflow: ellipsis }` rule that clips inside the narrower 2-col
   wishlist grid, but doesn't apply here. */
.ar-item .wl-item, .ar-item .wl-item > span {
    overflow: visible;
    text-overflow: clip;
    white-space: nowrap;
}
/* MS / OS columns sit content-sized side-by-side instead of splitting
   the available width 50/50. With `1fr 1fr`, a row with one MS name
   and an empty OS used to leave a huge gap between MS and OS — the
   user could read empty space as "more reservers go here". Sizing
   each column to content + a generous gap keeps them visually paired
   without that blank no-man's-land. */
.ar-cols { display: grid; grid-template-columns: max-content max-content; gap: var(--sp-6); justify-content: start; }
/* Minimum column width keeps MS and OS chunks lined up across rows even
   when one row's content is way shorter than another's (— vs. a long
   reserver list). Without this, every row's MS column had a different
   width and the columns visually drifted. */
.ar-cols .ar-col { min-width: 90px; }
/* All Reservations boss name sits directly under .wl-boss (the wishlist
   editor wraps it in .wl-boss-titlewrap with built-in spacing). Give it
   2px of breathing room before the .ar-list rows kick in. */
.wl-boss > .wl-boss-name { margin-bottom: var(--sp-1); }
.ar-row:has(.ar-col-shared) .ar-cols { grid-template-columns: 1fr; }
.ar-col-label { font-size: var(--fs-micro); letter-spacing: var(--tracking-wide); text-transform: uppercase; color: var(--c-text-faint); margin-bottom: 2px; }
.ar-col-ms .ar-col-label     { color: var(--c-good); }
.ar-col-os .ar-col-label     { color: var(--c-os); }
.ar-col-shared .ar-col-label { color: var(--c-special); }
/* Hard Reserve badge + controls on All Reservations rows. Officers
   set HR via the dropdown next to the slot pill; raiders see only
   the badge. Copper-amber color matches the existing HR convention
   from the (deprecated) Raid Mode award panel. */
.ar-hr-badge {
    display: inline-flex; align-items: center;
    margin-left: 8px;
    padding: 1px 6px;
    border-radius: 3px;
    background: rgba(232, 140, 77, 0.18);
    color: #e88c4d;
    font-size: var(--fs-micro);
    font-weight: 700;
    border: 1px solid rgba(232, 140, 77, 0.4);
    letter-spacing: 0.3px;
}
.ar-hr-controls {
    display: inline-flex; align-items: center; gap: 4px;
    margin-left: 8px;
}
.ar-hr-controls .admin-select {
    font-size: var(--fs-xs);
    padding: 2px 24px 2px 8px;
    line-height: 1.4;
    min-width: 0;
}
.ar-row-hr {
    background: linear-gradient(rgba(232, 140, 77, 0.06), rgba(232, 140, 77, 0.06)), var(--c-surface-2);
    border-color: rgba(232, 140, 77, 0.4);
}
.ar-name { font-size: var(--fs-sm); font-weight: 600; }
.ar-name-private { font-style: italic; opacity: 0.85; }
.ar-sep { color: var(--c-text-faint); margin: 0 6px; }
.ar-empty { color: var(--c-text-faint); font-size: var(--fs-sm); }
/* Wrapper so the +N bonus pill stays attached to its name when
   reservers wrap to a new line. Without this wrapper, the pill could
   detach from the name across line breaks and read as belonging to
   the next reserver. */
.ar-name-wrap { display: inline-flex; align-items: baseline; gap: 4px; }
/* Inline roll-bonus pill on All Reservations reserver names —
   replaces the +N column that used to live on Raid Mode's award
   panel. Accent color so officers can scan "who's owed loot" at a
   glance while the page is up live. Tabular-nums so two-digit
   bonuses don't shift the layout. */
.ar-bonus-pill {
    font-size: var(--fs-micro);
    font-weight: 700;
    color: var(--c-accent);
    font-variant-numeric: tabular-nums;
    padding: 1px 5px;
    border: 1px solid currentColor;
    border-radius: 9px;
    background: rgba(124, 156, 255, 0.08);
}

/* ── Raid Mode (Phase 3) ─────────────────────────────────────── */
.rm-toolbar { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; margin-bottom: var(--sp-3); }
.rm-help { color: var(--c-text-3); font-size: var(--fs-sm); margin: 0 0 var(--sp-4); line-height: 1.5; }
.rm-config-picker { min-width: 240px; }
.rm-att-chip {
    display: inline-flex; align-items: center;
    background: var(--c-surface-2); border: 1px solid var(--c-border);
    border-radius: var(--r-sm); padding: 2px 8px;
    font-size: var(--fs-xs); color: var(--c-text-faint);
}
.rm-spacer { flex: 1; }
/* Loot Export toolbar layout on phones: stack the "Export" label
   above the format seg-toggle (TMB / Boosted / PlusOnes), both
   left-anchored at the page content edge so they line up with the
   raid config picker below. Desktop unchanged. */
@media (max-width: 520px) {
    /* Mobile only: hoist the "How does X work?" callout above the
       TMB / Boosted / PlusOnes toolbar via flex order. Desktop
       keeps the original DOM order (toolbar first, help below) so
       the help text doesn't push the format buttons offscreen on
       wide viewports. Same treatment on the wishlist page so the
       help callout sits above the filter strip. */
    #loot-export-root,
    #loot-wishlist-root {
        display: flex;
        flex-direction: column;
    }
    #loot-export-root .wl-help-row,
    #loot-wishlist-root .wl-help-row { order: -1; }
    /* Tighten the gap above the "How does X work?" help callout on
       Wishlist and Export mobile — the base 20px reads as too much
       air on phones (per user feedback). Import keeps the base
       margin since it sits inside plain block flow rather than the
       flex-column wrappers Wishlist/Export use. */
    #loot-export-root .wl-help-row,
    #loot-wishlist-root .wl-help-row {
        margin-top: var(--sp-1);
    }
    #loot-export-root .rm-toolbar {
        flex-direction: column;
        align-items: flex-start;
        gap: var(--sp-2);
    }
    /* Stretch the raid-config + player-view <select> dropdowns on
       Loot Export and Loot Import to fill the full content width
       on phones — saves the user from a tiny dropdown crammed at
       the left edge of a wide row. width:100% + min-width:0 wins
       over the inline min-width:280px on the export config select. */
    #loot-export-root .admin-select,
    #loot-import-root .admin-select,
    #loot-history-root .admin-select,
    #loot-history-root input[type="search"],
    #loot-export-root input[type="search"],
    #loot-import-root input[type="search"],
    #loot-wishlist-root input[type="search"] {
        width: 100%;
        min-width: 0;
    }
}
/* Loot Export footer stat — N chars · N wishlist items · N HRs —
   N ranks. Inline on desktop with " · " separators between spans;
   on phones each span gets its own line so the digits read at a
   glance instead of crammed into a single wrapped paragraph. */
.loot-export-stats { display: inline-flex; align-items: center; flex-wrap: wrap; }
.loot-export-stats > span:not(:first-child)::before {
    content: '·';
    color: var(--c-text-faint);
    margin: 0 6px;
}
@media (max-width: 520px) {
    .loot-export-stats {
        flex-direction: column;
        align-items: flex-end;
    }
    .loot-export-stats > span:not(:first-child)::before {
        content: none;
        margin: 0;
    }
}

.rm-bosses { display: flex; flex-direction: column; gap: var(--sp-3); }
.rm-boss { background: var(--c-surface); border: 1px solid var(--c-border); border-radius: var(--r-md); }
.rm-boss[open] { border-color: var(--c-border-strong, var(--c-border)); }
.rm-boss-head {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: 8px 12px; cursor: pointer; list-style: none;
}
.rm-boss-head::-webkit-details-marker { display: none; }
.rm-boss-head .wl-boss-thumb { width: 36px; height: 36px; border-radius: var(--r-sm); }
.rm-boss-name { font-weight: 700; font-size: var(--fs-md); flex: 1; }
.rm-boss-meta { font-size: var(--fs-xs); color: var(--c-text-faint); }
.rm-boss-items { padding: 0 12px 10px; display: flex; flex-direction: column; gap: 6px; }

/* Per-item row in Raid Mode. Mirrors .wl-row's flex layout so the
   Wowhead-rendered item name (inside the .wl-item anchor) gets all the
   horizontal room left over after the slot label, bucket chip, and
   Award button. The earlier grid version (36px first column) crushed
   the item name into a 3-line wrapped column. */
.rm-item {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    padding: 6px 10px;
}
.rm-item .wl-slot { font-size: var(--fs-xs); color: var(--c-text-faint); display: inline-flex; gap: 4px; align-items: center; flex-shrink: 0; }
.rm-item-mid { display: inline-flex; gap: var(--sp-3); align-items: center; flex-wrap: wrap; flex-shrink: 0; }
.rm-item-bucket {
    display: inline-block; padding: 1px 8px; border-radius: var(--r-sm);
    font-size: var(--fs-micro); font-weight: 700; letter-spacing: var(--tracking-wide); text-transform: uppercase;
    border: 1px solid currentColor;
}
.rm-item-bucket-ms      { color: var(--c-good); }
.rm-item-bucket-os      { color: var(--c-os); }
.rm-item-bucket-shared  { color: var(--c-special); }
.rm-item-bucket-mixed   { color: var(--c-text-faint); }
.rm-item-bucket-none    { color: var(--c-text-faint); opacity: 0.7; }
/* Hard-reserved item — copper amber stands out from the bucket colors
   so the eye catches HR'd rows instantly while scanning. The row itself
   gets a subtle amber tint so HR'd items clearly diverge from normal
   contention. */
.rm-item-bucket-hr      { color: #e88c4d; font-weight: 800; letter-spacing: var(--tracking-wide); }
.rm-item.rm-item-hr     { border-color: rgba(232,140,77,0.45); background: rgba(232,140,77,0.06); }
.rm-item-winner { font-weight: 600; color: var(--c-text); font-size: var(--fs-sm); }

/* Award panel — inline rather than a modal so the officer keeps the boss
   list visible while resolving an item. Sits at the bottom of #raid-mode-root. */
.rm-award {
    margin-top: var(--sp-4);
    background: var(--c-surface); border: 1px solid var(--c-accent);
    border-radius: var(--r-md); padding: var(--sp-3) var(--sp-4);
}
.rm-award-head { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-3); flex-wrap: wrap; }
.rm-award-bucket {
    display: inline-block; padding: 2px 10px; border-radius: var(--r-sm);
    font-size: var(--fs-xs); font-weight: 700; letter-spacing: var(--tracking-wide); text-transform: uppercase;
    border: 1px solid currentColor;
}
.rm-award-bucket-ms      { color: var(--c-good); }
.rm-award-bucket-os      { color: var(--c-os); }
.rm-award-bucket-shared  { color: var(--c-special); }
.rm-award-bucket-mixed   { color: var(--c-text-faint); }
.rm-award-bucket-none    { color: var(--c-text-faint); }
.rm-award-bucket-hr      { color: #e88c4d; }
.rm-award-row-hr {
    border-color: rgba(232,140,77,0.55);
    background: rgba(232,140,77,0.08);
}
.rm-award-row-hr .rm-award-rank {
    color: #e88c4d; font-weight: 800; letter-spacing: var(--tracking-wide);
}
/* Header above the natural-contender list when an HR is set — flagging
   that picking one of these rows fires a confirm + records the
   override in the history entry. */
.rm-award-override-head {
    margin-top: var(--sp-3);
    font-size: var(--fs-micro);
    text-transform: uppercase; letter-spacing: var(--tracking-wide);
    color: var(--c-text-faint);
    padding: 2px 4px;
}
/* HR controls at the bottom of the award panel: set or clear the hard
   reserve. Single-line layout when narrow viewport flexes the items. */
.rm-award-hr {
    display: flex; align-items: center; gap: var(--sp-2);
    margin-top: var(--sp-3);
    padding: 8px 10px;
    background: rgba(232,140,77,0.06);
    border: 1px dashed rgba(232,140,77,0.45);
    border-radius: var(--r-sm);
    flex-wrap: wrap;
}
.rm-award-hr-label {
    font-size: var(--fs-xs); color: var(--c-text-2);
    text-transform: uppercase; letter-spacing: var(--tracking-wide);
}
.rm-hr-select { min-width: 160px; }

/* ── Import (Phase 5) ──────────────────────────────────────── */
.import-preview { display: flex; flex-direction: column; gap: 4px; margin-top: var(--sp-4); }
.import-section-label {
    font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: var(--tracking-wide);
    margin-bottom: 4px; font-weight: 700;
}
.import-row {
    display: flex; align-items: center; gap: var(--sp-3);
    background: var(--c-surface-2); border: 1px solid var(--c-border);
    border-radius: var(--r-sm); padding: 6px 10px;
    font-size: var(--fs-sm);
}
/* Wrapper for stacks of import rows — matches the history page's
   .lh-rows treatment so spacing is consistent across both views. The
   6px flex gap separates rows without doubling up borders, and the
   wrapper takes the leading margin so individual rows don't have to
   carry their own margin-bottom logic. */
.import-rows { display: flex; flex-direction: column; gap: 6px; margin-top: var(--sp-2); }
.import-row-line {
    color: var(--c-text-faint); font-size: var(--fs-xs);
    min-width: 28px; text-align: right; font-variant-numeric: tabular-nums;
}
.import-row-arrow { color: var(--c-text-faint); }
.import-row-player { font-weight: 600; }
.import-row-bucket {
    font-size: var(--fs-micro); font-weight: 700;
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    padding: 1px 7px; border-radius: var(--r-sm); border: 1px solid currentColor;
}
.import-bucket-ms     { color: var(--c-good); }
.import-bucket-os     { color: var(--c-os); }
.import-bucket-shared { color: var(--c-special); }
.import-bucket-hr     { color: #e88c4d; }
.import-row-date    { color: var(--c-text-faint); font-size: var(--fs-xs); }
.import-row-err     { border-color: rgba(232,112,112,0.45); background: rgba(232,112,112,0.06); }
.import-row-skip    { border-color: var(--c-border); background: rgba(106,125,150,0.06); opacity: 0.75; }
/* Per-row import checkbox: each ok-row is now a label so clicking
   anywhere on the row toggles selection. Unticked rows dim out so
   the eye scans down to "what will actually commit". */
.import-row.import-row-ok { cursor: pointer; opacity: 0.55; transition: opacity 0.1s, border-color 0.1s; }
.import-row.import-row-ok.is-checked { opacity: 1; border-color: var(--c-accent); }
.import-row .import-row-check { margin-right: 4px; cursor: pointer; }
/* DE rows have a different chrome — desaturated background, dashed
   border to flag "no recipient", and the "Disenchanted" pill. */
.import-row.import-row-de {
    background: rgba(180, 120, 60, 0.05);
    border-color: rgba(180, 120, 60, 0.30);
    border-style: dashed;
}
.import-row.import-row-de.is-checked { border-color: rgba(180, 120, 60, 0.7); }
.import-row-de-pill {
    font-size: var(--fs-micro); font-weight: 700;
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    padding: 1px 7px; border-radius: var(--r-sm);
    border: 1px solid rgba(180, 120, 60, 0.7);
    color: rgb(220, 160, 90);
    background: rgba(180, 120, 60, 0.08);
}
/* Already-imported state on a parsed row. Dimmed treatment so the row
   reads as "informational, not actionable" — same visual weight as
   skipped rows but in a muted (not warn) tone since these aren't
   errors, just dupes against existing history. .import-row-already-pill
   sits in the right cluster where the boost cell would normally go,
   reading "Already imported" in muted text. The row override is more
   specific than the .import-row.import-row-ok dim/checked rules so it
   stays at consistent ~0.7 opacity regardless of selection state. */
.import-row.import-row-already {
    cursor: default;
    opacity: 0.7;
    background: var(--c-surface);
    border-style: dashed;
}
.import-row-already-pill {
    display: inline-flex;
    align-items: center;
    font-size: var(--fs-micro); font-weight: 700;
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    padding: 1px 7px; border-radius: var(--r-sm);
    border: 1px dashed var(--c-border);
    color: var(--c-text-faint);
    background: transparent;
    flex-shrink: 0;
    line-height: 1.4;
    cursor: help;
}
/* Raid-group header in the Loot Import preview. Multi-raid pastes
   (Karazhan + Gruul + Mag chained together in a single Gargul JSON
   export) render with a raid-name banner per cluster of items so the
   user can scan "this is the Gruul drops, this is the Mag drops"
   instead of mentally re-parsing every row. Sub-header style — quiet
   uppercase label, no card chrome, just a thin tinted divider above
   so it visually separates from the prior group without being loud. */
.import-raid-group { margin-top: var(--sp-3); }
.import-raid-group:first-child { margin-top: 0; }
.import-raid-group-head {
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-2);
    font-weight: 700;
    padding: 7px 0 4px; /* +3px above so title breathes from the divider line */
    border-top: 1px solid var(--c-border);
    margin-bottom: 2px;
}
.import-raid-group:first-child .import-raid-group-head { border-top: 0; padding-top: 0; }
.import-raid-group-count {
    font-size: var(--fs-micro);
    color: var(--c-text-faint);
    font-weight: 600;
    margin-left: 6px;
    letter-spacing: 0;
    text-transform: none;
}
.import-raid-group .import-rows { margin-top: 4px; }
/* PUG tag next to a recipient name in the Import preview — flags
   one-night joiners (no roster row, but matches a pug_name in
   PUG_WISHLISTS). Soft accent-tinted chip to distinguish from regular
   roster awards without screaming for attention. */
.import-row-pug-tag {
    font-size: var(--fs-micro); font-weight: 700;
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    padding: 1px 6px; border-radius: var(--r-sm);
    border: 1px solid var(--c-accent);
    color: var(--c-accent);
    background: rgba(51, 228, 222, 0.08);
    margin-left: 6px;
}
/* Duplicate-collapsed flag — same player/item/date appearing twice. */
.import-row-dup {
    font-size: var(--fs-micro); font-weight: 700;
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    padding: 1px 6px; border-radius: var(--r-sm);
    border: 1px solid rgba(232, 200, 90, 0.5);
    color: rgb(232, 200, 90);
    background: rgba(232, 200, 90, 0.08);
    cursor: help;
}
.import-row.import-row-has-dup { border-color: rgba(232, 200, 90, 0.5); }
/* "Auto-detected: <config>" badge next to the scope picker. Surfaces
   loudly when our scoring pass picked the config; disappears once the
   user explicitly chooses one via the dropdown. Warn-yellow tone so
   it reads as informational (not error) but still grabs the eye. */
.import-config-detected {
    font-size: var(--fs-xs);
    color: #E5CB7F;  /* WoW quest yellow */
    background: rgba(229, 203, 127, 0.10);
    border: 1px solid rgba(229, 203, 127, 0.40);
    border-radius: var(--r-sm);
    padding: 2px 8px;
    font-weight: 600;
}
.import-config-detected strong { color: #ffd966; }
/* Auto-detect refused to guess (no config near the loot's date) — red-toned
   variant of the badge so it reads as "action needed", not info. */
.import-config-detected.import-config-nomatch {
    color: var(--c-bad);
    background: rgba(232, 112, 112, 0.10);
    border-color: rgba(232, 112, 112, 0.45);
}
.import-config-detected.import-config-nomatch strong { color: var(--c-bad); }
/* Raids-detected chips: surfaces "Gruul · 5", "Magtheridon · 3" under
   the scope picker so officers can see what raids the paste covers
   and whether the selected config covers all of them. .out variant
   (with ⚠) flags a raid that's NOT covered by the current scope. */
.import-raid-chip {
    font-size: var(--fs-xs);
    color: var(--c-text-2);
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-sm);
    padding: 2px 8px;
    font-weight: 600;
}
.import-raid-chip.out {
    color: var(--c-warn);
    background: rgba(212, 170, 68, 0.10);
    border-color: rgba(212, 170, 68, 0.40);
}
/* Date-mismatch banner: warns when the selected raid config's date is far
   from the pasted loot's dates (the "imported June loot into a stale May
   config" trap). Full-width, warn-toned with a left accent so it reads as
   "stop and check" without looking like a hard error. */
.import-date-warn {
    width: 100%;
    font-size: var(--fs-xs);
    color: var(--c-warn);
    background: rgba(212, 170, 68, 0.12);
    border: 1px solid rgba(212, 170, 68, 0.45);
    border-left: 3px solid var(--c-warn);
    border-radius: var(--r-sm);
    padding: var(--sp-2) var(--sp-3);
    margin-bottom: var(--sp-3);
    font-weight: 600;
}
.import-date-warn strong { color: #ffd966; }
/* Right-edge cluster: roll value + boost cell + date all packed
   together at the right end of the row. Wrapper takes margin-left:
   auto so the children pack normally inside (instead of each child
   fighting for the slack with their own auto-margins). */
.import-row-right {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-3);
    margin-left: auto;
    flex-shrink: 0;
}
/* Roll value — only renders when JSON ships ground-truth roll data
   (Gargul's Rolls[] array). CSV imports never show this. Reads as
   "🎲 117 (MS)" / "🎲 44 (OS)". inline-flex + same padding as the
   boost pill so both right-cluster pills sit at identical height
   and vertically-center next to the date. */
.import-row-roll {
    display: inline-flex;
    align-items: center;
    font-size: var(--fs-micro); font-weight: 700;
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    padding: 1px 7px; border-radius: var(--r-sm);
    border: 1px solid rgba(91, 155, 213, 0.4);
    color: rgb(140, 195, 255);
    background: rgba(91, 155, 213, 0.06);
    cursor: help;
    flex-shrink: 0;
    line-height: 1.4;
}
/* Per-row boost toggle — clickable button on the right edge of the row.
   `margin-left: auto` consumes all the row's slack so the boost cell
   sticks to the right end regardless of how wide the bucket / date /
   recipient cluster on the left grew. ON state shows "⚡ −10 Points";
   OFF state shows just "0 Points". Auto-defaulted at parse time
   based on raid-scoped contest count (≥2 reservers → ON, else OFF). */
.import-row-boost {
    display: inline-flex;
    align-items: center;
    font-size: var(--fs-micro); font-weight: 700;
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    padding: 1px 7px; border-radius: var(--r-sm);
    border: 1px solid var(--c-border);
    background: var(--c-surface);
    color: var(--c-text-faint);
    cursor: pointer;
    flex-shrink: 0;
    line-height: 1.4;
    /* margin-left: auto removed — the parent .import-row-right wrapper
       handles the right-edge alignment now. */
    transition: color 0.1s, border-color 0.1s;
}
.import-row-boost:hover { color: var(--c-text); border-color: var(--c-text-faint); }
.import-row-boost.is-boosted {
    color: var(--c-accent);
    border-color: rgba(51, 228, 222, 0.55);
    background: rgba(51, 228, 222, 0.06);
}
/* Hide the 0 Points / N Points boost pill on phones — saves
   horizontal room in the import-row's right-cluster, and the
   boost state is mostly an officer-side concern that's clearer
   on a desktop session. The toggle behavior remains accessible
   on desktop. */
@media (max-width: 520px) {
    .import-row .import-row-boost { display: none; }
}
.import-row-raw     { font-family: monospace; font-size: var(--fs-xs); color: var(--c-text-2); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.import-row-error   { color: var(--c-bad); font-size: var(--fs-xs); }
/* Format banner — sits at the top of the preview when Gargul export
   is auto-detected. Unobtrusive accent so the user knows the parser
   recognized their format. */
.import-format-banner {
    font-size: var(--fs-xs);
    color: var(--c-text-2);
    background: rgba(51, 228, 222, 0.06);
    border: 1px solid rgba(51, 228, 222, 0.30);
    border-radius: var(--r-sm);
    padding: 4px 10px;
    margin-bottom: var(--sp-2);
    width: fit-content;
}

/* ── History (Phase 6) ──────────────────────────────────────── */
.lh-rows { display: flex; flex-direction: column; gap: 6px; margin-top: var(--sp-3); }
.lh-row {
    display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
    background: var(--c-surface-2); border: 1px solid var(--c-border);
    border-radius: var(--r-sm); padding: 6px 10px;
    font-size: var(--fs-sm);
}
.lh-row-voided { opacity: 0.5; text-decoration: line-through; text-decoration-color: var(--c-bad); }
.lh-date {
    color: var(--c-text-faint); font-size: var(--fs-xs);
    min-width: 92px; font-variant-numeric: tabular-nums;
}
.lh-arrow { color: var(--c-text-faint); }
/* Recipient cluster wraps the player link + PUG tag + bucket pill so
   they sit TIGHTLY (6px internal gap) instead of inheriting the row's
   12px gap that pushed every pill apart. Reads as "Achtungg OS PUG"
   tightly together rather than "Achtungg    OS    PUG" with air
   between every chip. */
.lh-recipient {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    min-width: 0;
}
/* Drop the .import-row-pug-tag's default margin-left:6px when the
   tag lives inside a History recipient cluster — flex gap already
   handles spacing, so the extra margin doubled up to 12px. */
.lh-recipient .import-row-pug-tag { margin-left: 0; }
/* Same override inside a Loot Import row — when PUG sits as a sibling
   of the bucket pill, the row's flex gap (12px) is already enough; the
   tag's default 6px margin would double the spacing. */
.import-row .import-row-pug-tag { margin-left: 0; }
/* Drop the old min-width: 100px — forced the player link to 100px
   wide which left dead whitespace between the name and the bucket
   pill. The .lh-recipient wrapper now governs spacing inside the
   recipient cluster. */
.lh-player { font-weight: 700; }
.lh-bucket {
    font-size: var(--fs-micro); font-weight: 700;
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    padding: 1px 7px; border-radius: var(--r-sm); border: 1px solid currentColor;
    flex-shrink: 0;
}
.lh-bucket-ms     { color: var(--c-good); }
.lh-bucket-os     { color: var(--c-os); }
.lh-bucket-shared { color: var(--c-special); }
.lh-bucket-hr     { color: #e88c4d; }
.lh-bucket-none   { color: var(--c-text-faint); opacity: 0.6; }
/* Raid + boss label sits in the right cluster now (margin-left: auto
   pushes it past the score cell so the raid·boss + flag chips +
   awarded-by all align right). The flex: 0 keeps it from stretching
   to fill remaining slack; raid labels with long boss names just
   ellipsize gracefully. */
.lh-raid     { color: var(--c-text-faint); font-size: var(--fs-xs); margin-left: auto; max-width: 60%; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; }
/* Right-cluster wrapper used on the profile loot section to keep the
   roll badge adjacent to the raid · boss label (both flush right) so
   the roll doesn't float in the middle of the row. The wrapper takes
   margin-left: auto, so .lh-raid inside it loses its own auto margin
   to avoid double-pushing. */
.lh-row-right { margin-left: auto; display: inline-flex; gap: var(--sp-2); align-items: center; min-width: 0; }
.lh-row-right .lh-raid { margin-left: 0; max-width: none; }
/* GM-only "drop this award" ✕ on loot-history + boss-loot-popup rows. */
.loot-void-x {
    flex-shrink: 0; cursor: pointer; color: var(--c-text-3);
    font-size: var(--fs-sm); line-height: 1; padding: 2px 5px; border-radius: 4px;
    border: 1px solid transparent; user-select: none;
}
.loot-void-x:hover { color: #fff; background: var(--c-bad); border-color: var(--c-bad); }
/* GM-only "permanently remove" ✕ on an ALREADY-voided loot row — hard-deletes
   that single line. Persistent red border (vs the soft-void ✕) so it reads as
   a delete; text-decoration:none keeps it legible on the struck-through row. */
.loot-clear-x {
    flex-shrink: 0; cursor: pointer; color: var(--c-bad);
    font-size: var(--fs-sm); line-height: 1; padding: 2px 5px; border-radius: 4px;
    border: 1px solid var(--c-bad); user-select: none;
    /* Make the ✕ its own flex container: an ancestor's line-through (from the
       voided row) is NOT propagated into a flex container's contents, so the
       glyph isn't struck through. (inline-block doesn't help here — as a flex
       ITEM it blockifies to `block`, which the strike still paints through.) */
    display: inline-flex; align-items: center; justify-content: center;
    text-decoration: none; font-weight: 700;
}
.loot-clear-x:hover { color: #fff; background: var(--c-bad); }
/* Spec picker on raid-builder slots — small modal with one button
   per spec. Active spec highlighted in accent. Hover lights up the
   border so the click target reads as obvious. */
.slot-spec-list { display: flex; flex-direction: column; gap: 4px; max-height: 320px; overflow-y: auto; margin-bottom: 10px; }
.slot-spec-row {
    font-family: inherit;
    text-align: left;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    color: var(--c-text-2);
    border-radius: 4px;
    padding: 6px 10px;
    font-size: var(--fs-sm);
    cursor: pointer;
    transition: background 0.1s, border-color 0.1s, color 0.1s;
}
.slot-spec-row:hover { border-color: var(--c-accent); color: var(--c-text); }
.slot-spec-row.is-active { background: rgba(51,228,222,0.12); border-color: var(--c-accent); color: var(--c-accent); font-weight: 700; }
.slot-spec-pick:hover { color: var(--c-accent) !important; }
/* Boss name takes a brighter shade than the muted raid prefix so
   "Karazhan @ 2026-04-27 [2] · Moroes" reads with Moroes as the
   eye-catching detail (the raid + date is repetitive context the
   user already knows from the section header above). */
.lh-raid .lh-boss { color: var(--c-text-2); font-weight: 600; }
.lh-score    { color: var(--c-text); font-weight: 700; font-variant-numeric: tabular-nums; min-width: 36px; text-align: right; }
.lh-score-na { color: var(--c-text-faint); font-weight: 400; }
.lh-by       { color: var(--c-text-faint); font-size: var(--fs-xs); }
.lh-flag {
    font-size: var(--fs-micro); font-weight: 700;
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    padding: 1px 6px; border-radius: var(--r-sm);
    background: var(--c-surface); border: 1px solid var(--c-border);
}
.lh-flag-imported    { color: var(--c-os); border-color: rgba(91,155,213,0.45); }
.lh-flag-hr          { color: #e88c4d; border-color: rgba(232,140,77,0.45); }
.lh-flag-hr-override { color: var(--c-bad); border-color: rgba(232,112,112,0.45); }
.lh-flag-voided      { color: var(--c-bad); border-color: rgba(232,112,112,0.45); }
.lh-flag-boosted     { color: var(--c-accent); border-color: rgba(51,228,222,0.45); }
.lh-flag-dup         { color: rgb(232, 200, 90); border-color: rgba(232, 200, 90, 0.5); }

/* ── Mobile (≤520px): loot-history row stacks into 3 logical lines ──
       line 1: [icon] item-name → recipient [MS/OS] [PUG]
       line 2: raid · boss (left-aligned, flush against the row's left edge)
       line 3: imported by Achtungg (flag chips + "by …")
   Desktop right-aligns .lh-raid via margin-left:auto + text-align:right so
   it floats to the row's right end. On phones the row wraps and that left
   an awkward empty gap on the LEFT side of the raid line; flipping to
   flex: 0 0 100% + margin-left:0 + text-align:left puts it flush left on
   its own row. The item name is set to flex-grow with min-width:0 so its
   inner ellipsis kicks in when the row would otherwise push the recipient
   onto its own line — keeps "name → player [MS] [PUG]" together. */
@media (max-width: 520px) {
    .lh-row { column-gap: 6px; row-gap: 4px; }
    .lh-row .wl-item { flex: 1 1 0; min-width: 0; }
    .lh-row .lh-arrow { flex: 0 0 auto; }
    .lh-row .lh-recipient { flex: 0 0 auto; flex-wrap: nowrap; }
    .lh-row .lh-raid {
        flex: 0 0 100%;
        margin-left: 0;
        max-width: 100%;
        text-align: left;
    }
    /* Drop line 3 (flag chips + "by …") on phones. Minimal context
       value at this width, took a whole vertical row, and most rows
       only carry the "imported" chip which is already implied on the
       History page. Keeps each row to 2 lines: item→recipient + raid. */
    .lh-row .lh-flag,
    .lh-row .lh-by { display: none; }
    /* Drop the dice-roll badge on phones — adds horizontal noise to a
       row that's already tight, and the roll is recoverable on the
       loot history / boss-loot modal on desktop. Applies anywhere
       wlItemAnchor rows render rolls (profile loot, boss-loot modal,
       import preview, history). */
    .import-row-roll { display: none; }
    /* Drop the redundant gray date on bl-group section headers
       (collapsed AND expanded) — the date is already baked into the
       section label (e.g. "Karazhan @ 2026-04-27 [2]"), and the extra
       88px-min-width date cell was forcing the header to line-wrap
       on narrow phones. Hiding always (rather than only when closed)
       avoids the layout shift that would happen mid-tap when the
       section opens. */
    .bl-group .bl-group-date { display: none; }
    /* Force the leading "History" title on its own row in the toolbar
       so the player / raid / search controls stack underneath it
       instead of crowding next to the title. Matches the user mental
       model: the title is the page heading, the controls are the
       toolbar beneath it. */
    .lh-toolbar > .loot-section-label:first-child { flex-basis: 100%; }
}
/* Per-raid sections — each raid renders as its own block with a
   header row (raid name + award count + DE'd count chip). DE'd
   items get a sub-block with their own subhead so officers can
   eyeball which items got vendor'd/sharded per raid. */
/* History → "Crafting loot — where it went" aggregate. Recipes + reagents
   (Nether Vortex, etc.) awarded in the current view, grouped so officers can
   see who holds what and plan crafting. */
.lh-craft { margin-top: var(--sp-3); border: 1px solid color-mix(in srgb, var(--c-accent) 30%, var(--c-border)); border-radius: var(--r-md); background: color-mix(in srgb, var(--c-accent) 5%, var(--c-surface)); overflow: hidden; }
.lh-craft-summary { list-style: none; cursor: pointer; display: flex; align-items: baseline; gap: var(--sp-2); padding: 9px 12px; user-select: none; }
.lh-craft-summary::-webkit-details-marker { display: none; }
.lh-craft-summary::before { content: '▸'; color: var(--c-accent); font-size: var(--fs-xs); transition: transform 0.12s; }
.lh-craft[open] > .lh-craft-summary::before { transform: rotate(90deg); }
.lh-craft-title { font-size: var(--fs-sm); font-weight: 700; color: var(--c-text); letter-spacing: 0.02em; }
.lh-craft-sub { font-size: var(--fs-xs); color: var(--c-text-faint); margin-left: auto; }
.lh-craft-body { padding: 4px 12px 12px; }
.lh-craft-group { margin-top: 10px; }
.lh-craft-group-head { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.05em; color: var(--c-text-2); font-weight: 700; border-bottom: 1px solid var(--c-border); padding-bottom: 3px; margin-bottom: 5px; }
.lh-craft-gcount { color: var(--c-accent); font-weight: 700; }
.lh-craft-hint { text-transform: none; letter-spacing: 0; font-weight: 400; color: var(--c-text-faint); }
.lh-craft-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; padding: 4px 0; border-top: 1px solid color-mix(in srgb, var(--c-border) 55%, transparent); }
.lh-craft-group > .lh-craft-row:first-of-type { border-top: none; }
.lh-craft-item { display: flex; align-items: center; gap: 4px; min-width: 220px; flex: 0 0 auto; }
.lh-craft-total { font-size: var(--fs-xs); font-weight: 700; color: var(--c-accent); font-variant-numeric: tabular-nums; }
.lh-craft-awards { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: center; font-size: var(--fs-xs); }
.lh-craft-award { display: inline-flex; align-items: baseline; gap: 4px; background: color-mix(in srgb, var(--c-border) 30%, transparent); border-radius: var(--r-sm); padding: 1px 7px; }
.lh-craft-award .player-link { font-size: var(--fs-xs); }
.lh-craft-arrow { color: var(--c-text-faint); align-self: center; }
.lh-craft-awardmeta { color: var(--c-text-faint); white-space: nowrap; }
.lh-sections { display: flex; flex-direction: column; gap: 6px; margin-top: var(--sp-3); }
/* Each raid renders as a <details> accordion. Closed by default for
   all but the first (most-recent) raid so officers can scan the list
   of raid headings and expand only the one they want. The header is
   the <summary> — clicking anywhere on it toggles open/closed. We hide
   the default browser caret (::-webkit-details-marker / list-style)
   and supply our own ▶ that rotates 90° via CSS when the section
   opens. */
.lh-section { display: block; margin-bottom: var(--sp-2); }
.lh-section > summary { list-style: none; }
.lh-section > summary::-webkit-details-marker { display: none; }
.lh-section-header {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: 6px 10px;
    background: var(--c-surface-3, var(--c-surface));
    border: 1px solid var(--c-border);
    border-radius: var(--r-sm);
    border-left: 3px solid var(--c-accent);
    cursor: pointer;
    user-select: none;
    transition: background 0.1s;
}
.lh-section-header:hover {
    background: var(--c-surface-2);
}
.lh-section-caret {
    display: inline-block;
    font-size: var(--fs-xs);
    color: var(--c-text-faint);
    width: 12px;
    transition: transform 0.15s ease-out;
    transform-origin: center;
}
.lh-section[open] > summary .lh-section-caret {
    transform: rotate(90deg);
}
.lh-section-body {
    padding: var(--sp-3) 0 var(--sp-2);
}
/* History page sections now reuse the bl-group accordion chrome (date
   pill + label + meta), same as the progression modal + profile loot
   section. The body lives inside the rounded bl-group container, so
   horizontal padding lines up with the header text and the rows
   don't sit flush against the border. */
.bl-group > .lh-section-body { padding: 4px 12px 12px; }
.bl-group > .lh-section-body > .lh-rows { margin-top: 0; }
.bl-group > .lh-section-body > .lh-de-block { margin-top: var(--sp-3); }
/* Per-raid sub-banner inside a multi-raid history section. Uppercase
   quiet label that splits Gruul/Mag combo nights into 2 sub-blocks
   under one accordion. No top divider line — the parent .bl-group
   accordion already provides visual containment, so an extra divider
   read as noise. 7px top padding gives the title breathing room
   above where the divider used to be. */
.lh-raid-subgroup { margin-top: var(--sp-3); }
.lh-raid-subgroup:first-child { margin-top: 0; }
.bl-group > .lh-section-body > .lh-raid-subgroup:first-child { margin-top: 0; }
.lh-raid-subgroup-head {
    font-size: var(--fs-xs);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--c-text-2);
    font-weight: 700;
    padding: 7px 0 4px;
    margin-bottom: 2px;
}
.lh-raid-subgroup-count {
    font-size: var(--fs-micro);
    color: var(--c-text-faint);
    font-weight: 600;
    margin-left: 6px;
    letter-spacing: 0;
    text-transform: none;
}
.lh-raid-subgroup .lh-rows { margin-top: 4px; }
/* Loot history search input — wider than the default ar-search baseline
   so officers have room to type a full item name + raid + boss search.
   `flex: 1` lets it stretch into the remaining toolbar slack, with a
   sensible min so it doesn't shrink to nothing on narrow viewports. */
.lh-search {
    flex: 1 1 auto;
    min-width: 320px;
}
.lh-section-name {
    font-size: var(--fs-md);
    font-weight: 700;
    color: var(--c-text);
    letter-spacing: var(--tracking-tight);
}
.lh-section-count {
    font-size: var(--fs-xs);
    color: var(--c-text-faint);
    font-variant-numeric: tabular-nums;
}
.lh-section-de {
    font-size: var(--fs-micro); font-weight: 700;
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    padding: 1px 7px; border-radius: var(--r-sm);
    color: rgb(220, 160, 90);
    background: rgba(180, 120, 60, 0.10);
    border: 1px solid rgba(180, 120, 60, 0.40);
    margin-left: auto;
}
/* DE sub-block within a raid section — clearly demarcated so the
   user sees which items got DE'd inline with the awards. */
.lh-de-block { display: flex; flex-direction: column; gap: var(--sp-2); }
.lh-de-subhead {
    font-size: var(--fs-xs);
    color: rgb(220, 160, 90);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    font-weight: 700;
    padding: 2px 4px;
}
/* Matching subheads above the awarded rows + DE rows inside each
   per-raid sub-group — replaces the old per-banner count chip with
   a small label cluster ("Items (4)" / "Disenchanted (14)") tucked
   close to the rows they describe. Both labels share the same shape;
   the DE variant takes the amber tint to match the existing DE row
   chrome. */
.lh-block-subhead {
    font-size: var(--fs-xs);
    color: var(--c-text-faint);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    font-weight: 700;
    padding: 6px 4px 2px;
}
.lh-block-subhead-de { color: rgb(220, 160, 90); }
/* DE history rows — desaturated, dashed border so they read as
   "tracked but no recipient". */
.lh-row-de {
    background: rgba(180, 120, 60, 0.05);
    border-color: rgba(180, 120, 60, 0.30);
    border-style: dashed;
}
.lh-de-pill {
    font-size: var(--fs-micro); font-weight: 700;
    letter-spacing: var(--tracking-wide); text-transform: uppercase;
    padding: 1px 7px; border-radius: var(--r-sm);
    border: 1px solid rgba(180, 120, 60, 0.7);
    color: rgb(220, 160, 90);
    background: rgba(180, 120, 60, 0.08);
    min-width: 100px;
    text-align: center;
}
/* +1 leaderboard row above the history list — top 5 highest counts. */
.lh-plus-row {
    display: flex; align-items: center; gap: var(--sp-2);
    flex-wrap: wrap;
    margin-bottom: var(--sp-3);
    padding: 6px 10px;
    background: var(--c-surface-2); border: 1px solid var(--c-border);
    border-radius: var(--r-sm);
}
.lh-plus-chip {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 2px 8px;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--r-sm);
    font-size: var(--fs-xs);
}
.lh-plus-chip strong {
    color: var(--c-accent);
    font-variant-numeric: tabular-nums;
}
/* Per-row +1 chip — recipient's cumulative count after this award. */
.lh-plusone {
    font-size: var(--fs-xs);
    font-weight: 700;
    color: var(--c-accent);
    font-variant-numeric: tabular-nums;
    padding: 1px 6px;
    background: rgba(51,228,222,0.10);
    border: 1px solid rgba(51,228,222,0.35);
    border-radius: var(--r-sm);
}
.rm-award-tie {
    background: var(--c-surface-2); border: 1px dashed var(--c-warn);
    border-radius: var(--r-sm); padding: 6px 10px; font-size: var(--fs-sm);
    color: var(--c-text); margin-bottom: var(--sp-2);
}
.rm-award-tie code { background: var(--c-surface); padding: 1px 5px; border-radius: 3px; font-size: var(--fs-xs); }
.rm-award-rows { display: flex; flex-direction: column; gap: 4px; }
.rm-award-row {
    display: grid;
    grid-template-columns: 28px minmax(120px, 1fr) 56px auto auto auto;
    gap: var(--sp-3); align-items: center;
    background: var(--c-surface-2); border: 1px solid var(--c-border);
    border-radius: var(--r-sm); padding: 5px 10px;
    font-size: var(--fs-sm);
}
.rm-award-row-winner { border-color: var(--c-accent); background: rgba(124, 156, 255, 0.06); }
.rm-award-row-tied   { border-color: var(--c-warn); }
.rm-award-rank   { color: var(--c-text-faint); font-variant-numeric: tabular-nums; text-align: center; }
.rm-award-name   { font-weight: 600; }
.rm-award-score  { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--c-text); }
/* Roll bonus column on the award panel — same width as the score
   column it replaced. Accent color so the eye reads it as the
   actionable number (officer adds this to the player's /roll 100). */
.rm-award-bonus  {
    font-variant-numeric: tabular-nums; font-weight: 700;
    color: var(--c-accent);
    min-width: 36px; text-align: right;
}
.rm-award-detail { font-size: var(--fs-xs); color: var(--c-text-faint); font-variant-numeric: tabular-nums; }
.rm-award-error  { color: var(--c-bad); padding: var(--sp-3); }

/* ── Character profile page ────────────────────────────────
   #tab-profile is its own routable tab. Body content (roster info,
   loot grouped per raid-night → boss, roll-bonus stats, Classic
   Armory deep-link) renders into #profile-page-body. Boss-loot
   modal is still a modal — just the profile graduated to a page. */
#boss-loot-modal,
#have-list-modal {
    position: fixed; inset: 0;
    background: rgba(0,0,0,0.65);
    z-index: 1000;
    /* Center the box vertically + horizontally. The box itself caps at
       calc(100vh − 48px) and scrolls internally when content overflows
       (rather than letting the whole backdrop scroll, which made the
       modal feel like an awkwardly-tall page section). */
    display: flex; align-items: center; justify-content: center;
    padding: var(--sp-4);
}
.profile-box {
    position: relative;
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: var(--r-lg);
    width: 100%;
    max-width: 880px;
    padding: var(--sp-5) var(--sp-6);
    box-shadow: 0 12px 48px rgba(0,0,0,0.55);
    /* Cap height: 70vh → 80vh per user (a touch taller — was feeling
       too compact for the HAVE-list modal where 11+ raid sections
       want to be visible at once). Anything past that scrolls inside
       the box. */
    max-height: 80vh;
    overflow-y: auto;
}
.profile-close {
    position: absolute;
    top: 8px; right: 12px;
    background: transparent; border: 0;
    color: var(--c-text-faint); font-size: 24px;
    cursor: pointer; line-height: 1;
    padding: 4px 8px; border-radius: var(--r-sm);
}
.profile-close:hover { color: var(--c-text); background: var(--c-surface-2); }
/* Profile-page back-button row — sits just above the content. The
   button itself is a regular .tab-btn so styling stays consistent. */
.profile-page-back {
    margin-bottom: var(--sp-3);
}
.profile-head {
    display: flex; align-items: center; gap: var(--sp-4);
    padding-bottom: var(--sp-4);
    border-bottom: 1px solid var(--c-border);
    margin-bottom: var(--sp-4);
}
.profile-head-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.profile-name { font-size: var(--fs-xl); font-weight: 700; line-height: 1.1; }
.profile-sub  { font-size: var(--fs-sm); color: var(--c-text-2); }
.profile-rank { font-size: var(--fs-xs); color: var(--c-text-faint); }
.profile-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: var(--sp-3);
    margin-bottom: var(--sp-5);
}
.profile-stat {
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    padding: var(--sp-3);
}
/* Profile head — name + rank pill share row 1, class/spec/role pills
   wrap onto row 2. Inline-flex so the rank pill sits flush next to
   the name without a forced newline. */
.profile-name-row {
    display: inline-flex;
    align-items: center;
    gap: 6px;   /* match the Class/Spec pill spacing (.profile-pills) */
    flex-wrap: wrap;
}
/* Small pills under the character name — Class (class-coloured) ·
   Spec (muted) · Role (muted). Replaces the previous "Holy · Priest
   · Healer" plain-text sub line. */
.profile-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 6px;
}
.profile-pill {
    display: inline-block;
    font-size: var(--fs-micro);
    font-weight: 700;
    padding: 2px 8px;
    border-radius: 3px;
    border: 1px solid currentColor;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    line-height: 1.4;
    white-space: nowrap;
}
.profile-pill-muted {
    color: var(--c-text-3);
    background: var(--c-surface-2);
    border-color: var(--c-border);
}
.profile-stat-label { font-size: var(--fs-xs); color: var(--c-text-faint); text-transform: uppercase; letter-spacing: 0.5px; }
.profile-stat-value { font-size: var(--fs-lg); font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 2px; }
.profile-stat-sub   { font-size: var(--fs-xs); color: var(--c-text-3); margin-top: 2px; }
/* Inline link inside a .profile-stat-value (HAVE count opener on
   the profile page). White number with a dotted underline — same
   text-link convention used elsewhere in the app (Achtung mailto,
   NetFire footer). Only the number is clickable; the rest of the
   tile chrome stays static. */
.profile-stat-link {
    color: #fff;
    text-decoration: none;
    border-bottom: 1px dotted #6a7d96;
    cursor: pointer;
    transition: border-color 0.15s;
}
.profile-stat-link:hover {
    border-bottom-color: var(--c-accent);
}
.profile-section { margin-bottom: var(--sp-5); }
.profile-section-title {
    font-size: var(--fs-md); font-weight: 700;
    color: var(--c-text); margin-bottom: var(--sp-3);
    display: flex; align-items: center; gap: var(--sp-2);
}
.profile-section-count { font-weight: 400; font-size: var(--fs-xs); color: var(--c-text-faint); }
.profile-loot-raid { margin-bottom: var(--sp-3); border: 1px solid var(--c-border); border-radius: var(--r-md); overflow: hidden; }
.profile-loot-raid summary {
    cursor: pointer;
    padding: var(--sp-2) var(--sp-3);
    background: var(--c-surface-2);
    display: flex; gap: var(--sp-3); align-items: center;
    font-weight: 600;
    list-style: none;
}
.profile-loot-raid summary::-webkit-details-marker { display: none; }
.profile-loot-raid summary::before { content: '▸'; color: var(--c-text-faint); font-size: var(--fs-xs); margin-right: 4px; }
.profile-loot-raid[open] summary::before { content: '▾'; }
.profile-loot-boss { padding: var(--sp-3); border-top: 1px solid var(--c-border); }
.profile-loot-boss-name { font-size: var(--fs-sm); color: var(--c-text-2); margin-bottom: 4px; font-weight: 600; }
.profile-loot-row {
    display: grid;
    grid-template-columns: 92px 1fr max-content;
    align-items: center; gap: var(--sp-3);
    padding: 3px 0;
    font-size: var(--fs-sm);
}
.profile-loot-date { color: var(--c-text-faint); font-variant-numeric: tabular-nums; }
.profile-loot-bucket { font-size: var(--fs-micro); font-weight: 700; padding: 1px 6px; border-radius: var(--r-sm); }
.profile-empty { color: var(--c-text-faint); font-style: italic; padding: var(--sp-3) 0; }
/* Boss-loot modal raid-night accordion. Each group covers one
   raidConfig (e.g. "Karazhan @ 2026-04-27 [2]") so multiple raid
   nights for the same boss read as separate scannable sections. */
.bl-group {
    margin-top: var(--sp-3);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    background: var(--c-surface);
}
.bl-group:first-of-type { margin-top: var(--sp-2); }
/* Inside .lh-sections (the History page accordion list) the parent
   already drives row-to-row spacing via flex gap. Zero out the
   per-bl-group margin-top so adjacent raid sections sit at uniform
   intervals — the previous `gap + margin-top` combo was producing
   24px between adjacent sections vs 8px above the first one
   (visibly inconsistent). The progression boss-loot modal +
   profile loot section don't use .lh-sections, so they keep the
   default margin-top spacing untouched. */
.lh-sections > .bl-group,
.lh-sections > .bl-group:first-of-type { margin-top: 0; }
.bl-group-summary {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    padding: 8px 12px;
    cursor: pointer;
    list-style: none;
    user-select: none;
}
.bl-group-summary::-webkit-details-marker { display: none; }
.bl-group-caret {
    color: var(--c-text-faint);
    font-size: var(--fs-xs);
    transition: transform 0.12s ease;
    display: inline-block;
}
.bl-group[open] .bl-group-caret { transform: rotate(90deg); }
.bl-group-date {
    color: var(--c-text-faint);
    font-variant-numeric: tabular-nums;
    font-size: var(--fs-sm);
    font-weight: 600;
    min-width: 88px;
}
.bl-group-label {
    color: var(--c-text);
    font-size: var(--fs-sm);
    font-weight: 600;
    flex: 1 1 auto;
    min-width: 0;
}
.bl-group-meta {
    color: var(--c-text-2);
    font-size: var(--fs-xs);
}
.bl-group .import-rows { padding: 4px 12px 12px; margin-top: 0; }
.profile-armory-link {
    display: inline-flex; align-items: center; gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    color: var(--c-text); text-decoration: none;
    font-size: var(--fs-sm);
    white-space: nowrap;
}
.profile-armory-link:hover { background: var(--c-surface); border-color: var(--c-text-faint); color: var(--c-accent); }
/* Variant when the armory link sits in the .profile-head row instead of
   its own section. margin-left:auto pushes it to the right edge so it
   sits flush opposite the character name + meta block. Slightly tighter
   padding so it doesn't overpower the head's vertical proportions. */
.profile-armory-link-head {
    padding: 5px 10px;
    flex-shrink: 0;
}
/* Right-side action stack in the profile head: raider picker above the
   Classic Armory link. margin-left:auto pushes the whole stack to the
   right edge opposite the name/meta block. */
.profile-head-actions {
    margin-left: auto;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: var(--sp-2);
    flex-shrink: 0;
    width: 260px;
    max-width: 260px;
}
.profile-head-actions .profile-armory-link-head { margin-left: 0; justify-content: center; }
.profile-head-actions .profile-raider-picker { max-width: none; }
/* Main/alt indicator pills, inline next to the character name. "Main" is a
   quiet tag; "＋N Alts" / "＋ Add alt" / "Alt of X" are pills — the first two
   are clickable (open the alts modal). */
.profile-name-row .profile-main-tag,
.profile-name-row .profile-alts-pill,
.profile-name-row .profile-altof-pill {
    font-size: var(--fs-xs);
    line-height: 1;
    padding: 3px 8px;
    border-radius: 999px;
    white-space: nowrap;
    align-self: center;
}
.profile-name-row .profile-main-tag {
    /* Muted gray, matching the Class/Spec pills below (.profile-pill-muted);
       same squared shape/size as the rank pill. */
    color: var(--c-text-3);
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    border-radius: 3px;
    font-size: var(--fs-micro);
    padding: 2px 7px;
}
.profile-alts-pill {
    color: var(--c-text-2);
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    cursor: pointer;
    font-family: inherit;
    font-weight: 600;
}
.profile-alts-pill:hover { border-color: var(--c-accent); color: var(--c-accent); }
.profile-name-row .profile-altof-pill {
    /* Muted gray chrome (matches the pills below); the main's name inside keeps its class colour. */
    color: var(--c-text-3);
    background: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: 3px;
    font-size: var(--fs-micro);
    padding: 2px 7px;
    text-transform: uppercase;
    letter-spacing: .06em;
    font-weight: 700;
}
.profile-altof-pill .player-link { font-size: var(--fs-micro); text-transform: none; font-weight: 700; }
.profile-classify-btn {
    background: none; border: none; cursor: pointer; padding: 0 2px;
    color: var(--c-text-faint); font-size: var(--fs-sm); line-height: 1; align-self: center;
}
.profile-classify-btn:hover { color: var(--c-accent); }
/* "Jump to another raider" dropdown — same chrome as the armory link. */
.profile-raider-picker {
    padding: 5px 28px 5px 10px;
    background-color: var(--c-surface-2);
    border: 1px solid var(--c-border);
    border-radius: var(--r-md);
    color: var(--c-text);
    font-size: var(--fs-sm);
    font-family: inherit;
    cursor: pointer;
    max-width: 240px;
    /* Suppress the native chevron (which sits flush to the edge) and use an
       inline SVG caret inset 10px from the right so it isn't jammed against
       the border. Matches the .admin-select / .cfg-box select treatment. */
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%23cfd9e6' d='M0 0l5 6 5-6z'/></svg>");
    background-repeat: no-repeat;
    background-position: right 10px center;
}
.profile-raider-picker:hover { border-color: var(--c-text-faint); }
@media (max-width: 600px) {
    /* On narrow screens, drop the action stack to its own full-width line
       below the name so it doesn't squeeze the meta column. */
    .profile-head { flex-wrap: wrap; }
    .profile-head-actions { margin-left: 0; width: 100%; }
    .profile-raider-picker { max-width: none; }
}
/* Inline clickable character name — used everywhere in place of bare
   text spans. Looks like a plain colored name (callers keep their
   class color via inline style) but gets a subtle underline + cursor
   on hover so the click affordance is discoverable. */
.player-link {
    background: transparent; border: 0; padding: 0; margin: 0;
    color: inherit;
    font-family: inherit; font-size: inherit; font-weight: inherit;
    cursor: pointer;
    text-align: inherit;
    text-decoration: none;
}
.player-link:hover { text-decoration: underline; text-decoration-style: dotted; }

/* —— BiS section injected into Wowhead's tooltip ——
   The injector lives at the bottom of app.js as an IIFE and appends
   .bis-tooltip-block to the inner <td> of any visible .wowhead-tooltip
   on item-link hover. Two sub-sections:
     - "# Best for:"  — every spec that ranks this item in LoonBestInSlot
                        (BIS / Alt / BIS Thrt / BIS Mit / BIS Stam)
     - "Inventory"    — the currently-viewed player (loot-tab editing
                        target, view-as impersonation, or signed-in
                        char) if they HAVE the item.
   We INHERIT typography from Wowhead's tooltip (font-family, font-size,
   line-height, regular text color) so the section reads as a natural
   continuation of the tooltip body. Only layout (flex, padding, gap)
   and semantic colors (header yellow / tag accents / class colors)
   are forced. Bold is reserved for section headers ("# Best for:" /
   "Inventory") to match the typographic hierarchy of Wowhead's own
   stat / equip lines (regular weight). */
/* Tall tooltips with a long "# Best for:" list (e.g. Skull of Gul'dan
   ranks BIS for ~10 caster specs) used to extend off-screen when the
   user hovered an item near the bottom of the viewport. Cap the
   tooltip's height at the viewport (minus a small margin) and let
   the body scroll internally. The post-inject JS hook also shifts
   the tooltip's `top` upward when it would overflow below the
   viewport, so the cap is a fallback for the rare case where even
   shifting all the way up isn't enough. */
.wowhead-tooltip {
    max-height: calc(100vh - 24px) !important;
    overflow-y: auto !important;
}
.wowhead-tooltip .bis-tooltip-block {
    margin-top: 8px !important;
    padding-top: 4px !important;
    padding-bottom: 2px !important;
    text-align: left !important;
    display: block !important;
    /* font-family / font-size / color / line-height all inherit from
       the tooltip's <td>. */
}
.wowhead-tooltip .bis-tooltip-block .bis-tt-head {
    color: #E5CB7F !important;
    /* No margin-bottom — matches the Inventory header below so both
       sections have identical header → first-row spacing. Earlier 4px
       gap on # Best for: vs 0 on Inventory read as inconsistent. */
}
.wowhead-tooltip .bis-tooltip-block .bis-tt-row {
    display: flex !important;
    justify-content: space-between !important;
    align-items: center !important;
    padding: 1px 0 !important;
    gap: 12px !important;
}
.wowhead-tooltip .bis-tooltip-block .bis-tt-spec-cell {
    display: inline-flex !important;
    align-items: center !important;
    gap: 6px !important;
    min-width: 0 !important;
}
.wowhead-tooltip .bis-tooltip-block .bis-tt-class-icon {
    width: 14px !important;
    height: 14px !important;
    border-radius: 2px !important;
    flex-shrink: 0 !important;
    display: inline-block !important;
    vertical-align: middle !important;
}
/* Tag color comes from inline style (the spec's class color) — see
   buildBisRows() in app.js. Reading the row left-to-right then keeps
   one color throughout: icon, spec name, and tag all match. */
.wowhead-tooltip .bis-tooltip-block .bis-tt-tag { white-space: nowrap !important; }
/* Phase suffix (P1 / P4 / etc.) sits next to the tag — inherits the
   class color from its parent .bis-tt-tag (which carries an inline
   color: <classCol> style), so the whole "BIS · P1" reads in one
   color. Just a small left margin to separate it from the tag word. */
.wowhead-tooltip .bis-tooltip-block .bis-tt-phase { margin-left: 4px !important; }
.wowhead-tooltip .bis-tooltip-block .bis-tt-inv-head {
    color: #E5CB7F !important;
    margin-top: 6px !important;
}
/* Inventory row aligns flush-left with the BiS rows above it (icon
   sits at the same column). Earlier 12px padding-left was a leftover
   from the Total/Equipped two-column layout — now that we render just
   the player name, the indent broke the visual alignment. */
.wowhead-tooltip .bis-tooltip-block .bis-tt-inv-row {
    display: flex !important;
    align-items: center !important;
    padding: 1px 0 !important;
    gap: 6px !important;
}
/* (Inv name / stat / spec-cell name typography inherits from tooltip td.) */

/* ── Extra small: ≤380px ─────────────────────────────────────── */
@media (max-width: 380px) {
    .logo-img { height: 44px; }
    .prog-boss-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .prog-phase-grid { grid-template-columns: 1fr; }
}

/* Addon-pool live updates — a newly discovered census candidate flashes in
   (teal wash fading out) so you can watch finds arrive in real time. */
@keyframes scout-pool-new {
    from { background: rgba(78, 205, 196, 0.22); }
    to   { background: transparent; }
}
tr.scout-pool-new > td { animation: scout-pool-new 3.5s ease-out; }

/* Roster edit mode: hide read-only data columns (Parse / GS) — nothing to
   edit there, and the width is better spent on the input cells. */
#roster-table.roster-editing .rcol-readonly { display: none; }

/* Dual-thumb range slider (Scout pool parse filter). Two overlaid native
   range inputs; the track/fill are separate spans so the selected span
   reads in accent teal. Thumbs stay clickable via pointer-events. */
.vm-dualrange { position: relative; width: 130px; height: 18px; display: inline-block; }
.vm-dualrange-track { position: absolute; left: 0; right: 0; top: 8px; height: 3px; background: var(--c-border); border-radius: 2px; }
.vm-dualrange-fill { position: absolute; top: 8px; height: 3px; background: var(--c-accent); border-radius: 2px; }
.vm-dualrange input[type=range] { position: absolute; inset: 0; width: 100%; height: 18px; margin: 0; -webkit-appearance: none; appearance: none; background: none; pointer-events: none; }
.vm-dualrange input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; pointer-events: auto; width: 12px; height: 12px; border-radius: 50%; background: var(--c-accent); border: 2px solid var(--c-bg); box-shadow: 0 0 0 1px var(--c-accent); cursor: grab; }
.vm-dualrange input[type=range]::-moz-range-thumb { pointer-events: auto; width: 10px; height: 10px; border-radius: 50%; background: var(--c-accent); border: 2px solid var(--c-bg); cursor: grab; }
