/* admin.css — Admin panel styles */

/* Match Results sub-tabs now use the shared accordion sub-tabs
   (.subtab--accordion / .subtab-panel in css/subtabs.css, via mountAccordionTabs)
   — no admin-local duplication of the dashboard's tab chrome. */


/* Skip link (a11y) — duplicated from navigation.css since admin.html
   does not load navigation.css. Keep in sync if the base rules change. */
.skip-link {
    position: absolute;
    top: -100px;
    left: 0;
    background: var(--color-accent, #2563eb);
    color: var(--color-on-accent);
    padding: 8px 16px;
    z-index: 9999;
    text-decoration: none;
    font-weight: 600;
    border-radius: 0 0 4px 0;
    transition: top 150ms ease-out;
}
.skip-link:focus,
.skip-link:focus-visible {
    top: 0;
    outline: 2px solid #fff;
    outline-offset: -4px;
}

/* ---- Layout ---- */
.admin-layout {
    display: flex;
    min-height: 100vh;
}

/* .admin-sidebar's layout, flyout system, and mobile rail-mode are now
   entirely canonical — the aside carries BOTH `admin-sidebar` (id hook,
   admin-only content below) and `site-sidebar` (css/site-sidebar.css: width,
   flex layout, sticky/z-index/overflow, hover+pin flyouts, mobile off-canvas
   drawer, rail-mode collapse). Nothing admin-specific needs redeclaring here
   — see js/admin/render/adminSidebarNav.js for the shared markup. */

body.site-sidebar-closed .admin-main {
    margin-left: calc(var(--sidebar-width) * -1);
    transition: margin-left 240ms cubic-bezier(0.32, 0.72, 0, 1);
    /* Clear the fixed top bar (shown only in this closed state — same bar as
       the public site, mounted via mountTopbar in admin). The extra
       --space-lg matches the breathing room .admin-main's own top padding
       gives the title in the open/desktop state — without it the title sits
       flush against the bar with no gap. */
    padding-top: calc(var(--topbar-height) + env(safe-area-inset-top, 0px) + var(--space-lg));
}
.admin-main {
    transition: margin-left 240ms cubic-bezier(0.32, 0.72, 0, 1);
}

/* Brand row now reuses .site-sidebar-brand* (css/site-sidebar.css) directly
   — see js/admin/render/adminSidebarNav.js. No admin-specific override needed. */

/* ---- Welcome Card ---- */
.admin-welcome {
    margin-bottom: var(--space-lg);
    padding: 12px 10px 10px 12px;
    background: linear-gradient(135deg, rgba(37,99,235,0.22) 0%, rgba(255,255,255,0.04) 100%);
    border-radius: var(--radius-md);
    border: 1px solid rgba(255,255,255,0.08);
    border-top-color: rgba(255,255,255,0.13);
    position: relative;
    overflow: hidden;
    display: flex;
    align-items: center;
    gap: 10px;
    animation: welcome-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes welcome-in {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: translateY(0); }
}

.admin-welcome::before {
    content: '';
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 3px;
    background: linear-gradient(to bottom, #60a5fa 0%, #2563eb 100%);
    border-radius: 0 2px 2px 0;
}

.admin-welcome-avatar {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-105);
    font-weight: 800;
    color: #fff;
    flex-shrink: 0;
    text-transform: uppercase;
    box-shadow: 0 0 0 2px rgba(96,165,250,0.25), 0 2px 10px rgba(37,99,235,0.45);
}

.admin-welcome-body {
    min-width: 0;
    flex: 1;
}

.admin-welcome-label {
    font-size: var(--fs-062);
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgba(255,255,255,0.38);
    margin-bottom: 1px;
}

.admin-welcome-name {
    font-size: var(--fs-092);
    font-weight: 700;
    color: rgba(255,255,255,0.95);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    letter-spacing: 0.01em;
}

.admin-welcome-status {
    display: flex;
    align-items: center;
    gap: 5px;
    margin-top: 3px;
    font-size: var(--fs-065);
    color: rgba(52,211,153,0.85);
    letter-spacing: 0.04em;
    font-weight: 600;
    text-transform: uppercase;
}

.admin-welcome-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #34d399;
    box-shadow: 0 0 0 2px rgba(52,211,153,0.2);
    animation: pulse-dot 2.4s ease-in-out infinite;
    flex-shrink: 0;
}

@keyframes pulse-dot {
    0%, 100% { box-shadow: 0 0 0 2px rgba(52,211,153,0.2); }
    50%       { box-shadow: 0 0 0 4px rgba(52,211,153,0.12), 0 0 8px rgba(52,211,153,0.3); }
}

/* Nav items, icons, and the footer logout button now reuse .site-nav-item /
   .site-nav-icon / .site-sidebar-logout directly (css/site-sidebar.css) —
   see js/admin/render/adminSidebarNav.js. No admin-specific override needed. */

.admin-main {
    flex: 1;
    padding: var(--space-lg) var(--space-xl);
    background: var(--color-bg);
    overflow-y: auto;
}

.admin-main h1 {
    font-size: var(--fs-160);
    margin-bottom: var(--space-lg);
    color: var(--color-text);
}

/* ---- Login ---- */
.admin-login-wrapper {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    background: var(--color-bg);
}

.admin-login-box {
    background: var(--color-surface);
    padding: var(--space-xl);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    width: 100%;
    max-width: 400px;
}

.admin-login-box h1 {
    text-align: center;
    margin-bottom: var(--space-lg);
    font-size: var(--fs-150);
}

.admin-login-box .form-group {
    margin-bottom: var(--space-md);
}

/* ---- Forms ---- */
.form-group {
    margin-bottom: var(--space-md);
}

.form-group label {
    display: block;
    font-weight: 600;
    margin-bottom: var(--space-xs);
    font-size: var(--fs-090);
    color: var(--color-text-secondary);
}

/* A search field inside a form-group opts OUT of the admin's form chrome: its
   box is canonical in .app-search-input (layout.css), so the admin's own
   padding (16px, vs the canonical 12px) does not silently make the same control
   a different size here than on the public pages. Without the :not(), this rule
   simply wins on specificity — which is how the identity flag ended up sitting
   at a different inset in admin than everywhere else. Width still comes from
   here, since that IS this form's layout. */
.form-group input.app-search-input {
    width: 100%;
}

.form-group input:not(.app-search-input),
.form-group select,
.form-group textarea {
    width: 100%;
    box-sizing: border-box;
    min-width: 0;
    padding: var(--space-sm) var(--space-md);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-family: inherit;
    font-size: var(--fs-095);
    color: var(--color-text);
    background: var(--color-surface);
    transition: border-color 0.15s;
}

.form-group input:not(.app-search-input):focus,
.form-group select:focus,
.form-group textarea:focus {
    outline: none;
    border-color: var(--color-accent);
    box-shadow: 0 0 0 3px var(--color-accent-light);
}

/* ---- Buttons ---- */
/* ---- Buttons — single source of truth ---------------------------------
   ONE base `.btn`. Colour is the only thing the variant classes change;
   size is a linear ramp where the font comes from the type-scale tokens
   and the padding is in `em`, so the button scales proportionally with
   its font at every viewport (no hand-tuned px per size).

   Mirrors v2 `primitives/Button` (variants primary/secondary/success/
   danger + sizes sm/md/lg). v1→v2 class mapping is in
   v2/docs/MIGRATION-FROM-V1.md. Anything that used the old `.file-btn`
   now reuses `.btn .btn-secondary .btn-sm` (see filePickerHTML). */

/* The [hidden] attribute must win over the .btn display rule below —
   otherwise a button toggled via el.hidden = true (e.g. the bulk-action
   bar in Historical Changes) stays visible, since author CSS display
   beats the UA [hidden]{display:none} default. */
.btn[hidden] {
    display: none;
}

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4em;
    padding: 0.5em 1em;                 /* md (default) — em-based ramp */
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-family: inherit;
    font-size: var(--fs-090);
    font-weight: 600;
    line-height: 1.2;
    letter-spacing: 0.01em;
    cursor: pointer;
    transition: background 0.15s, opacity 0.15s, transform 0.12s, box-shadow 0.15s;
    position: relative;
    /* neutral default surface so variant-less buttons (e.g. the .btn-tech
       TA/TB/TD/NP toggles) still read as buttons */
    background: var(--color-surface);
    color: var(--color-text);
}

.btn:not(:disabled):hover {
    transform: translateY(-1px);
}

.btn:not(:disabled):active {
    transform: translateY(0);
}

.btn:disabled {
    opacity: 0.45;
    cursor: not-allowed;
}

/* Colour variants — the ONLY per-button difference. */
.btn-primary {
    background: var(--color-accent);
    color: #fff;
    border-color: transparent;
    box-shadow: 0 1px 3px color-mix(in srgb, var(--color-accent) 30%, transparent);
}

.btn-primary:hover:not(:disabled) {
    /* theme-aware darken (was a hardcoded blue that flashed on non-blue accents) */
    background: color-mix(in srgb, var(--color-accent) 85%, #000);
    box-shadow: 0 4px 12px color-mix(in srgb, var(--color-accent) 38%, transparent);
}

.btn-success {
    background: var(--color-running);
    color: #fff;
    border-color: transparent;
    box-shadow: 0 1px 3px rgba(4,120,87,0.3);
}

.btn-success:hover:not(:disabled) {
    background: #047857;
    box-shadow: 0 4px 12px rgba(4,120,87,0.38);
}

.btn-danger {
    background: var(--color-loss);
    color: #fff;
    border-color: transparent;
    box-shadow: 0 1px 3px rgba(185,28,28,0.25);
}

.btn-danger:hover:not(:disabled) {
    background: #b91c1c;
    box-shadow: 0 4px 12px rgba(185,28,28,0.35);
}

.btn-secondary {
    background: var(--color-surface);
    color: var(--color-text);
    border-color: var(--color-border);
    box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}

.btn-secondary:hover:not(:disabled) {
    background: var(--color-hover);
    border-color: #c0c4cd;
    box-shadow: 0 3px 8px rgba(0,0,0,0.09);
}

.btn-tech { font-weight: 700; }

/* Size ramp — linear: font token + em padding (scales together). */
.btn-lg { font-size: var(--fs-110); padding: 0.6em 1.25em; }
.btn-sm { font-size: var(--fs-082); padding: 0.4em 0.8em; }
.btn-xs { font-size: var(--fs-075); padding: 0.3em 0.65em; }

.btn-block {
    display: flex;
    width: 100%;
}

/* "Back to …" link button — pins font/size/padding to the public
   .open-full-btn (dashboard.css) so the admin back link reads identically
   to "‹ Back to full table". Pair with .btn-primary for the accent fill.
   Keep in sync with .open-full-btn. */
.btn-back {
    font-size: var(--fs-085);
    font-weight: 500;
    padding: 6px 14px;
}

/* ---- Staging Badge ---- */
.staging-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--color-loss);
    color: #fff;
    font-size: var(--fs-075);
    font-weight: 700;
    min-width: 20px;
    height: 20px;
    border-radius: var(--radius-full);
    padding: 0 6px;
    margin-left: var(--space-xs);
}

.staging-badge.empty {
    display: none;
}

/* ---- Pending Changes Panel ---- */
.pending-panel {
    background: var(--color-surface);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: var(--space-md);
    margin-bottom: var(--space-lg);
    border: 1px solid var(--color-border);
}

.pending-panel h3 {
    margin-bottom: var(--space-md);
    font-size: var(--fs-100);
}

.pending-list {
    list-style: none;
    margin-bottom: var(--space-md);
}

.pending-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-sm);
    border-bottom: 1px solid var(--color-border);
    font-size: var(--fs-090);
}

.pending-item:last-child {
    border-bottom: none;
}

.pending-item-desc {
    flex: 1;
}

/* Primary topic icon in a change label (changeVocabulary.js renderChangeLabel).
   League's icon is the site's inline league glyph (SVG) — size it to the text
   line so it sits inline like the emoji topic icons for Player/Settings. */
.change-topic-icon {
    display: inline-flex;
    align-items: center;
}
.change-topic-icon svg {
    width: 1em;
    height: 1em;
    vertical-align: -0.12em;
}

.pending-item-time {
    color: var(--color-text-muted);
    font-size: var(--fs-080);
    margin: 0 var(--space-md);
}

/* ---- Historical Changes: row select + bulk undo (scoped, .pending-item/
   .pending-list stay shared with the plain Pending Changes list above) ---- */
.history-item {
    flex-wrap: wrap;
    gap: var(--space-sm);
}

.history-row-select {
    flex: none;
    width: 1.3em;
    height: 1.3em;
    margin: 0 var(--space-sm) 0 0;
    cursor: pointer;
}

.history-diff {
    flex-basis: 100%;
}

/* Small pills on a batch row: how many rows it touched / how many were ghosts. */
.history-count-pill {
    display: inline-block;
    font-size: var(--fs-080);
    color: var(--color-text-secondary);
    background: var(--color-bg-muted);
    border-radius: var(--radius-sm);
    padding: 0.05em 0.5em;
    margin-left: var(--space-xs);
}
.history-ghost-note {
    font-size: var(--fs-080);
    color: var(--color-text-muted);
    margin-left: var(--space-xs);
}

/* Attachment sub-rows inside an expanded batch's Details. */
.history-attach {
    padding: var(--space-sm) 0;
    border-top: 1px solid var(--color-border);
}
.history-attach:first-child { border-top: none; }
.history-attach-head { font-size: var(--fs-090); margin-bottom: var(--space-xs); }
/* One plain-English bullet line per changed field (see changeDetails.js). */
.history-field-line {
    font-size: var(--fs-085);
    color: var(--color-text-secondary);
    padding-left: var(--space-md);
    line-height: 1.5;
    word-break: break-word;
}

.history-bulk-bar {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    margin-bottom: var(--space-sm);
}

/* Themed "Show last N changes" control (mirrors .themed-date's theming
   approach, but self-contained since it isn't inside a .form-group). */
.history-limit-bar {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    font-size: var(--fs-090);
    color: var(--color-text-secondary);
    margin-bottom: var(--space-sm);
}

/* Historical Changes ▸ exclude toggle. Reuses the pill primitive
   (.subtabs--pill / .subtab--pill, css/subtabs.css) rather than inventing a
   second filter chrome — the same visual vocabulary the league-type filters
   already use. It is a TOGGLE, not a tab, so it carries aria-pressed and the
   bar carries no role="tablist": borrowing a look must not mean borrowing the
   wrong semantics. No new colour or size values — everything below is layout. */
/* The bar owns its own layout. It used to borrow `display:flex` + gap +
   margin from `.subtabs subtabs--pill`, which it carried purely to satisfy the
   pill's descendant-scoped state selectors. Once `.pill-neutral` gained
   standalone rules those classes came off — and took the layout with them,
   leaving the toggle sitting flush against the history panel (measured: 0px).
   Borrowed layout is layout you lose without noticing. */
.history-filter-bar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-sm);
    margin-bottom: var(--space-md);
}
/* `.pending-item` is `display: flex`, an author rule — which out-specifies the
   UA stylesheet's `[hidden] { display: none }`. Setting `hidden` on a history
   row therefore did nothing at all until this rule existed: the toggle reported
   "7 hidden" while all 7 stayed on screen. Any list whose items carry a display
   value needs its own [hidden] rule; the attribute is not self-enforcing. */
.history-item[hidden] {
    display: none;
}
.history-filter-note {
    font-size: var(--fs-085);
    color: var(--color-text-muted);
}

.history-limit-input {
    width: 5em;
    padding: 0.3em 0.5em;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
    color: var(--color-text);
    font-family: inherit;
    font-size: inherit;
    accent-color: var(--color-accent);
}

.history-limit-input:focus {
    border-color: var(--color-accent);
    outline: none;
    box-shadow: 0 0 0 2px var(--color-accent-light);
}

.history-select-all {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    font-size: var(--fs-100);
    cursor: pointer;
}

.history-select-all input[type="checkbox"] {
    width: 1.3em;
    height: 1.3em;
    margin: 0;
    cursor: pointer;
}

.history-selected-count {
    color: var(--color-text-muted);
    font-size: var(--fs-085);
}

/* ---- Cards ---- */
.admin-card {
    background: var(--color-surface);
    border-radius: var(--radius-md);
    box-shadow: 0 1px 3px rgba(0,0,0,0.06), 0 4px 16px rgba(0,0,0,0.04);
    /* Fluid card padding: was a fixed 24px (var(--space-lg)). Now ≈0.8em of the
       font-large fluid token (--fs-093) — same clamp slope as the --fs family,
       so it scales linearly with the viewport like the cell padding does. Max
       caps at ~11px (< half the old 24px). Expressed as clamp() rather than a
       literal em because plain .admin-card font-size is a static 15px, so an em
       would not be fluid; this keeps the slope without touching any font-size. */
    padding: clamp(0.57rem, 0.45rem + 0.62vw, 0.74rem);
    margin-bottom: var(--space-lg);
    border: 1px solid var(--color-border);
    transition: box-shadow 0.2s;
}

.admin-card:hover {
    box-shadow: 0 2px 6px rgba(0,0,0,0.08), 0 8px 24px rgba(0,0,0,0.06);
}

.admin-card h2 {
    font-size: var(--fs-080);
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    margin-bottom: var(--space-md);
    padding-bottom: var(--space-sm);
    border-bottom: 1px solid var(--color-border);
}

/* Section frame, heading and collapse for the admin New/Edit League views now
   come from the shared css/sections.css (.app-section / .app-section--card /
   .app-section-h2) + js/render/sectionCollapse.js — no admin-local duplication. */

/* font-small content for League Settings & Automatic Sync cards. */
.edit-card-sm,
.edit-card-sm small,
.edit-card-sm .form-group label,
.edit-card-sm .form-group input,
.edit-card-sm .form-group select,
.edit-card-sm .form-group textarea {
    font-size: var(--fs-085);
}

/* Inputs in these cards scale linearly with the viewport: --fs-085 is a
   clamp()/vw token, so em-based padding + the inherited unitless line-height
   (1.6) make the whole control grow and shrink with the font — and therefore
   with the viewport width. Covers Issue Date and every Automatic-Sync field. */
.edit-card-sm {
    --input-pad-y: 0.6em;
    --input-pad-x: 1.1em;
}
.edit-card-sm .form-group input,
.edit-card-sm .form-group select {
    padding: var(--input-pad-y) var(--input-pad-x);
}
/* date/time controls carry ~2px of constant WebKit widget chrome — trim 1px
   off each vertical padding so their box height matches the text/number inputs
   at every breakpoint. Higher specificity (attribute selector) than the rule
   above, so it wins. max-width/min-width keep the control inside its column. */
.edit-card-sm .form-group input[type="date"],
.edit-card-sm .form-group input[type="time"] {
    padding-top: calc(var(--input-pad-y) - 1px);
    padding-bottom: calc(var(--input-pad-y) - 1px);
    min-width: 0;
    max-width: 100%;
}
/* iOS Safari sizes native date/time controls to their intrinsic content width
   and ignores width:100% / min-width:0, so they overflow their column to the
   right (the reported Issue Date + External Source Start/End Date bug on iPhone).
   Resetting the native appearance — scoped to iOS only via the
   -webkit-touch-callout feature query — makes them honour the author box model
   and fill exactly 100% of the column. Desktop is untouched, so the themed
   ::-webkit-calendar-picker-indicator icon is preserved there. */
@supports (-webkit-touch-callout: none) {
    .edit-card-sm .form-group input[type="date"],
    .edit-card-sm .form-group input[type="time"] {
        -webkit-appearance: none;
        appearance: none;
        width: 100%;
    }
}

/* Checkboxes / radios must not inherit the full-width + padded text-input
   sizing from `.form-group input` (base) or `.edit-card-sm .form-group input`
   (responsive padding) — otherwise they stretch and shove their label aside.
   Covers the "Sync together with" league pickers in the Automatic Sync card. */
.form-group input[type="checkbox"],
.form-group input[type="radio"],
.edit-card-sm .form-group input[type="checkbox"],
.edit-card-sm .form-group input[type="radio"] {
    width: auto;
    min-width: 0;
    padding: 0;
    margin: 0;
    flex: none;
}

/* Themed checkboxes/radios — every admin checkbox (hidden toggles, doubles
   picker, history select-all, sync league pickers) tints to the active
   theme's accent instead of the browser's default blue. */
input[type="checkbox"],
input[type="radio"] {
    accent-color: var(--color-accent);
}

/* generic font-small table utility (mirrors components.css for admin.html) */
table.font-small { font-size: var(--fs-085); }

/* ── F6 — Medals & Prizes ─────────────────────────────────────────────────
   Rendered on the unified FF chrome (.ff-wrap + .admin-table.font-large,
   data-mf-table-id="F6") — see ffMedalsTableHTML in js/admin/leagueManager.js.
   The shared FF rules above own the wrapper, header, hairlines, sticky col, and
   the medal column (sticky col 1). Only the two bits below are F6-specific:
   the Count/Prize columns are held equal, and the medal-name cell content gets
   its inline-flex layout + per-medal colour. The Count/Prize <input>s inherit
   the responsive font-small sizing from .edit-card-sm .form-group input.
   (Hand-built FF chrome like F1–F4; the mountFFTable rewire is Phase 8 of
   docs/plans/table-lab-unification.md.) */
[data-mf-table-id="F6"] th:nth-child(2),
[data-mf-table-id="F6"] td:nth-child(2),
[data-mf-table-id="F6"] th:nth-child(3),
[data-mf-table-id="F6"] td:nth-child(3) {
    width: 28%;
}
/* Column 4 — the add/remove buttons for a medal's EXTRA prize rows. Sized to
   the button, so widening the table for it doesn't come out of Count/Prize. */
[data-mf-table-id="F6"] .prize-row-actions {
    width: 1%;
    white-space: nowrap;
    text-align: center;
}
/* The extra rows belong to the medal above them: same colour, quieter weight,
   indented, so the medal's OWN row still reads as the tier's headline. */
[data-mf-table-id="F6"] .medal-cell.is-extra {
    font-weight: 400;
    opacity: 0.75;
    padding-inline-start: var(--space-md);
}
.medal-cell {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    font-weight: 700;
    white-space: nowrap;
}
.medal-cell .medal-icon {
    font-size: 1.15em;
    line-height: 1;
}
.medal-cell.medal-gold   { color: var(--color-gold); }
.medal-cell.medal-silver { color: var(--color-silver); }
.medal-cell.medal-bronze { color: var(--color-bronze); }

/* ---- Admin Table ---- */
/* font-large mirrors base.css — admin.html doesn't load components.css */
table.font-large { font-size: var(--fs-093); }

.admin-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--fs-090);
    color: var(--color-text);
}

.admin-table th {
    text-align: left;
    padding: var(--space-sm) var(--space-md);
    background: var(--color-bg);
    font-weight: 700;
    color: var(--color-text-secondary);
    font-size: var(--fs-078);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    border-bottom: 2px solid var(--color-border);
    white-space: nowrap;
}

.admin-table td {
    padding: var(--space-sm) var(--space-md);
    border-bottom: 1px solid var(--color-border);
    vertical-align: middle;
    color: var(--color-text);
}

.admin-table td[data-label="Actions"] {
    white-space: nowrap;
}

/* ── FF format — canonical chrome for F1/F2/F3/F4 (admin tables). ──────
   Mirrors table-lab/formats/ff/ff.css. When Phase 8 of the unification
   plan rewires admin tables to mountFFTable, the lab file becomes the
   sole source and this block is removed.

   Three cell modes (per ColDef in mountFFTable):
   • Display — read-only HTML
   • Action  — button(s) with data-* attrs, caller wires event delegation
   • Edit    — input/select/toggle + getValue (participates in getDiff)

   Caller owns: heading, "+ Add" button, Save buttons, validation msg slot,
   sub-forms (e.g. Upload Custom Flag), and event-delegation handlers.
   FF owns only the scroll wrapper + table + sticky behavior. */

.ff-wrap {
    overflow-x: auto;
    overflow-y: clip;
    border-radius: var(--radius-sm);
}

.admin-table.font-large {
    border-collapse: separate;
    border-spacing: 0;
    width: 100%;
}

.admin-table.font-large th,
.admin-table.font-large td {
    padding: 0.45em 0.5em;
    text-align: left;
    white-space: nowrap;
}

/* Sticky thead — header background --color-bg (subtle tint vs body
   --color-surface). Admin-style header chrome: uppercase + letter-spacing
   + heavier weight. */
.admin-table.font-large thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    background: var(--color-bg);
    color: var(--color-text-secondary);
    font-weight: 700;
    font-size: var(--fs-078);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    border-bottom: 0;
    box-shadow: inset 0 -1px 0 var(--color-border);
}

/* Row hairlines via box-shadow inset (matches SF). */
.admin-table.font-large tbody td {
    border-bottom: 0;
    box-shadow: inset 0 -1px 0 var(--color-border);
    vertical-align: middle;
    /* inset well so the FF table separates from its --color-surface .admin-card
       (matches MF/SF/exp). Edit inputs / hover / validation / F3 match-block
       state colours override per-cell on top. */
    background: var(--color-inset);
}

.admin-table.font-large tbody tr:last-child td {
    box-shadow: none;
}

.admin-table.font-large tbody tr:hover td {
    background: var(--color-hover);
}

/* Sticky left column (auto-applied to first col). Top-left corner z=4
   (above sticky thead z=3 and sticky body z=2), matching MF/SF/exp. */
.admin-table.font-large th:first-child {
    position: sticky;
    left: 0;
    z-index: 4;
    background: var(--color-bg);
}
.admin-table.font-large tbody td:first-child {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--color-inset);
}
.admin-table.font-large tbody tr:hover td:first-child {
    background: var(--color-hover);
}

/* Sticky-boundary drop-shadow on horizontal scroll — mirrors MF/SF.
   `.is-scrolled-x` is set by attachStickyShadow when scrollLeft > 0. */
.ff-wrap.is-scrolled-x .admin-table.font-large thead th:first-child {
    box-shadow:
        inset 0 -1px 0 var(--color-border),
        6px 0 8px -4px rgba(0, 0, 0, 0.18);
}
.ff-wrap.is-scrolled-x .admin-table.font-large tbody td:first-child {
    box-shadow:
        inset 0 -1px 0 var(--color-border),
        6px 0 8px -4px rgba(0, 0, 0, 0.18);
}
.ff-wrap.is-scrolled-x .admin-table.font-large tbody tr:last-child td:first-child {
    box-shadow: 6px 0 8px -4px rgba(0, 0, 0, 0.18);
}

/* F1 (Leagues list) — the ONE FF table that sits directly on the page canvas
   (--color-bg) with NO framing --color-surface .admin-card: the card wrapper
   was removed (the table was its only child, so the card was redundant double
   chrome). Per the Surface-elevation canon, a table that sits on the page bg
   and is NOT nested in a section shifts UP one tier — exactly like the
   dashboard's .dash-card. So body + sticky col paint --color-surface (not the
   nested-well --color-inset).
   HEADER — uses the per-theme table-header tokens --header-bg / --header-text
   (the same deep, themed header tone the MF tables use), NOT --color-inset.
   --color-inset is only 28% off --color-bg, so on the bare page canvas the
   header read as nearly the page bg (too low contrast). --header-bg is a
   distinct, theme-tailored tone (defined per theme in css/themes.css —
   dark #14141e, vegas deep-red, casino #8b0000, x22 green, rainbow purple…),
   so the header separates clearly from the canvas in EVERY theme automatically,
   and --header-text guarantees readable text on it. Hover (--color-hover) and
   the generic scroll-shadow rules above are unaffected — their selectors carry
   higher specificity than these static background overrides. */
.admin-table.font-large[data-mf-table-id="F1"] tbody td,
.admin-table.font-large[data-mf-table-id="F1"] tbody td:first-child {
    background: var(--color-surface);
}
.admin-table.font-large[data-mf-table-id="F1"] thead th,
.admin-table.font-large[data-mf-table-id="F1"] thead th:first-child {
    background: var(--header-bg);
    color: var(--header-text);
}

/* Validation marker for Edit-mode cells (mountFFTable's validate() applies
   this class to invalid <td>s; caller can override via the cellInvalidClass
   arg). */
.cell-invalid {
    outline: 2px solid var(--color-loss);
    outline-offset: -2px;
}

/* ── FF button sizing — unified, em-based, viewport-fluid ──────────
   All buttons inside an FF table share the table's fluid font scaling.
   font-size is set in em — it inherits the cell's clamp-fluid font
   (var(--fs-093) for font-large). Padding, border-radius, and gap are
   all em-based so they scale proportionally with the font. Ratios
   between buttons and cells are preserved at every viewport width.
   Variant classes (.btn-primary / .btn-danger / .btn-secondary /
   .btn-tech) keep their color / weight treatment unchanged — only the
   sizing dimension is unified across the FF family.

   Density tiers (inside FF):
   • default (.btn)      = 0.88em — comfortable, used for primary actions
   • .btn-sm             = 0.85em — slightly more compact (legacy alias)
   • .btn-xs             = 0.75em — tight; used by F3 row-action buttons */
.ff-wrap .btn,
.ff-wrap .btn-sm,
.ff-wrap .btn-xs {
    font-size: 0.85em;
    padding: 0.3em 0.7em;
    border: 1px solid var(--color-border);
    border-radius: 0.25em;
    line-height: 1.2;
    gap: 0.3em;
}
.ff-wrap .btn {
    font-size: 0.88em;
    padding: 0.35em 0.8em;
}
.ff-wrap .btn-xs {
    font-size: 0.75em;
    padding: 0.25em 0.6em;
    border-radius: 0.2em;
}
.ff-wrap .btn-tech {
    font-weight: 700;
}
/* Disabled state — opacity only; size unchanged. */
.ff-wrap .btn:disabled,
.ff-wrap .btn-sm:disabled,
.ff-wrap .btn-xs:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}
/* Inter-button spacing inside F3's Actions column — also em-based. */
.ff-wrap .match-actions .btn {
    margin: 0.1em 0.15em;
}

/* Legacy hover/border rules for non-font-large admin tables (keep). */
.admin-table tbody tr:hover {
    background: var(--color-hover);
}
.admin-table tbody tr:last-child td {
    border-bottom: none;
}

/* ---- Status Toggle ---- */
.toggle-switch {
    position: relative;
    display: inline-block;
    width: 44px;
    height: 24px;
}

.toggle-switch input {
    opacity: 0;
    width: 0;
    height: 0;
}

.toggle-slider {
    position: absolute;
    inset: 0;
    background: var(--color-border);
    border-radius: var(--radius-full);
    cursor: pointer;
    transition: background 0.2s;
}

.toggle-slider::before {
    content: '';
    position: absolute;
    height: 18px;
    width: 18px;
    left: 3px;
    bottom: 3px;
    background: #fff;
    border-radius: 50%;
    transition: transform 0.2s;
}

.toggle-switch input:checked + .toggle-slider {
    background: var(--color-running);
}

.toggle-switch input:checked + .toggle-slider::before {
    transform: translateX(20px);
}

/* ---- Messages ---- */
.admin-msg {
    padding: var(--space-sm) var(--space-md);
    border-radius: var(--radius-sm);
    margin-bottom: var(--space-md);
    font-size: var(--fs-090);
}

.admin-msg-success {
    background: var(--color-running-bg);
    color: var(--color-running);
    border: 1px solid var(--color-running);
}

.admin-msg-error {
    background: var(--color-loss-bg);
    color: var(--color-loss);
    border: 1px solid var(--color-loss);
}

.admin-msg-info {
    background: var(--color-accent-light);
    color: var(--color-accent);
    border: 1px solid var(--color-accent);
}

/* "It worked, but you need to know something" — e.g. the sync completed but the
   source had dropped already-played results, which were restored. Distinct from
   -error, which means nothing was updated. */
.admin-msg-warning {
    background: var(--color-warning-bg);
    color: var(--color-warning);
    border: 1px solid var(--color-warning);
}

/* Sync activity log — running list of the last few status lines (scrolls). */
.bgsync-log {
    max-height: 240px;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-xs);
    margin-bottom: var(--space-md);
}

.bgsync-log .admin-msg {
    margin-bottom: 0;
}

.bgsync-log-time {
    opacity: 0.6;
    font-variant-numeric: tabular-nums;
    margin-inline-end: var(--space-xs);
}

/* A placeholder / status line (no timestamp) — slightly muted + italic so it
   reads as a notice, not a logged event. */
.bgsync-log-status {
    opacity: 0.75;
    font-style: italic;
}

/* CSV import gate — the acknowledgement an admin must tick before a CSV that
   doesn't match the league (or that would erase already-played results) can arm
   the page's existing "Confirm & Stage" button. Deliberately loud: this is the
   last thing standing between a bad file and the league's data. */
.csv-import-gate {
    display: flex;
    align-items: flex-start;
    gap: var(--space-sm);
    margin-top: var(--space-md);
    padding: var(--space-sm) var(--space-md);
    border: 1px solid var(--color-danger);
    border-radius: var(--radius-md);
    cursor: pointer;
    font-weight: 600;
}

.csv-import-gate input[type="checkbox"] {
    margin-top: 0.15em;
    flex-shrink: 0;
    cursor: pointer;
}

/* Collapsible log block — a tappable "Activity log / Last run" header that
   hides/shows its body. Styled as a clear, full-width bar (reads as a button,
   comfortable to tap on mobile). Set off from the controls above it with a top
   rule + generous spacing. State persists in localStorage. */
.sync-log-collapsible {
    margin-top: var(--space-lg);
    padding-top: var(--space-md);
    border-top: 1px solid var(--color-border);
}

.sync-log-toggle {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    width: 100%;
    min-height: 2.75rem;               /* ≥44px — comfortable mobile tap target */
    padding: var(--space-sm) var(--space-md);
    background: var(--color-inset);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    color: var(--color-text);
    font: inherit;
    font-size: var(--fs-090);
    font-weight: 600;
    transition: background 0.15s, border-color 0.15s;
}

.sync-log-toggle:hover,
.sync-log-toggle:focus-visible {
    background: var(--color-hover);
    border-color: var(--color-accent);
    outline: none;
}

.sync-log-caret {
    font-size: 0.9em;
    line-height: 1;
    color: var(--color-text-muted);
}

.sync-log-title {
    flex: 1;
    text-align: left;
}

/* The Show / Hide affordance — a pill that clearly reads as the action. */
.sync-log-hint {
    margin-inline-start: auto;
    flex: none;
    font-weight: 600;
    font-size: var(--fs-085);
    color: var(--color-accent);
    padding: 0.15em 0.7em;
    border: 1px solid var(--color-accent);
    border-radius: 999px;
    white-space: nowrap;
}

.sync-log-toggle:hover .sync-log-hint,
.sync-log-toggle:focus-visible .sync-log-hint {
    background: var(--color-accent);
    color: #fff;
}

.sync-log-body {
    margin-top: var(--space-md);
}

/* ---- Run Now: estimate + live stage timers ---- */
/* The estimate an admin sees BEFORE running ("~2:15 if all goes smoothly"). */
.sync-eta {
    margin-bottom: var(--space-sm);
    color: var(--color-text-muted);
    font-size: var(--fs-090);
}

.sync-eta b {
    color: var(--color-text);
}

/* Stage list shown DURING a run. Lives outside the collapsible log, so progress
   stays visible even when the log lines are hidden. */
.sync-stages-panel {
    margin-top: var(--space-md);
    padding: var(--space-md);
    background: var(--color-inset);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    /* Run Now scrolls this panel to the top of the viewport (revealAtTop). No
       scroll-margin-top here on purpose: that only feeds scrollIntoView, and a
       CSS constant cannot know the fixed topbar's real height (nor that the bar
       is only present while the sidebar is CLOSED). revealAtTop measures the bar
       live and adds the gap itself — see js/utils/scrollOffset.js. */
}

.sync-stages-head {
    display: flex;
    align-items: baseline;
    gap: var(--space-sm);
    margin-bottom: var(--space-sm);
}

.sync-stages-title {
    font-weight: 600;
}

.sync-stages-remaining {
    margin-inline-start: auto;
    color: var(--color-text-muted);
    font-size: var(--fs-085);
    font-variant-numeric: tabular-nums;
}

.sync-stages {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* Each stage: [icon] name … elapsed/expected, with a progress bar underneath.
   The bar spans the full row (grid column 1/-1) so it reads as one meter. */
.sync-stage {
    display: grid;
    grid-template-columns: 1.25rem 1fr auto;
    align-items: center;
    gap: 0 var(--space-sm);
    padding: var(--space-xs) 0;
    color: var(--color-text-muted);
}

.sync-stage-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.sync-stage-time {
    font-size: var(--fs-085);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.sync-stage-icon::before {
    content: '○';
}

.sync-stage-bar {
    grid-column: 1 / -1;
    height: 3px;
    margin-top: 4px;
    border-radius: 999px;
    background: var(--color-border);
    overflow: hidden;
}

.sync-stage-bar i {
    display: block;
    width: 0;
    height: 100%;
    border-radius: inherit;
    background: var(--color-accent);
    transition: width 0.9s linear;   /* matches the 1s tick — the bar glides */
}

/* Running: full-strength text + a pulsing dot. */
.sync-stage.is-active {
    color: var(--color-text);
    font-weight: 600;
}

.sync-stage.is-active .sync-stage-icon::before {
    content: '●';
    color: var(--color-accent);
    animation: sync-stage-pulse 1.4s ease-in-out infinite;
    display: inline-block;
}

@keyframes sync-stage-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.3; }
}

/* Done: a tick and a filled bar. */
.sync-stage.is-done .sync-stage-icon::before {
    content: '✓';
    color: var(--color-success, var(--color-accent));
}

.sync-stage.is-done .sync-stage-bar i {
    background: var(--color-success, var(--color-accent));
}

/* Failed: the stage stops where it stopped. */
.sync-stage.is-failed {
    color: var(--color-text);
}

.sync-stage.is-failed .sync-stage-icon::before {
    content: '✕';
    color: var(--color-danger, #e05260);
}

.sync-stage.is-failed .sync-stage-bar i {
    background: var(--color-danger, #e05260);
}

@media (prefers-reduced-motion: reduce) {
    .sync-stage-bar i { transition: none; }
    .sync-stage.is-active .sync-stage-icon::before { animation: none; }
}

/* ---- Publish Progress ---- */
.publish-progress {
    margin-top: var(--space-md);
}

.publish-progress-bar {
    height: 8px;
    background: var(--color-border);
    border-radius: var(--radius-full);
    overflow: hidden;
    margin-bottom: var(--space-sm);
}

.publish-progress-fill {
    height: 100%;
    background: var(--color-accent);
    border-radius: var(--radius-full);
    transition: width 0.3s;
}

.publish-progress-text {
    font-size: var(--fs-085);
    color: var(--color-text-muted);
}

/* ---- View Site link ---- */
.admin-view-site {
    text-decoration: none;
    color: var(--color-text-muted);
    display: block;
}

.admin-view-site:hover {
    color: var(--color-accent);
}

/* ---- Match filter bar ---- */
.match-filter-bar {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    margin-bottom: var(--space-md);
}

.match-filter-bar input {
    padding: 6px 10px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    /* font-size is canonical — see .app-search-input in layout.css */
    background: var(--color-surface);
    color: var(--color-text);
    min-width: 200px;
}

.match-filter-bar label {
    font-size: var(--fs-085);
    color: var(--color-text-secondary);
    font-weight: 600;
}

/* ---- Round selection pills (F3 Round Editor) ----
   Shows one round at a time instead of every round's match table rendering
   simultaneously — the round editor was the heaviest-DOM admin view (every
   match row carries 4 live inputs + 6 buttons; multiplied by every round in
   the league at once). Existing tokens (surface/border/accent/bg) already
   give the pill enough contrast in every theme — same tokens
   .subtab--accordion's toggle state already relies on — so no new colour
   token was needed. */
.round-pills-bar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-xs);
    margin-bottom: var(--space-md);
}

/* ---- Bulk technical-loss bar (F3 Round Editor) ----
   Appears only when the filter names a single player; forfeits all their
   matches in one staged action. A warning-tinted notice so a destructive
   bulk action reads distinctly from the neutral filter/pills chrome above
   and below it. Reuses existing tokens — no new colour token needed. */
.round-bulk-bar {
    display: flex;
    flex-wrap: wrap;
    /* NB: an explicit display wins over the `hidden` attribute's display:none,
       so hidden must be re-asserted or the bar shows with no player selected. */
    align-items: center;
    gap: var(--space-sm);
    margin-bottom: var(--space-md);
    padding: var(--space-sm) var(--space-md);
    border: 1px solid var(--color-warning);
    border-radius: var(--radius-sm);
    background: color-mix(in srgb, var(--color-warning) 12%, var(--color-surface));
}
.round-bulk-bar[hidden] {
    display: none;
}
.round-bulk-text {
    flex: 1 1 auto;
    min-width: 0;
    font-size: var(--fs-085);
    color: var(--color-text-secondary);
}
.round-bulk-text strong {
    color: var(--color-text);
}
.round-pill {
    padding: 4px 14px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-full);
    background: var(--color-surface);
    color: var(--color-text-secondary);
    font-family: var(--font-main);
    font-size: var(--fs-085);
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
    user-select: none;
}
.round-pill:hover {
    border-color: var(--color-accent);
    color: var(--color-accent);
}
.round-pill.active {
    background: var(--color-accent);
    border-color: var(--color-accent);
    color: var(--color-bg);
}
.round-pill.active:hover {
    color: var(--color-bg);
}
.round-pill-dot {
    display: inline-block;
    width: 6px;
    height: 6px;
    margin-left: 6px;
    border-radius: 50%;
    background: var(--color-warning);
    vertical-align: middle;
}
.round-pill.active .round-pill-dot {
    background: var(--color-bg);
}

/* ---- Inline match editing ---- */
.admin-table-compact th,
.admin-table-compact td {
    padding: 4px 5px;
    font-size: var(--fs-080);
}

.inline-edit-input {
    width: 52px;
    padding: 2px 3px;
    border: 1px solid var(--color-border);
    border-radius: 3px;
    font-size: var(--fs-080);
    text-align: center;
    background: var(--color-surface);
    color: var(--color-text);
}

.inline-edit-input:focus {
    border-color: var(--color-accent);
    outline: none;
}

.inline-edit-score {
    width: 42px;
}

/* Admin table compact inputs/selects (player rows, flag uploads) */
.player-name-input,
.player-flag-select,
.player-flag-custom,
[data-field="name"],
[data-field="flag"],
#new-upload-flag-code,
#upload-flag-code {
    background: var(--color-surface);
    color: var(--color-text);
}

/* ── F7 — Sync ▸ Active Leagues in-cell controls ───────────────────────────
   The Source League Name text input and the plan-membership checkboxes live in
   FF table cells, OUTSIDE .form-group, so they miss the themed form-control
   styling and would render browser-default (a white box in dark themes). Give
   the input the same surface/text/border/focus treatment as .form-group input,
   and pin the checkboxes to the theme accent. */
.sync-source-name {
    box-sizing: border-box;
    min-width: 160px;
    padding: 2px 6px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    font-family: inherit;
    font-size: var(--fs-090);
    color: var(--color-text);
    background: var(--color-surface);
    accent-color: var(--color-accent);
    transition: border-color 0.15s;
}
.sync-source-name:focus {
    outline: none;
    border-color: var(--color-accent);
    box-shadow: 0 0 0 2px var(--color-accent-light);
}
.sync-plan-member,
.sync-runnow-pick {
    accent-color: var(--color-accent);
}

/* Run-time hour/minute selects — inline (auto width), overriding the
   full-width .form-group select default so they read as one "HH : MM" clock. */
.sync-plan-time-row select {
    width: auto;
    min-width: 4.5em;
    padding: var(--space-xs) var(--space-sm);
}

.sync-plan-time-colon {
    font-weight: 600;
    color: var(--color-text-muted);
}

/* .btn-xs / .btn-tech now live in the unified button ramp above. */

.btn-save-match:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}
.btn-save-ready {
    background: var(--color-win, #16a34a) !important;
    color: #fff !important;
    border-color: var(--color-win, #16a34a) !important;
    animation: pulse-save 1.5s ease-in-out infinite;
}
@keyframes pulse-save {
    0%, 100% { box-shadow: 0 0 4px rgba(22, 163, 74, 0.3); }
    50% { box-shadow: 0 0 12px rgba(22, 163, 74, 0.6); }
}

.match-row-unplayed {
    opacity: 0.5;
}

.match-row-overridden {
    background: var(--color-accent-light);
}

.nowrap {
    white-space: nowrap;
}

.edit-ts {
    font-size: var(--fs-070);
    color: var(--color-text-muted);
}

/* ----------------------------------------------------------------------
   Table F — Round Editor (admin.html, ManualEntry leagues)
   2 rows per match (one per player). EDITED + ACTIONS rowspan=2.
   Sticky: leftmost (PLAYERS) column only — per TABLE-DESIGN.md iron rules.
   The scroll container is .round-editor-scroll (the wrapper). The table
   itself stays overflow:visible; backgrounds live on td (not tr) so the
   sticky column stays opaque under non-sticky cells.
   ---------------------------------------------------------------------- */
.admin-round-table {
    border-collapse: separate;
    border-spacing: 0;
    width: 100%;
    min-width: max-content;
}

.admin-round-table th,
.admin-round-table td {
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
}

.admin-round-table thead th {
    background: var(--color-bg);
}

/* Leftmost column = PLAYERS = the only sticky column.
   Static edge removed — FF chrome adds drop-shadow on horizontal scroll
   (see the .ff-wrap.is-scrolled-x rules + F3↔FF reconciliation below). */
.admin-round-table .match-player-cell {
    position: sticky;
    left: 0;
    z-index: 2;
}

.admin-round-table thead .match-player-cell {
    z-index: 3;
}

/* Each match = one <tbody class="match-block"> with two <tr>s.
   Border belongs to the *bottom* of each match-block and spans all 6 columns.
   Row-b carries it for the per-row columns; the rowspan'd EDITED/ACTIONS
   cells (physically in row-a) carry it for the right two columns.
   Suppressed for :last-child so the table doesn't end with a hanging line. */
.admin-round-table .match-block .match-row-a td {
    border-bottom: none;
}
.admin-round-table .match-block:not(:last-child) .match-row-b td {
    border-bottom: 1px solid var(--color-border);
}
.admin-round-table .match-block:not(:last-child) .match-row-a .match-edited,
.admin-round-table .match-block:not(:last-child) .match-row-a .match-actions {
    border-bottom: 1px solid var(--color-border);
}
.admin-round-table .match-block:last-child .match-row-b td {
    border-bottom: none;
}

/* Match-state colouring tints the WHOLE match-block row (incl. the sticky
   first column). The match-block is the <tbody> itself, so the selector is
   `tbody.match-block-X td` (not `tbody .match-block-X td` — there is no inner
   tbody to descend into). Scoped with the full FF table prefix so specificity
   (0,4,2) beats FF's base body well `.admin-table.font-large tbody td` (0,2,2,
   --color-inset) and its sticky `td:first-child` (0,2,1) — otherwise the FF
   inset tier masks the state tint on every cell (regression fixed 2026-06-24). */
.admin-table.font-large.admin-round-table tbody.match-block-unplayed td {
    background: var(--color-inset);
    color: var(--color-text-muted);
    opacity: 0.6;
}

/* Sticky first col (PLAYERS) must stay fully opaque — per TABLE-DESIGN.md
   iron rules, sticky cells can never let the horizontally-scrolled columns
   beneath them show through. `opacity: 0.6` above applies to the *whole*
   cell paint (background + text), and since this td is `position: sticky`
   it composites over whatever has scrolled underneath — so on an unplayed
   match-block, the sticky column visibly picked up the scrolled-under text.
   Reset opacity to 1 here and fade only the flag icon instead, so the
   muted look survives without breaking sticky opacity (regression fixed
   2026-07-03). */
.admin-table.font-large.admin-round-table tbody.match-block-unplayed td.match-player-cell {
    opacity: 1;
}
.admin-table.font-large.admin-round-table tbody.match-block-unplayed td.match-player-cell .flag {
    opacity: 0.6;
}

.admin-table.font-large.admin-round-table tbody.match-block-overridden td {
    background: var(--color-accent-light);
}

.admin-table.font-large.admin-round-table tbody.match-block-pending td {
    background: var(--color-warning-bg);
}

/* HOVER (colour E) — one UNIFORM colour over the WHOLE match-block on hover,
   regardless of status (played / unplayed / pending / overridden). Hovering any
   cell, sub-row, or input lights the entire tbody.match-block as a single unit.
   `:has(:hover)` + the full FF prefix gives specificity (0,5,2), which out-
   specifies FF's base body well (0,2,2), its row hover (0,3,2), its sticky-col
   hover (0,4,3), AND every per-state resting tint (0,4,2) — so the whole block
   (both sub-rows + the sticky first column) flips to one colour and NO first-col
   hover reconciliation is needed. `opacity: 1` overrides the unplayed resting
   opacity:.6 so an unplayed block hovers at the SAME full strength as a played
   one (its hover used to look faint/partial). This is the single hover rule for
   ALL statuses — there are intentionally no per-state hover overrides.
   (uniform whole-block hover, status-independent, 2026-06-24) */
.admin-table.font-large.admin-round-table tbody.match-block:has(:hover) td {
    background: var(--color-hover);
    opacity: 1;
}

/* ─────────────────────────────────────────────────────────────
   F5 — CSV Import Preview (Edit League ▸ Import CSV/Excel)
   ─────────────────────────────────────────────────────────────
   The only FF table rendered by the lab's `mountFFTable` (F1–F4/F6/F7 still
   hand-build the same chrome). One row per NEW match in the uploaded file,
   with TA/TB/TD/NP row actions mirroring F3's — but acting on the IMPORT, not
   on the league.

   Row-state model, 3 resting states + 1 hover, following F3's per-block model:
     1. import  (default) → FF's inset well, untouched
     2. skipped (NP)      → muted text, "not played" — row will be staged zeroed
     3. technical (TA/TB/TD) → --color-accent-light, matching F3's "overridden"
     4. hover → --color-hover, STATUS-INDEPENDENT (one rule, no per-state hovers)

   Specificity: the state rules are (0,4,3) — enough to beat FF's base body well
   `.admin-table.font-large tbody td` (0,2,2) and its sticky `td:first-child`
   (0,2,1). FF's own row hover is only (0,3,3), so the F5 hover below re-asserts
   at (0,5,3) via `tr.f5-row:hover` and covers the sticky first column too.

   NOTE — no `opacity` on the skipped state. F3 learned the hard way (2026-07-03)
   that fading a row whose first column is `position: sticky` lets the
   horizontally-scrolled cells show through it. Muted *colour* achieves the same
   read with none of that, so F5 never needed the first-col reconciliation. */
.admin-table.font-large[data-mf-table-id="F5"] tbody tr.f5-row-skipped td {
    background: var(--color-inset);
    color: var(--color-text-muted);
    font-style: italic;
}

.admin-table.font-large[data-mf-table-id="F5"] tbody tr.f5-row-tech td {
    background: var(--color-accent-light);
}

/* Hover (status-independent) — must come AFTER the state rules AND out-specify
   them; `tr.f5-row:hover` gives (0,5,3) vs their (0,4,3). Covers the sticky
   first column in the same rule, so no separate first-col hover is needed. */
.admin-table.font-large[data-mf-table-id="F5"] tbody tr.f5-row:hover td {
    background: var(--color-hover);
}

.admin-table.font-large[data-mf-table-id="F5"] .f5-actions,
.admin-table.font-large[data-mf-table-id="F5"] .f5-status {
    vertical-align: middle;
    white-space: nowrap;
}

.ff-wrap .f5-actions .btn {
    margin: 0.1em 0.15em;
}

/* Result tags — em-sized so they track the cell's fluid font like FF's buttons. */
.f5-tag {
    display: inline-block;
    padding: 0.15em 0.5em;
    border-radius: 0.9em;
    font-size: 0.8em;
    font-weight: 600;
    font-style: normal;
    white-space: nowrap;
}
.f5-tag-import {
    background: var(--color-inset);
    color: var(--color-text-muted);
    border: 1px solid var(--color-border);
}
.f5-tag-tech {
    background: var(--color-accent);
    color: var(--color-bg);
}
.f5-tag-skip {
    background: transparent;
    color: var(--color-text-muted);
    border: 1px dashed var(--color-border);
}

/* All cells left-aligned per TABLE-DESIGN.md iron rule 7. */
.admin-round-table th,
.admin-round-table td {
    text-align: left;
}

.admin-round-table .match-edited {
    vertical-align: middle;
}

.admin-round-table .match-actions {
    vertical-align: middle;
    white-space: nowrap;
}

.admin-round-table .match-actions .btn {
    margin: 1px 2px;
}

/* Tight inputs so columns size to their data (the table already uses
   min-width: max-content; otherwise the default 150px <input type=number>
   dominates column width). */
.admin-round-table .inline-edit-input {
    width: 4em;
    -moz-appearance: textfield;
}
.admin-round-table .inline-edit-input.inline-edit-score,
.admin-round-table .inline-edit-input.inline-edit-score-select {
    width: 3.2em;
}
.admin-round-table .inline-edit-input.inline-edit-score-select {
    /* Score <select> needs extra room for the arrow chrome on top of digits.
       em-based so it scales with the cell font (fluid via --fs-093). */
    width: 3.8em;
    min-width: 3.8em;
}
.admin-round-table .inline-edit-input::-webkit-outer-spin-button,
.admin-round-table .inline-edit-input::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

/* Score <select> dropdown — same chrome as the number inputs it replaces.
   All sizing in em so the arrow gutter and the digit gutter scale together
   with the cell font (fluid via --fs-093). The old px values (18px right
   padding, 5px arrow inset) stayed constant while width was em — at mobile
   font sizes the px gutter ate ~half the box and clipped the digit. */
.admin-round-table select.inline-edit-score-select {
    padding: 0.15em 1.4em 0.15em 0.45em;
    border: 1px solid var(--color-border);
    border-radius: 0.2em;
    background-color: var(--color-surface);
    color: var(--color-text);
    font-size: var(--fs-080);
    font-family: inherit;
    cursor: pointer;
    accent-color: var(--color-accent);
    appearance: none;
    -webkit-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='%232563eb' d='M0 0l5 6 5-6z'/></svg>");
    background-repeat: no-repeat;
    background-position: right 0.4em center;
    background-size: 0.6em 0.4em;
}
.admin-round-table select.inline-edit-score-select:focus {
    border-color: var(--color-accent);
    outline: none;
    box-shadow: 0 0 0 2px var(--color-accent-light);
}

/* Themed <input type="date"> — used for Issue Date (form) and EDITED column.
   accent-color tints the popup calendar in modern browsers. color-scheme is
   the only knob the browser exposes for the popup itself: 'light' renders
   the OS light calendar (white bg), 'dark' renders the OS dark calendar
   (dark-grey bg). We pick whichever is closest per theme below. */
.themed-date {
    accent-color: var(--color-accent);
    color-scheme: light;
    font-family: inherit;
    color: var(--color-text);
    background: var(--color-surface);
}
/* Note: the ~2px date/time widget-chrome height compensation lives in the
   `.edit-card-sm` block above (responsive em padding), so it is not repeated
   here. */
.themed-date::-webkit-calendar-picker-indicator {
    cursor: pointer;
    opacity: 0.85;
    filter: invert(28%) sepia(95%) saturate(2200%) hue-rotate(213deg) brightness(95%) contrast(95%);
    transition: opacity 0.15s, transform 0.15s;
}
.themed-date::-webkit-calendar-picker-indicator:hover {
    opacity: 1;
    transform: scale(1.1);
}

/* Dark themes — surface lightness < 50%, so the white popup is jarring.
   Switch the popup to the browser's dark calendar (dark-grey bg, light text)
   and drop the blue-tint filter from the icon so it stays light-on-dark. */
[data-theme="dark"] .themed-date,
[data-theme="vegas"] .themed-date,
[data-theme="casino"] .themed-date,
[data-theme="x22"] .themed-date {
    color-scheme: dark;
}
[data-theme="dark"] .themed-date::-webkit-calendar-picker-indicator,
[data-theme="vegas"] .themed-date::-webkit-calendar-picker-indicator,
[data-theme="casino"] .themed-date::-webkit-calendar-picker-indicator,
[data-theme="x22"] .themed-date::-webkit-calendar-picker-indicator {
    filter: none;
}

/* Compact themed date input inside the EDITED cell. */
.admin-round-table .match-edited-date {
    width: 8.5em;
    padding: 2px 4px;
    border: 1px solid var(--color-border);
    border-radius: 3px;
    font-size: var(--fs-078);
    line-height: 1.2;
}
.admin-round-table .match-edited-date:focus {
    border-color: var(--color-accent);
    outline: none;
    box-shadow: 0 0 0 2px var(--color-accent-light);
}

/* Flag thumbnails inside FF tables — mirror of the canonical .flag rule
   in table-lab/formats/base/base.css. em-based: height tracks cell font
   (fluid via --fs-093), so the flag scales with text at every viewport.
   image-rendering:auto is explicit — guards against any inherited
   `crisp-edges`/`pixelated` from other CSS layers (the 2026-06-03 F3
   pixelation bug was the canonical retrospective).
   admin.html doesn't load base.css/components.css, so the canonical
   rule is duplicated here as an admin-side mirror. */
.ff-wrap .flag {
    height: 1em;
    width: auto;
    margin-right: 0.3em;
    object-fit: contain;
    border-radius: 0.15em;
    vertical-align: middle;
    image-rendering: auto;
}

/* ----------------------------------------------------------------------
   F3 ↔ FF chrome reconciliation
   F3 wears the FF chrome (.ff-wrap + .admin-table.font-large) like F1/F2/F4
   but has 2 structural quirks that need surgical overrides:
     1. Hairlines come from per-match-block <tbody> border-bottom logic
        (see .match-block rules above), not from FF's universal box-shadow.
     2. Match-state coloring (.match-block-overridden/-pending/-unplayed)
        sets td backgrounds — must beat FF's first-col background rule.
   ---------------------------------------------------------------------- */

/* 1. Suppress FF's universal cell hairline — F3 owns its own per-match
      border logic above. */
.admin-table.font-large.admin-round-table tbody td {
    box-shadow: none;
}

/* 1b. Re-add drop-shadow on the sticky first col when wrapper is scrolled.
       Body cells: shadow only (F3's per-match border-bottom logic handles
       horizontal hairlines). Header cells: shadow + inset (the inset is the
       thead/body separator and must stay visible while scrolled). */
.ff-wrap.is-scrolled-x .admin-table.font-large.admin-round-table tbody td:first-child {
    box-shadow: 6px 0 8px -4px rgba(0, 0, 0, 0.18);
}
.ff-wrap.is-scrolled-x .admin-table.font-large.admin-round-table thead th:first-child {
    box-shadow:
        inset 0 -1px 0 var(--color-border),
        6px 0 8px -4px rgba(0, 0, 0, 0.18);
}

/* 2. Hover (whole-block) on the sticky first column is already handled by the
      `tbody.match-block:has(:hover) td` rules near the top of this F3 section
      — their (0,5,2) specificity covers `td:first-child` too, so no separate
      first-col hover reconciliation is needed. The resting first-col tint comes
      from the `tbody.match-block-X td` state rules (overridden/pending/unplayed)
      or the FF inset base (default played). */

/* ---- Mobile topbar (hamburger + title + user chip) ----
   Hidden on desktop; on mobile replaces the floating standalone hamburger.
   Lives inside .admin-mobile-topbar which is fixed across the viewport top. */
.admin-mobile-topbar {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: calc(60px + env(safe-area-inset-top, 0px));
    padding: env(safe-area-inset-top, 0px) 16px 0 8px;
    background: var(--header-bg);
    color: var(--header-text, #fff);
    z-index: 110;
    align-items: center;
    gap: 4px;
    box-shadow: 0 1px 0 rgba(255,255,255,0.07), 0 4px 16px rgba(15,23,42,0.22);
}

.admin-topbar-logo {
    height: 32px;
    width: 32px;
    object-fit: contain;
    flex-shrink: 0;
    border-radius: 50%;
}

.admin-topbar-title {
    flex: 1;
    font-size: var(--fs-088);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: rgba(255,255,255,0.88);
    padding-left: 6px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.admin-topbar-user {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 5px 10px 5px 6px;
    border-radius: 22px;
    background: rgba(255,255,255,0.07);
    border: 1px solid rgba(255,255,255,0.12);
    max-width: 160px;
    flex-shrink: 0;
}

.admin-topbar-avatar {
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-072);
    font-weight: 800;
    color: #fff;
    flex-shrink: 0;
    text-transform: uppercase;
}

.admin-topbar-name {
    font-size: var(--fs-078);
    font-weight: 600;
    color: rgba(255,255,255,0.88);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Hamburger lives inside .admin-mobile-topbar on mobile — no fixed position needed. */
.admin-hamburger {
    display: inline-flex;
    position: static;
    width: 44px;
    height: 44px;
    padding: 0;
    border: none;
    border-radius: 10px;
    background: transparent;
    color: var(--header-text, #fff);
    cursor: pointer;
    align-items: center;
    justify-content: center;
    transition: background-color 160ms ease-out;
    flex-shrink: 0;
}
.admin-hamburger:hover  { background: rgba(255,255,255,0.09); }
.admin-hamburger:active { background: rgba(255,255,255,0.15); }

.admin-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    /* Navy-tinted rather than pure black — app recedes into the sidebar's
       color world instead of just dimming. */
    background: rgba(15, 23, 42, 0.55);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
    z-index: 99;
    opacity: 0;
    transition: opacity 260ms cubic-bezier(0.32, 0.72, 0, 1);
}
.admin-backdrop.visible {
    display: block;
    opacity: 1;
}

/* Staggered nav reveal — plays once the drawer has started entering.
   Each nav item fades + slides 8px with 40ms offset. Subtle, not showy. */
@keyframes admin-nav-reveal {
    from { opacity: 0; transform: translateX(-8px); }
    to   { opacity: 1; transform: translateX(0); }
}

/* ---- Responsive: mobile drawer ----
   The mobile drawer behavior is now driven by `body.site-sidebar-closed`
   (toggled by the universal hamburger in js/render/sidebarToggle.js). On
   mobile the sidebar is CLOSED by default; opening it overlays content.
   The legacy `.admin-mobile-topbar` is no longer mounted — the floating
   hamburger replaces it, and the page title <h1> in the main content
   provides the "where am I" cue. */
@media (max-width: 767px) {
    .admin-layout {
        flex-direction: column;
    }

    /* .admin-sidebar's mobile off-canvas drawer + backdrop are handled
       entirely by the shared .site-sidebar rules in site-sidebar.css (the
       aside carries both classes — see js/admin/render/adminSidebarNav.js). */

    .admin-main {
        /* Top padding to clear the bar is applied by the closed-state rule
           (body.site-sidebar-closed .admin-main) so no gap remains when the
           drawer is open and the bar is hidden. */
        padding: 0 var(--space-md) var(--space-md);
        width: 100%;
        margin-left: 0 !important;       /* main fills viewport — sidebar overlays */
    }
}

/* Home + Logout footer items now reuse .site-nav-item / .site-sidebar-logout
   directly (css/site-sidebar.css) — see js/admin/render/adminSidebarNav.js. */

/* ---- Add League form: card grid ---- */
.add-league-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-md);
    margin-bottom: var(--space-md);
}
.add-league-grid .admin-card.full {
    grid-column: 1 / -1;
}
@media (max-width: 768px) {
    .add-league-grid { grid-template-columns: 1fr; }
}
.add-league-row {
    display: flex;
    gap: var(--space-md);
    flex-wrap: wrap;
}
.add-league-row .form-group {
    flex: 1;
    min-width: 100px;
}
@media (max-width: 480px) {
    .add-league-row {
        flex-direction: column;
        align-items: stretch !important;
    }
    .add-league-row .form-group {
        min-width: 0;
        width: 100%;
        text-align: left;
    }
}

/* ---- Input + action row (input/file-picker beside a button) ----------
   One layout for every "field + button" pair (Add Player, Upload CSV,
   Upload Flag …). The field flexes; the button keeps its intrinsic
   width and stays vertically centred with the field. Replaces the
   ad-hoc inline `display:flex;align-items:flex-end` + `margin-top`
   hacks that left buttons floating beside helper text. */
.input-action-row {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex-wrap: wrap;
}
.input-action-row > .ac-field,
.input-action-row > .custom-file-input,
.input-action-row > select {
    flex: 1;
    min-width: 0;
}
/* Autocomplete anchor — dropdown positions against the field, not the row. */
.ac-field { position: relative; }

/* Short fixed-width input (e.g. a 2-letter flag code) inside an action row.
   Font + padding mirror .btn-sm so it lines up to the same height as the
   adjacent "Choose File" button (both border-box, 1px border). */
.input-action-row .input-code {
    width: 4.5em;
    flex: 0 0 auto;
    text-align: center;
    text-transform: uppercase;
    font-size: var(--fs-082);
    padding: 0.4em 0.8em;
    line-height: 1.2;
}

/* Roster heading + its Clear All action on one line (Add New League). */
.players-list-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-sm);
    margin-bottom: var(--space-xs);
}

/* Inline checkbox option under an action row (e.g. "Also copy its players"
   beside the Add-League preset picker). Sits between the row and its hint. */
.preset-opt {
    display: flex;
    align-items: center;
    gap: var(--space-xs);
    margin-top: var(--space-xs);
    font-size: var(--fs-082);
    color: var(--color-text-secondary);
    cursor: pointer;
}
.preset-opt input { cursor: pointer; }

/* Helper text under a field (kept on its own line, full width). */
.form-hint {
    display: block;
    margin-top: var(--space-xs);
    font-size: var(--fs-075);
    color: var(--color-text-muted);
}

/* Sub-heading inside an admin card — one step above the field label on
   the type scale, so the hierarchy stays linear (h2 ▸ subhead ▸ label). */
.admin-subhead {
    font-size: var(--fs-110);
    font-weight: 600;
    color: var(--color-text-secondary);
    margin: 0 0 var(--space-sm);
}
.admin-subhead:not(:first-child) { margin-top: var(--space-lg); }

/* ---- Custom file input (force English labels) ---- */
.custom-file-input {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
}
.custom-file-input input[type="file"] {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    overflow: hidden;
    pointer-events: none;
}
/* The visible "Choose File" label reuses the button primitive
   (.btn .btn-secondary .btn-sm). When it lives inside a .form-group the
   broad `.form-group label` rule would otherwise win (block display,
   --fs-090, margin) and make it taller than its sibling buttons — so
   restore the button box here (matches .btn-sm height). */
.form-group label.btn {
    display: inline-flex;
    margin-bottom: 0;
    font-size: var(--fs-082);
}
.custom-file-input .file-name {
    font-size: var(--fs-085);
    color: var(--color-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

/* ---- Dashboard manager view ---- */
.dashboard-manager-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-md);
}
@media (max-width: 900px) {
    .dashboard-manager-grid { grid-template-columns: 1fr; }
}
.dashboard-list {
    list-style: none;
    padding: 0;
    margin: 0;
}
.dashboard-list li {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-sm);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    margin-bottom: var(--space-xs);
    background: var(--color-bg);
}
.dashboard-list li.dragging { opacity: 0.4; }
.dashboard-list li.drag-over { border-color: var(--color-accent); }
.dashboard-list .drag-handle {
    cursor: grab;
    color: var(--color-text-muted);
    user-select: none;
    font-size: var(--fs-110);
}
.dashboard-list .league-name {
    flex: 1;
}
.dashboard-save-bar {
    margin-top: var(--space-md);
    display: flex;
    gap: var(--space-sm);
}

/* ---- Player manager ---- */
/* The Players tab's own result list is GONE: that search now runs on the
   canonical field (js/utils/combobox.js), so the list, its hover state and its
   flag spacing all come from .app-combo-dropdown like every other search on the
   site. Only .player-meta-tag survives — it still labels a player's state in the
   edit form's heading. */
.player-meta-tag {
    display: inline-block;
    margin-left: var(--space-xs);
    padding: 1px 6px;
    background: var(--color-accent-light, #dbeafe);
    color: var(--color-accent, #2563eb);
    border-radius: var(--radius-sm);
    font-size: var(--fs-070);
    font-weight: 600;
}
.player-meta-inactive {
    background: var(--color-completed-bg);
    color: var(--color-completed);
}
.player-meta-hidden {
    background: color-mix(in srgb, var(--color-loss) 18%, transparent);
    color: var(--color-loss);
}
.player-photo-row {
    display: flex;
    align-items: center;
    gap: var(--space-md);
    flex-wrap: wrap;
}
.player-photo-preview {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    border: 2px solid var(--color-border);
    background: var(--color-bg);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    flex-shrink: 0;
}
.player-photo-preview img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}
.player-photo-preview .muted {
    color: var(--color-text-muted);
    font-size: var(--fs-075);
}
.player-preview-frame {
    width: 100%;
    height: 600px;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    background: var(--color-surface);
}

/* ---- Player autocomplete dropdown ---- */
.player-autocomplete {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    list-style: none;
    margin: 0;
    padding: 4px 0;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-sm);
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    z-index: 100;
    max-height: 200px;
    overflow-y: auto;
}
.player-autocomplete[hidden] { display: none; }
.player-autocomplete li {
    padding: 8px 12px;
    cursor: pointer;
    font-size: var(--fs-090);
}
.player-autocomplete li:hover {
    background: var(--color-hover);
}
.player-autocomplete .ac-hint {
    color: var(--color-text-muted);
    font-size: 0.82em;
    font-weight: 400;
}
/* Inline flag left of the name (this list is block, not flex — the shared
   .search-flag rule sizes it; here we just space + baseline-align it). */
.player-autocomplete .search-flag {
    margin-right: var(--space-xs);
    vertical-align: middle;
}

/* ========================================================================
   Step 6.6b — Mobile Matrix for Admin Pages
   admin.html does NOT load components.css, so these rules are standalone.
   ======================================================================== */

/* Dual-label toggle (Step 6.6 pattern, standalone for admin-only pages) */
.th-abbr { display: none; }

@media (max-width: 640px) {
    .th-full { display: none; }
    .th-abbr { display: inline; }

    /* Admin table compression — font-large tables keep their enum size (iron rule 3) */
    .admin-table:not(.font-large) {
        font-size: var(--fs-070);
        width: 100%;
        table-layout: auto;
    }
    .admin-table:not(.font-large) thead th {
        padding: 3px 2px;
        font-size: var(--fs-065);
        white-space: nowrap;
    }
    .admin-table:not(.font-large) tbody td {
        padding: 2px 3px;
    }
    .admin-table:not(.font-large) tbody td input[type="text"],
    .admin-table:not(.font-large) tbody td input[type="number"],
    .admin-table:not(.font-large) tbody td select {
        font-size: var(--fs-070);
        padding: 1px 3px;
    }

    /* Forms: add-league grid collapses to 1-col (also set at 768px) */
    .add-league-grid { grid-template-columns: 1fr; }

    /* Edit-league form uses inline flex-wrap with min-width:80-140px;
       force each form-group to stack full-width on mobile */
    .admin-content .form-group {
        flex: 1 1 100% !important;
        min-width: 0 !important;
    }

    /* CSV editor is 10 cols — stays on horizontal scroll (min-width:max-content) */
    .admin-table-compact { min-width: max-content; }
    .table-scroll { overflow-x: auto; }
}

/* ---- League-type pill (mirrors components.css for admin page, which
   doesn't load components.css) ----
   `1em` — tracks the table it sits in; see the long note above .status-pill in
   components.css. This copy MUST move with that one: F1 shows the type pill and
   the status pill in adjacent columns, so a change applied to only one file
   splits that pair on admin.html alone. */
.league-type-pill {
    display: inline-block;
    padding: 0.2em 0.85em;
    font-size: 1em;
    font-weight: 600;
    border-radius: var(--radius-full);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    line-height: 1.5;
}
.league-type-pill.type-doubling {
    background: var(--lt-doubling-bg);
    color: var(--lt-doubling-text);
}
.league-type-pill.type-regular {
    background: var(--lt-regular-bg);
    color: var(--lt-regular-text);
}
.league-type-pill.type-ubc {
    background: var(--lt-ubc-bg);
    color: var(--lt-ubc-text);
}
.league-type-pill.type-all {
    background: var(--lt-all-bg);
    color: var(--lt-all-text);
}

/* ---- Status pill (mirrors components.css for admin page) ---- */
/* `1em` — twin of .league-type-pill above; see components.css. */
.status-pill {
    display: inline-block;
    padding: 0.2em 0.85em;
    border-radius: var(--radius-full);
    font-size: 1em;
    font-weight: 600;
    line-height: 1.5;
    letter-spacing: 0.02em;
}
.status-running {
    background: var(--color-running-bg);
    color: var(--color-running);
}
.status-completed {
    background: var(--color-completed-bg);
    color: var(--color-completed);
}
/* A third status, admin-only: the league exists as queued work and nowhere
   else. Deliberately NOT red — it is a draft waiting to be published, not a
   failure, and it used to render as a red "Failed to load" saying the exact
   opposite. Amber matches the preview banner's "not yet published" signal.
   Lives here rather than in components.css because admin.html does not load
   that file — it carries its own copy of the status-pill set, right above. */
.status-pending {
    background: var(--color-pending-bg);
    color: var(--color-pending);
}
/* The row itself stays legible as a draft while scrolling past the published
   ones, without shouting: a hairline amber rail on the leading edge only. */
.league-row-pending > td:first-child {
    box-shadow: inset 3px 0 0 var(--color-pending);
}

/* ---- Global hover tooltip (mirrors components.css for the admin page, which
   doesn't load components.css). Keep in sync with the canonical rule there. ---- */
.app-tooltip {
    position: fixed;
    z-index: 1100;
    background: var(--popover-bg);
    color: var(--color-text);
    border: 1px solid var(--popover-border-color);
    border-radius: var(--popover-radius);
    box-shadow: var(--popover-shadow);
    font-size: var(--fs-085);
    padding: 0.4em 0.7em;
    line-height: 1.4;
    /* Kept in sync with the copy in components.css. */
    max-width: min(88vw, 30em);
    white-space: normal;
    overflow-wrap: anywhere;
    pointer-events: none;
    opacity: 0;
    transform: translateY(-2px);
    transition: opacity 0.12s ease, transform 0.12s ease;
}
.app-tooltip--visible {
    opacity: 1;
    transform: translateY(0);
}

/* ---- F8 / F9 — Mail-sourced matches (Sync page) --------------------------
   Two FF tables fed by sql/mail_sync.sql: F8 "Unassigned Email Matches"
   (league picker + Apply/Discard) and F9 "Mail Automated Matches" (the log).
   Rendered by js/admin/mailSync.js on the hand-built FF chrome like every
   other admin table pending Phase 8.                                       */

/* FF Edit-cell <select>. The canon in table-lab/formats/ff/ff.css had never
   been mirrored here, because until F8 the only FF select was F3's score cell
   with its own sizing block. Two canons compose, and neither is re-invented:

     • ff.css §"FF Edit-cell <select> sizing" — the em-based gutter. A px
       gutter on an em-width box is exactly what hid F3's digit behind the
       arrow at mobile font scale (2026-06-03). Values copied verbatim.
     • .form-group select above — the admin's token-based skin (border,
       colour, surface, focus ring). Same tokens, same focus treatment, so a
       picker inside a table doesn't look like a different control family
       from every other select on the page.

   Deliberately NO `appearance: none` + custom arrow: the canon keeps the
   native arrow, which the UA already paints to the OS/theme. A drawn arrow
   means a hardcoded stroke colour that ignores every theme — an earlier
   draft of this block shipped `stroke='%23888'` and would have rendered a
   grey arrow on vegas' deep red and on dark.

   The header filter sits outside .ff-wrap and shares the chrome so it
   doesn't read as a stray control next to the in-table pickers. */
.ff-wrap select,
.mail-league-filter {
    padding: 0.15em 1.4em 0.15em 0.45em;
    border-radius: 0.2em;
    font-family: inherit;
    background-position: right 0.4em center;
    background-size: 0.6em 0.4em;
    min-width: 3.5em;
    max-width: 16em;
    border: 1px solid var(--color-border);
    color: var(--color-text);
    background-color: var(--color-surface);
    transition: border-color 0.15s;
}
.ff-wrap select:focus,
.mail-league-filter:focus {
    outline: none;
    border-color: var(--color-accent);
    box-shadow: 0 0 0 3px var(--color-accent-light);
}

/* A row whose league has been picked but not yet applied reads as "pending",
   reusing F3's tint so the admin speaks one colour language. The
   `td:first-child` half is NOT redundant: FF paints the sticky first column
   at (0,2,3), and a specificity tie loses on source order — the same trap
   that left F3's match-block sticky cell un-tinted (see TABLE-DESIGN, F3
   match-block state tint). */
.admin-table.font-large[data-mf-table-id="F8"] tbody tr.is-resolved td,
.admin-table.font-large[data-mf-table-id="F8"] tbody tr.is-resolved td:first-child,
.admin-table.font-large[data-mf-table-id="F8"] tbody tr.is-resolved td:nth-child(2) {
    background: var(--color-warning-bg);
}

/* Winner green / loser red. B5's treatment lives in css/dashboard.css scoped
   to .rounds-scroll-wrap, so it cannot reach an FF table; re-scoped here. */
.ff-wrap td.player-cell.result-win  { color: var(--color-win); }
.ff-wrap td.player-cell.result-loss { color: var(--color-loss); }

.ff-wrap .mail-len { color: var(--color-text-muted); }
/* The per-row reason a report matched no league (sql/mail_sync.sql
   mail_orphan_reason). This is the ONE FF cell that is allowed to wrap: FF is
   `white-space: nowrap` throughout.

   The reasons are now two to four words ("Different leagues", "Length mismatch
   (7 vs 5)") and fit on one line unaided — the wrap and the cap are kept for
   the one case that can still run long, "Unknown player: <name>", since a
   player name has no length limit. max-width is a CAP, not a width, so short
   reasons are unaffected by it. */
.ff-wrap .mail-no-cand {
    display: inline-block;
    max-width: 18em;
    white-space: normal;
    line-height: 1.35;
    color: var(--color-text-muted);
    font-size: 0.85em;
}

/* Provenance pill — AUTO (resolver placed it) vs ADMIN (a human picked the
   league). Same shape family as .league-type-pill. */
.mail-src-pill {
    display: inline-block;
    padding: 0.1em 0.5em;
    border-radius: 0.8em;
    font-size: 0.78em;
    font-weight: 700;
    letter-spacing: 0.02em;
    white-space: nowrap;
    background: var(--color-accent-light);
    color: var(--color-accent);
}
.mail-src-pill.is-admin {
    background: var(--color-warning-bg);
    color: var(--color-text);
}

/* Health strip. An automation running in someone else's mailbox dies without
   telling anyone, and silence is indistinguishable from "no matches were
   played" — this line is the only thing that separates them. */
/* ---- Mail health strip ---------------------------------------------------
   Was a single flex row of "· <b>N</b> label ·" text. Flex makes one anonymous
   item out of a bare text run, so the row wrapped mid-phrase and split every
   figure from the label it belonged to — "14" ending a line and "applied
   automatically" beginning the next. Reading it meant reassembling it.

   Now: a lead line, then a GRID of atomic figure/label pairs. auto-fit lays
   them out four-across when there is room and two-across on a phone, and no
   pair can ever be broken up because each one is its own grid cell. */
.mail-health {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-sm) var(--space-md);
    margin-bottom: var(--space-md);
    font-size: 0.9em;
    color: var(--color-text-muted);
}
.mail-health b { color: var(--color-text); }

.mail-health-lead {
    display: flex;
    align-items: center;
    gap: 0.45em;
    margin: 0;
    white-space: nowrap;
}

.mail-stats {
    display: grid;
    /* An explicit column count, NOT auto-fit. auto-fit derives the count from
       leftover width, and the leftover here is whatever the lead line and the
       league filter have not taken — which lands on three columns and orphans
       the fourth stat on a line of its own, at both phone and desktop widths.
       Four stats want 2x2 or 1x4; three-and-one is the one arrangement that
       reads as broken, and it is exactly what "fit as many as will fit" picks. */
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-xs) var(--space-md);
    margin: 0;
    /* Takes the rest of the row on desktop so the league filter sits at the far
       end; on a phone it is the whole width and the filter drops below it. */
    flex: 1 1 16rem;
}
.mail-stat {
    display: flex;
    flex-direction: column;
    gap: 0.1em;
    min-width: 0;
}
.mail-stat dt {
    font-weight: 600;
    font-size: 1.15em;
    line-height: 1.1;
    color: var(--color-text);
    font-variant-numeric: tabular-nums;
}
.mail-stat dd {
    margin: 0;
    font-size: 0.85em;
    line-height: 1.2;
    /* The labels are single words or hyphenated pairs, so a cell never has to
       wrap; nowrap keeps a narrow column from breaking "By admin" in half. */
    white-space: nowrap;
}

/* 768px is layout.css's existing phone/desktop line — reused rather than
   introducing a fifth breakpoint for one strip. Above it the whole strip fits
   on one line: lead, four stats, filter. */
@media (min-width: 769px) {
    .mail-stats { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
.mail-dot {
    width: 0.6em;
    height: 0.6em;
    border-radius: 50%;
    display: inline-block;
}
.mail-dot.is-live  { background: var(--color-win); }
.mail-dot.is-stale { background: var(--color-warning); }

/* ---- Show-all button under F9 — layout glue only -------------------------
   The button itself is canonical and already styled: admin.html loads
   table-lab/formats/mf/mf.css, which carries `.table-row-hidden` and imports
   base.css for `.show-more-btn`. Nothing about the button is redefined here.

   These two rules are the FF twin of mf.css's `.mf-wrap:has(+ .show-more-btn)`
   pair — collapse the wrap's bottom margin so the button sits flush against
   the table it belongs to, and centre it when the wrap's parent is a flex
   container. */
.ff-wrap:has(+ .show-more-btn) {
    margin-bottom: 0;
}
.ff-wrap + .show-more-btn {
    align-self: center;
}

/* ---- FF sticky columns 2/3 — mirror of table-lab/formats/ff/ff.css --------
   FF's canon pins exactly one left column (`th/td:first-child`, no class).
   `.ff-sticky-2` on the TABLE pins the second one too, for tables whose
   subject is a PAIR — F8/F9, where a pinned Player A alone leaves "7 - 4"
   against one name, i.e. half a result.

   Naming, class placement and the measured var all follow SF, which solved
   this first (`sf-sticky-N` + `--sf-col1-w`, table-lab/formats/sf/sf.css) —
   the format family already treats sticky-column count as a parameter, so
   FF gets the sibling's spelling rather than a second vocabulary.

   Canon source: table-lab/formats/ff/ff.css. This block is the production
   mirror, like every other FF rule here, until the Phase 8 rewire. */
.admin-table.font-large.ff-sticky-2 thead th:nth-child(2) {
    position: sticky;
    left: var(--ff-col1-w, 9em);
    z-index: 4;
    background: var(--color-bg);
}
.admin-table.font-large.ff-sticky-2 tbody td:nth-child(2) {
    position: sticky;
    left: var(--ff-col1-w, 9em);
    z-index: 2;
    background: var(--color-inset);
}
.admin-table.font-large.ff-sticky-2 tbody tr:hover td:nth-child(2) {
    background: var(--color-hover);
}

/* The scroll shadow marks the boundary between pinned and scrolling content,
   so it moves to the LAST pinned column. Cancelling it on col 1 is the whole
   point — two adjacent shadows would draw a seam between two columns that are
   pinned together. */
.ff-wrap.is-scrolled-x .admin-table.font-large.ff-sticky-2 thead th:first-child,
.ff-wrap.is-scrolled-x .admin-table.font-large.ff-sticky-2 tbody td:first-child {
    box-shadow: inset 0 -1px 0 var(--color-border);
}
.ff-wrap.is-scrolled-x .admin-table.font-large.ff-sticky-2 tbody tr:last-child td:first-child {
    box-shadow: none;
}
.ff-wrap.is-scrolled-x .admin-table.font-large.ff-sticky-2 thead th:nth-child(2),
.ff-wrap.is-scrolled-x .admin-table.font-large.ff-sticky-2 tbody td:nth-child(2) {
    box-shadow:
        inset 0 -1px 0 var(--color-border),
        6px 0 8px -4px rgba(0, 0, 0, 0.18);
}
.ff-wrap.is-scrolled-x .admin-table.font-large.ff-sticky-2 tbody tr:last-child td:nth-child(2) {
    box-shadow: 6px 0 8px -4px rgba(0, 0, 0, 0.18);
}
