:root{
  --bg:#f4f7f2; --card:#fff; --text:#18331f;
  /* Darkened from #6e7d72 (~4.34:1 on white, just under WCAG AA's 4.5:1 minimum for normal
     text) to ~5.6:1 — this color carries genuinely useful info (slot times, waitlist position,
     level), not just decoration, so it needs to hold up in bright/outdoor phone use. */
  --muted:#5c6b60;
  --accent:#197844; --accent2:#e2f5e8; --line:#dce7df; --danger:#9f3434;
  --accent3:#1a5fb4; --accent3-bg:#e3ecfa;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,sans-serif}
.wrap{max-width:1220px;margin:auto;padding:22px}
/* Smallest phones get tighter side margins to reclaim content width — larger screens (and the
   layout's own max-width) are unaffected. */
@media(max-width:480px){.wrap{padding:16px}}

/* Mobile-only sticky Save bar (players.html) — see updateMobileSaveBar() in the page script.
   Rendered as a sibling of .wrap, not inside it, so it's never subject to any ancestor's
   overflow:hidden (details.card sets one) and position:fixed always anchors to the viewport. */
.mobileSaveBar{
  display:flex; gap:8px; position:fixed; left:0; right:0; bottom:0; z-index:500;
  background:var(--bg); border-top:1px solid var(--line); padding:12px 16px;
  box-shadow:0 -6px 16px rgba(0,0,0,.08);
}
.mobileSaveBar button{flex:1}
/* Reserves room at the bottom of the page so the fixed bar never covers the last bit of
   scrollable content underneath it — toggled alongside .mobileSaveBar's own visibility. */
body.hasMobileSaveBar{padding-bottom:76px}
@media(min-width:681px){
  /* Desktop/tablet never shows the mobile bar (see the JS media-query check too — this is a
     belt-and-braces CSS backstop) and keeps the original inline Save/Cancel row. */
  .mobileSaveBar{display:none!important}
}
@media(max-width:680px){
  /* The fixed bar replaces this row one-for-one on narrow viewports, rather than showing both. */
  #desktopSaveActions{display:none}
}
/* padding-right permanently reserves a strip for the absolutely-positioned .inlineLogo (below) —
   so .headerRight's own buttons never lay out underneath it, in EITHER desktop's side-by-side
   arrangement or mobile's wrapped-onto-its-own-row one (max-width:680px block below). */
header{position:relative;display:flex;justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:18px;padding-right:128px}
/* .headerRight has flex-shrink:0 (never gives up width) and .headerLeft has no min-width, so on
   a narrow phone .headerRight's badges/buttons squeezed .headerLeft down to almost nothing —
   the .sub description sentence then wrapped across 6-7 lines in that sliver of space, worst on
   admin.html now that .headerRight holds three items (admin name badge, Log out, Help) instead
   of two. Wrapping header onto two rows below this width lets .headerLeft use the full row
   width for its text instead of sharing it with .headerRight. */
@media(max-width:680px){
  header{flex-wrap:wrap}
  .headerRight{width:100%;justify-content:flex-end}
}
/* players.html/admin.html — pinned to the header's own top-right corner via position:absolute
   (header{position:relative} above), deliberately OUTSIDE .headerRight's flex flow — .headerRight
   wraps onto its own row below .headerLeft on a narrow phone (see the max-width:680px block
   below), and the logo needs to stay put in the actual top-right corner regardless, not wrap down
   into the middle of the page along with Log out/Help. */
.inlineLogo{position:absolute;top:0;right:0;width:112px;height:112px}
h1{margin:0;font-size:clamp(28px,5vw,44px);letter-spacing:-.04em}
.sub{margin-top:6px;color:var(--muted);max-width:820px}
.badge{background:var(--accent2);color:var(--accent);padding:8px 12px;border-radius:999px;font-size:12px;font-weight:800}
.layout{display:grid;grid-template-columns:1fr 1fr;gap:18px}
@media(max-width:900px){.layout{grid-template-columns:1fr}}
.card{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:18px;box-shadow:0 8px 28px rgba(30,60,35,.05)}
h2{margin:0 0 12px;font-size:19px}
label{display:block;font-size:12px;font-weight:800;margin:10px 0 5px}
/* A label paired with a small inline action (e.g. the attendance form's "How is this ordered?"
   link) — sits on the label's own line, so it reads as attached to that specific field rather
   than a separate control floating elsewhere in the form. */
.labelRow{display:flex;align-items:baseline;justify-content:space-between;gap:8px}
.labelRow label{margin:10px 0 5px}
.helpLink{background:none;border:0;padding:0;margin:10px 0 5px;color:var(--accent);font-size:12px;font-weight:800;text-decoration:underline;cursor:pointer}
input,select{width:100%;padding:10px 11px;border:1px solid var(--line);border-radius:10px;background:white;color:var(--text);font:inherit}
/* A checkbox has a small, fixed native size — inheriting width:100% from the rule above stretches
   its (invisible but still layout-affecting) box to fill the whole row, which is what pushed a
   checkbox+label pairing's text out into a cramped remaining sliver and made it wrap awkwardly. */
input[type=checkbox]{width:auto}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
@media(max-width:560px){.grid2{grid-template-columns:1fr}}
/* One card per row at every width — voting tiles, Current Votes, Match sessions, and Match
   history (both pages) plus Session setup's own slot cards all need room to show their full
   details (court badges, booker names, roster) without cramming into 2-3 tight columns. */
.session-grid{display:grid;grid-template-columns:1fr;gap:8px;margin-top:12px}
.session-choice{border:1px solid var(--line);border-radius:12px;padding:10px;background:#fafcfa}
.session-choice .start{font-size:14px;font-weight:850}
.session-choice .end{font-size:11px;color:var(--muted);margin:2px 0 0}
/* Player slot tile (players.html) — time on the left, a court-type ID/OD badge on the right when
   the slot has one (see slotBuckets.js). Seats-remaining was dropped — capacity was always just
   informational, never a hard block, and wasn't worth the clutter. */
.slotHead{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:9px}
/* Session setup (admin.html) — a single-court group's Publish/Unpublish button sits right after
   the time, inside .slotHead's first child rather than as its own sibling, so .slotHead itself
   still only ever has its usual 2 flex children (this + .courtBadges) and courtBadges stays
   pinned to the far right exactly as before. */
.slotHeadTimeRow{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
/* Settings → Facilities — each facility's fixed Indoor 1-8 / Outdoor 1-8 checkboxes (see
   admin.html's courtCheckboxesHtml), always laid out in numerical order. Small fixed-width tiles
   rather than a full-width row per number, since the content is just a single digit. */
.courtCheckGrid{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px}
.courtCheck{
  display:flex;align-items:center;justify-content:center;gap:6px;width:44px;height:36px;
  border:1px solid var(--line);border-radius:10px;background:#fafcfa;cursor:pointer;font-weight:800;
}
.courtCheck:has(input:checked){background:var(--accent2);border-color:#bfe3c9;color:var(--accent)}
.courtCheck input{width:auto;margin:0}
/* Separates a facility's Tennis courts from its Padel courts (admin.html's renderFacilityCard) —
   a plain top border + extra spacing rather than a full card-within-a-card, since it's really
   just a second heading-plus-grid pair, not a separate nested component. */
.sportSectionHeading{margin-top:18px;padding-top:14px;border-top:1px solid var(--line)}
.facilityCard .sportSectionHeading:first-of-type{margin-top:14px;padding-top:0;border-top:none}
/* Still used for the facility picker's "N selected" count (renderFacilityPicker) — a plain pill,
   same treatment as .countBadge elsewhere. */
.seatsBadge{
  display:inline-block;font-size:13px;font-weight:800;border-radius:999px;padding:5px 11px;
  white-space:nowrap;background:var(--accent2);color:var(--accent);border:1px solid #bfe3c9;
}
.courtTypeBadge{
  display:inline-block;font-size:11px;font-weight:800;border-radius:999px;padding:5px 9px;
  white-space:nowrap;background:var(--bg);color:var(--muted);border:1px solid var(--line);
}
/* Indoor reads as purple, outdoor as clay-court terracotta — a real-world color cue on top of
   the text itself, so the two read apart at a glance, not just by which word is printed. */
.courtTypeBadge.indoor{background:#f1e8fb;color:#7440ad;border-color:#ddc4f2}
.courtTypeBadge.outdoor{background:#fbe8dd;color:#b5501e;border-color:#f0c7a8}
/* Settings → Facilities' "Indoor courts"/"Outdoor courts" section headers reuse the same colour
   badges rather than a plain text label, so the purple/clay cue carries all the way from a
   match's court badges down to the checkbox grid that sets them up. Needs its own block-level
   sizing since a plain .courtTypeBadge is meant to sit inline next to other badges, not head up
   a whole section on its own line. */
.courtTypeHeading{display:block;width:fit-content;margin:14px 0 8px;font-size:12px}
/* A slot/match with more than one specific court shows one badge per court (e.g. "ID 3" then
   "ID 7"), not one deduped badge per type. Without a wrapper, each badge is its own direct child
   of .slotHead's flex row — .slotHead's justify-content:space-between would then spread them
   ACROSS the whole row (one badge pinned far right, another stranded in the middle) instead of
   clustering them together. This wrapper makes the whole badge set act as ONE flex item on the
   right, and stays right-aligned + wraps together as a unit if there are enough courts to need a
   second line — never left-aligned wrapping. */
.courtBadges{display:flex;flex-wrap:wrap;gap:6px;justify-content:flex-end;min-width:0}
/* Who's already signed up for THIS slot, right on its own tile — same font treatment as the
   rest of the tile's secondary text (was .whosInNames, before the separate "Who's signed up"
   accordion below the grid got folded into each tile instead). */
.slotSignups{font-size:13.5px;color:var(--text);overflow-wrap:break-word;margin:6px 0}
.slotSignups.slotSignupsEmpty{color:var(--muted);font-style:italic}
/* Facility/venue name on a match card — bolder than the plain booked-by/time/roster lines around
   it so it reads as the card's own "where" at a glance, not just another line in the list. */
.venueLine{font-weight:800}
/* Separates the venue/court/booker details from the player roster within one .slotSignups block
   (players.html's match session/history cards) — two different kinds of information, not just
   more lines in the same list. */
.slotSignupsDivider{border-top:1px solid var(--line);margin:6px 0}
.slot-btn{
  width:100%; border:1px solid var(--line); background:#fff; color:var(--text);
  border-radius:10px; padding:10px; font-weight:800; cursor:pointer; margin-top:14px;
}
.slot-btn.active{background:var(--accent2);color:var(--accent);border-color:#bfe3c9}
.slot-btn.disabled{background:#f1f3f1;color:#a0aaa2;cursor:not-allowed}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}
/* Admins list (admin.html renderAdminsList) — role <select> + Deactivate/Regenerate/Remove
   buttons on one row. Same shape as .actions, but the select needs its own width:auto override:
   the global input,select{width:100%} rule (meant for vertical form fields) would otherwise make
   it claim the whole row's width as its flex-basis and shove every button onto a line by itself
   below it, the same overflow the .courtRow/.timePickerRow selects elsewhere already needed fixing. */
.adminActions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:9px}
.adminActions select.adminRoleSelect{width:auto;min-width:0}
/* Admins list name cell — the "You" pill and, on your own row, an inline "Regenerate my code"
   button sit to the right of the name itself (replacing a separate button above the whole list,
   which read as disconnected from whichever row was actually yours). display:flex+wrap keeps
   them vertically centered with the name instead of the plain .start block's baseline text flow. */
.adminNameCell{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
/* Download/Upload roster (admin.html Players tab) — kept on one row even on narrow phones
   (each button sharing the row equally) instead of the default .actions wrap stacking them. */
@media(max-width:680px){
  .csvActions{flex-wrap:nowrap}
  .csvActions button{flex:1;min-width:0;padding-left:8px;padding-right:8px;font-size:12.5px}
}

/* Player roster pagination (admin.html) — Previous/Next pushed to opposite edges, page count
   landing in the middle, instead of the default .actions left-packed row. */
.pagination{display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;margin-top:14px}
/* Keeps First+Previous clustered on the left and Next+Last clustered on the right as one flex
   child each, so .pagination's own space-between still reads as [left cluster, page label, right
   cluster] instead of spreading all 5 controls evenly across the row. */
.pageJumpGroup{display:flex;align-items:center;gap:6px}
button{border:0;border-radius:11px;padding:11px 14px;font:inherit;font-weight:800;cursor:pointer}
/* Immediate visual feedback on tap, before any network round-trip completes — without it, a
   button can feel unresponsive for a beat on a slow connection. Covers every real <button> in
   the app (almost everything tappable is one), so no per-component work needed. */
button:active{transform:scale(.97)}
/* Small inline actions (Regenerate a login code, Swap a match player) — still compact enough to
   sit inline with text, but closer to a comfortable tap target than the original 2px-padding
   version, which measured well under the ~44px touch-target guideline. */
.btnSmall{padding:7px 11px;font-size:12px;min-height:34px}
button:disabled{opacity:.5;cursor:not-allowed}
.dateDisplay{
  width:100%;padding:10px 11px;border:1px solid var(--line);border-radius:10px;
  background:#f1f3f1;color:var(--text);font-weight:800;
}
/* A subtle shadow gives the actual primary action a slight "raised, tappable" quality that flat
   informational pills (badges, chips, status pills) never have — the app leans on one accent
   hue for almost everything, so shape/elevation carries more of the "this is the button, not
   just a fact" signal than colour alone can. */
.primary{background:var(--accent);color:#fff;box-shadow:0 3px 8px rgba(25,120,68,.28)}
.secondary{background:var(--accent2);color:var(--accent)}
.ghost{background:#f7f9f7;border:1px solid var(--line)}
/* Players' "← All facilities" back button (only shown when there's more than one facility to
   choose from) — full-width and a size up from a normal button so it reads as a clear step back,
   not a small inline link, with real breathing room before the slot cards below it. */
.allFacilitiesBtn{width:100%;padding:14px 16px;font-size:15px;margin:4px 0 16px}
.danger{background:#fff0f0;color:var(--danger)}
/* Blue, not the app's usual green — Share sits right next to Publish (green .primary) inside a
   .facilityCard whose own background IS var(--accent2), the same colour .secondary uses, so a
   .secondary Share button used to nearly vanish against the card. */
.info{background:var(--accent3-bg);color:var(--accent3)}
/* Player roster row (admin.html) — each player is its own distinct bordered card (not just a
   divider between rows), so a long roster reads as a clear stack of separate entries rather
   than one continuous list. Three internal rows, none of which wrap: name+level, code+
   regenerate (.playerCodeRow), then booker+edit+remove+linked (.playerMeta) — each row scrolls
   horizontally on its own if it's ever too wide for the screen, rather than breaking onto a
   second line, so the card's height stays predictable and the row's items stay lined up.
   -ms-overflow-style/scrollbar-width hide the scrollbar itself; it still scrolls fine with a
   swipe, it just doesn't need to look like a scroll area for what's normally a single tap away
   from fitting anyway. */
.player{border:1px solid var(--line);border-radius:12px;padding:12px;margin-top:10px;background:#fafcfa}
.player:first-child{margin-top:0}
.playerTopRow{display:flex;align-items:center;gap:10px;flex-wrap:nowrap}
.playerTopRow .name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.playerTopRow .levelPill{flex-shrink:0}
/* Roster row's name swaps to this while renaming (see startEditPlayerName) — two inputs sharing
   the row, each with a sane minimum before wrapping to its own line on a narrow phone, instead of
   either overflowing or the row growing wider than its siblings (Booker/Remove) expect. */
.nameEditRow{display:flex;gap:6px;flex-wrap:wrap}
.nameEditRow input{width:auto;flex:1 1 100px;min-width:0}
.playerCodeRow,.playerMeta{
  display:flex;align-items:center;gap:8px;flex-wrap:nowrap;margin-top:9px;
  overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none;
}
.playerCodeRow::-webkit-scrollbar,.playerMeta::-webkit-scrollbar{display:none}
.playerCodeRow>*,.playerMeta>*{flex-shrink:0}
.metaPill{display:inline-block;font-size:11px;font-weight:800;color:var(--muted);background:var(--bg);border:1px solid var(--line);border-radius:999px;padding:4px 10px}
/* Find-player status pill (admin.html Add Player form) — "New player" uses the plain .metaPill
   look above; picking a cross-group match swaps to this accent variant so a Linked outcome is
   visually distinct from a fresh one at a glance. */
.metaPill.linked{color:var(--accent);background:var(--accent2);border-color:#bfe3c9}
/* The roster's Level pill embeds a live <select> for the number itself (see setPlayerLevel) —
   stripped back to plain inherited text so it reads as part of the pill, not a bolted-on form
   control, overriding the global input,select rule's own width/padding/border/background. */
.levelPill{padding-right:6px}
.levelSelect{width:auto;min-width:0;padding:0;margin:0 0 0 2px;border:0;background:transparent;color:inherit;font:inherit;font-weight:inherit;cursor:pointer}
.matchHead{display:flex;justify-content:space-between;gap:10px;align-items:flex-start;flex-wrap:wrap}
/* Defensive against a genuinely long facility/player name forcing horizontal overflow of its
   row/card — everything here wraps naturally already, this just stops an unbroken long word
   (rather than a normal multi-word name) from being the one exception. */
.name,.matchTitle{font-weight:850;overflow-wrap:break-word}
.meta,.note{font-size:12px;color:var(--muted)}
.slots{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}
.slot{font-size:12px;border:1px solid #bfe3c9;background:var(--accent2);color:var(--accent);border-radius:999px;padding:5px 8px}
.session{border:1px solid var(--line);background:#fbfdfb;border-radius:14px;padding:14px;margin-top:10px}
.courtRow{display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap}
.courtRow label{margin:0;white-space:nowrap}
.courtRow select{width:auto;max-width:100%}
.courtRow input{width:auto;max-width:100%}
.printOnly{display:none}
.teamgrid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px}
@media(max-width:560px){.teamgrid{grid-template-columns:1fr}}
.person{border:1px solid var(--line);background:#fff;border-radius:10px;padding:9px}
.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:14px}
.stat{background:#f7faf7;border:1px solid var(--line);border-radius:12px;padding:10px}
.stat strong{display:block;font-size:20px}.stat span{font-size:11px;color:var(--muted)}
.empty{font-size:14px;color:var(--muted);padding:10px 0}
.notice{margin-top:10px;font-size:12px;color:var(--muted)}
.admin{border:1px dashed #b9c9bc;background:#f9fcf9}
footer{text-align:center;color:var(--muted);font-size:12px;margin-top:18px}
.footNote{margin-top:6px}
.footNote a{color:inherit;text-decoration:underline;text-decoration-color:var(--line)}
.footNote a:hover{text-decoration-color:currentColor}

.toast{
  position:fixed;left:50%;bottom:24px;transform:translateX(-50%) translateY(20px);
  background:var(--text);color:#fff;padding:12px 18px;border-radius:999px;
  font-weight:800;font-size:14px;box-shadow:0 10px 30px rgba(0,0,0,.18);
  opacity:0;pointer-events:none;transition:opacity .25s ease, transform .25s ease;
  z-index:1000;max-width:90vw;text-align:center;
}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}

/* Signup status (admin.html session form, per date) */
.closeCardHead{display:flex;align-items:center;justify-content:space-between;gap:8px}
.closeCardHead label{margin:0}
.closeCard .courtRow{margin-top:8px}
.closeCard .courtRow input{flex:1;min-width:120px}
.statusRow{display:flex;align-items:center;gap:8px;margin-top:12px;flex-wrap:wrap}
.statusBadge{padding:6px 12px;border-radius:999px;font-size:12px;font-weight:800}
/* Inline next to the session date in the read-only saved-picks summary (players.html) — a
   little breathing room since it now sits right after the date text, not in its own column. */
.name .statusBadge{margin-left:8px;vertical-align:middle}
.statusBadge.active{background:var(--accent2);color:var(--accent)}
.statusBadge.closed{background:#fbe9e9;color:var(--danger)}
/* Neutral, not red — a draft (unpublished) match isn't an error state, just not confirmed yet. */
.statusBadge.pending{background:#f1f3f1;color:var(--muted)}
@media(max-width:680px){
  .closeCard .courtRow{flex-wrap:nowrap}
  .closeCard .courtRow label{flex-shrink:0}
  .closeCard .courtRow input{min-width:0}
}

/* Shared "info card" look (players.html: availability, matches, history) — a left accent
   stripe rather than a full tint, so it reads as a distinct, confirmed item without competing
   with any also-green badge inside it. .muted swaps the stripe to gray for a closed/past-tense
   card (a closed availability window, a historical result) rather than an active one. */
.infoCard{background:var(--card);border:1px solid var(--line);border-left:4px solid var(--accent);border-radius:14px;padding:16px;margin-top:12px}
.infoCard:first-child{margin-top:0}
.infoCard.muted{border-left-color:var(--muted)}
.infoCardHead{display:flex;justify-content:space-between;align-items:flex-start;gap:14px;flex-wrap:wrap}
.infoCardStatus{display:flex;flex-direction:column;align-items:flex-end;gap:6px;text-align:right}
.infoCard .actions{margin-top:14px}

/* Multi-Round's derived-summary card (admin.html's populateRoundSessionBuilderDefaults) packs
   three separate facts — roster, courts, timing — into one card; a divider between each keeps
   them from reading as one run-on paragraph without going back to three separately-bordered
   cards stacked on top of each other. */
.rsSummarySection+.rsSummarySection{margin-top:10px;padding-top:10px;border-top:1px solid var(--line)}

/* Small pill facts on the availability card (signup timestamp, how many games requested) —
   shared styling so both read as distinct, prominent facts rather than plain muted text. */
.countBadge{display:inline-block;font-size:13px;font-weight:800;border:1px solid var(--line);border-radius:999px;padding:5px 11px;margin-top:9px;color:var(--text);background:var(--bg)}
/* Groups the saved-picks summary's small facts (signed-up time, how many, court preference) into
   one row instead of a stack of separate lines — see renderAvailabilitySummaryCard. */
.prefRow{display:flex;gap:6px;flex-wrap:wrap}

/* Sessions list (admin.html) — one row per configured date, with quick actions. Status/publish
   badges are grouped into their own flex item (.sessionRowBadges) rather than sitting as two
   independent children of the space-between row — two badges wrapping as separate items each
   landed at a different left offset depending on how much else had already wrapped onto that
   line, which is what made "Active"/"Closed" look randomly indented row to row on narrow screens.
   Keeping them together means they always wrap as a pair and always left-align as a pair. */
.sessionRow{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;border-top:1px solid var(--line);padding:13px 0}
.sessionRow:first-child{border-top:0}
.sessionRow .actions{margin-top:0}
.sessionRowCheck{flex:0 0 auto;width:18px;height:18px;accent-color:var(--accent)}
.sessionRowMain{flex:1 1 200px;min-width:0}
.sessionRowBadges{display:flex;gap:8px;flex-wrap:wrap;flex:0 0 auto}

/* Bulk actions bar (Sessions list) — appears above the list only once at least one date is
   ticked, same "hidden until something's selected" treatment as the Facilities/Courts
   multi-select delete buttons. */
.sessionsBulkBar{display:flex;align-items:center;gap:10px;flex-wrap:wrap;background:var(--accent2);border-radius:10px;padding:12px 14px;margin:6px 0 16px}
.bulkBarCount{font-weight:800;margin-right:6px}

/* Create/edit session form — visually set apart from the list above it, same dashed
   treatment as the surrounding admin card. */
.sessionForm{border:1px dashed #b9c9bc;background:#f9fcf9;border-radius:14px;padding:14px;margin-top:14px}

/* Checkbox variant of .session-choice — the whole tile is a <label> so tapping anywhere on
   it (not just the tiny checkbox) toggles it, and a ticked tile gets the same highlight an
   active .slot-btn gets on players.html. */
.slotCheck{display:block;cursor:pointer}
.slotCheck .checkRow{display:flex;align-items:center;gap:6px;margin-top:6px}
.slotCheck .checkRow input{width:auto}
.slotCheck:has(input:checked){background:var(--accent2);border-color:#bfe3c9}

/* Add-slot mini-form (admin.html session form) — a labeled field grid set apart from the
   dashed .sessionForm container around it, so it reads as its own input group rather than a
   cramped single row. */
.addSlotForm{border:1px solid var(--line);background:#fff;border-radius:14px;padding:14px;margin-top:12px}
/* Collapsed by default: just a centered "+ Add slot" opener, so a date with several slots
   already added doesn't permanently dedicate a big field-grid's worth of space to adding one
   more. Expanding reveals the real form; a successful add (or Cancel) collapses it back. */
.addSlotForm .addSlotOpenRow{display:flex;justify-content:center;padding:4px 0}
.addSlotForm .addSlotFormBody{display:none}
.addSlotForm.expanded .addSlotOpenRow{display:none}
.addSlotForm.expanded .addSlotFormBody{display:block}
.addSlotForm .fieldGrid,.facilityCardBody .fieldGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px}
/* .facilityCardBody's own "Court name"/"Type" add-court fields sit directly under the courts grid
   above them (courtsHtml) with no gap otherwise — .session-grid only has margin-top, not
   margin-bottom, so the two ran together with no visible separation. Scoped to just this context
   (not .addSlotForm's own fieldGrid, which has nothing above it to separate from). */
.facilityCardBody .fieldGrid{margin-top:16px}
/* Grid items default to min-width:auto, which lets a field's *content* (a native time input's
   own intrinsic rendering width, on some mobile browsers) force it wider than its track instead
   of shrinking to fit — the classic cause of two fields visually overlapping in a CSS grid.
   min-width:0 lets the track's own sizing win instead. Single column below 480px removes any
   side-by-side squeeze at all on the smallest phones. */
.addSlotForm .fieldGrid>div,.facilityCardBody .fieldGrid>div{min-width:0}
.addSlotForm .fieldGrid input,.addSlotForm .fieldGrid select,.facilityCardBody .fieldGrid input,.facilityCardBody .fieldGrid select{min-width:0}
@media(max-width:480px){.addSlotForm .fieldGrid,.facilityCardBody .fieldGrid{grid-template-columns:1fr}}
.addSlotForm .fieldGrid>div label,.facilityCardBody .fieldGrid>div label{margin:0 0 5px}
.addSlotForm .actions,.facilityCardBody .actions{margin-top:12px;gap:8px}
/* Publish/Share/More (grouped into one native <select> each — see .actionSelect below) + Add
   match: smaller/tighter than normal buttons so they read as one compact row on most phones, but
   genuinely wraps onto a second line rather than forcing a horizontal scrollbar on the narrowest
   ones — a scrollbar on a 4-item button row is easy to miss entirely, wrapping never is. */
.dateActions{flex-wrap:wrap;gap:6px}
.dateActions>button,.dateActions>select{padding:6px 8px;font-size:12px;white-space:nowrap;flex-shrink:0}
/* A grouped action picker styled to read as a button, not a native <select> — used instead of a
   custom position:absolute dropdown (see the removed .actionMenu* history above) specifically
   because a real <select>'s own popup is rendered by the browser's OS-level UI, entirely outside
   the page's DOM/CSS, so it's immune to whatever unexplained rendering issue broke the custom
   dropdown for a real user despite every computed style checking out normal. appearance:none
   strips the native chrome so the .primary/.info background+color (combined via a second class,
   e.g. class="actionSelect primary") reads the same as this row's plain buttons; the inline SVG
   background-image is a plain caret standing in for the native dropdown arrow that got removed. */
.actionSelect{
  appearance:none;-webkit-appearance:none;-moz-appearance:none;
  width:auto;min-width:0;border:0;border-radius:11px;font:inherit;font-weight:800;cursor:pointer;
  padding-right:22px;
  background-repeat:no-repeat;background-position:right 6px center;background-size:10px;
}
/* Caret color matched per background — .primary is dark green with white text (white caret),
   .info is light blue with dark blue text (a white caret would be nearly invisible there). */
.actionSelect.primary{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='5 8 10 13 15 8'/%3E%3C/svg%3E")}
.actionSelect.info{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%231a5fb4' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='5 8 10 13 15 8'/%3E%3C/svg%3E")}
/* The "More…" picker (Update match-ups/Regenerate groups, consolidated off the date's action row
   to keep it to one line on mobile) reads as a plain .ghost button, same as the button it sits
   next to — its own caret matches --text rather than either accent color. */
.actionSelect.ghost{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='%2318331f' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='5 8 10 13 15 8'/%3E%3C/svg%3E")}
/* Browsers size a <select>'s closed box to its WIDEST option ("Publish to players (5)"), not just
   the visible placeholder text ("Publish…") — width:auto alone can't fix that, since it's still
   measuring every option. Explicit widths sized to just the placeholder text instead. */
.actionSelect.primary{width:92px}
.actionSelect.info{width:78px}
.actionSelect.ghost{width:68px}
/* Duration (type=number) — same base sizing the Start time picker's selects match below. */
.addSlotForm .fieldGrid input[type=number]{
  height:40px;width:100%;box-sizing:border-box;-moz-appearance:textfield;
}
.addSlotForm .fieldGrid input[type=number]::-webkit-inner-spin-button,
.addSlotForm .fieldGrid input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}
/* Start time — a plain 12-hour + AM/PM picker (hour/minute/AM-PM selects) instead of a native
   <input type=time>, which some browsers/locales render in 24-hour format, inconsistent with
   every "6:30 pm"-style time shown elsewhere in the app. Same 40px height as Duration next to it;
   minute gets a bit more room than hour/AM-PM since "00"-"59" is wider than "1"-"12"/"AM". */
.timePickerRow{display:flex;gap:6px}
.timePickerRow select{height:40px;box-sizing:border-box;min-width:0;flex:1}
.timePickerRow select:nth-child(2){flex:1.3}

/* Session-setup slot cards (admin.html's renderSlotGroupCard) — deliberately just `.session-choice`
   (the exact class players.html's own tiles use) plus this one modifier for the expand/collapse
   behavior, so admin and player cards are the SAME box: same border/background, same bold
   start-time + small duration + court-type badge header, same muted-text detail lines. No
   separate color/badge language invented for admin — only the interaction (an "Edit" trigger
   that expands into the field grid, instead of a "Tap to select" vote button) differs. */
.slotEditCard .fieldGrid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px}
/* Same fix as .addSlotForm .fieldGrid — grid items default to min-width:auto, so a <select>
   with long option text (a facility name, a booker's full name) could force its column wider
   than the card, pushing the whole card off-screen horizontally on a narrow phone. */
.slotEditCard .fieldGrid>div{min-width:0}
.slotEditCard .fieldGrid input,.slotEditCard .fieldGrid select{min-width:0}
@media(max-width:480px){.slotEditCard .fieldGrid{grid-template-columns:1fr}}
.slotEditCard .fieldGrid label{margin:0 0 4px}
.slotEditCard .actions{margin-top:10px;gap:6px}
.slotEditCard .actions button{flex:1}
.slotEditCard .slotEditCardBody{display:none;margin-top:10px;padding-top:10px;border-top:1px solid var(--line)}
.slotEditCard.expanded .slotEditCardBody{display:block}
/* One compact row per court within a time+facility group (admin.html's renderSlotGroupRow) —
   deliberately narrower than .fieldGrid's two-column blocks so a group with many courts (the
   whole point of this card) stacks as many slim lines instead of many full forms; no upper bound
   on how many rows can stack, the card and the page just grow. The action buttons (Publish/
   Unpublish, shown only once the parent date is itself published, plus the always-present
   "Delete Court") sit stacked in their own auto-sized 4th column (wide enough for their own
   text) so they never compete with the 3 real fields for width. */
.slotGroupRows{margin-top:10px}
.slotGroupRow{
  display:grid;grid-template-columns:1fr 1fr 90px auto;gap:8px;align-items:end;
  padding:10px 0;border-top:1px solid var(--line);
}
.slotGroupRow:first-child{border-top:none;padding-top:0}
.slotGroupRow>div{min-width:0}
.slotGroupRow select,.slotGroupRow input{min-width:0}
.slotGroupRow label{margin:0 0 4px}
.slotRowActions{display:flex;flex-direction:column;gap:6px}
@media(max-width:480px){.slotGroupRow{grid-template-columns:1fr}}

/* Settings tab (admin.html) — Players/Facilities/Courts/Developer, each its own named service
   collapsed by default (toggleSettingsService), expanding in place to reveal that service's own
   form/list. Reference data you set up once and rarely touch again, unlike the tabs either side
   of it, so nothing here is ever open by default. */
.settingsServiceGrid{display:flex;flex-direction:column;gap:10px;margin-top:12px}
.settingsService{border:1px solid var(--line);border-radius:14px;background:#fff;overflow:hidden}
.settingsServiceHead{
  width:100%;display:flex;justify-content:space-between;align-items:center;gap:8px;
  padding:14px 16px;background:none;border:none;font:inherit;font-weight:800;font-size:15px;
  cursor:pointer;text-align:left;color:inherit;
}
.settingsServiceChevron{transition:transform .15s;color:var(--muted);flex-shrink:0}
.settingsService.expanded .settingsServiceChevron{transform:rotate(180deg)}
.settingsServiceBody{display:none;padding:0 16px 16px}
.settingsService.expanded .settingsServiceBody{display:block}

/* Facility cards nested inside the Facilities service (admin.html's renderFacilityCard) — a
   lighter, tinted variant of .settingsService's own collapse chrome so a facility reads as
   something INSIDE the Facilities panel, not a sibling accordion at the same level. Expanding one
   reveals just that facility's own courts. */
/* overflow:hidden removed — .facilityCard's own background+border-radius already clip themselves
   fine with no children needing corner-clipping (facilityCardHead has no background of its own),
   but it was silently clipping the Publish/Share .shareMenu dropdown (a position:absolute child
   several levels down in facilityCardBody) whenever the menu tried to render outside the card's
   own box — the actual cause of "the dropdown doesn't show / looks cut off". */
.facilityCard{border:1px solid var(--line);border-radius:12px;margin-top:10px;background:var(--accent2)}
.facilityCard:first-child{margin-top:0}
.facilityCardHead{
  width:100%;display:flex;justify-content:space-between;align-items:center;gap:8px;
  padding:12px 14px;background:none;border:none;font:inherit;font-weight:800;font-size:14px;
  cursor:pointer;text-align:left;color:inherit;
}
.facilityCardChevron{transition:transform .15s;color:var(--muted);flex-shrink:0}
/* Child combinators (>), not descendant selectors — What's New nests a date-card's own
   .facilityCard inside an expanded month card's body (see app.js's whatsNewMonthCard). A plain
   descendant selector would match the CHILD card's chevron/body too, since it's still nested
   somewhere inside an ".expanded" ancestor — making every nested card look permanently expanded
   regardless of its own state. Every other .facilityCard usage in the app already has its own
   head/body as direct children of itself, so this is a no-op for all of them. */
.facilityCard.expanded>.facilityCardHead .facilityCardChevron{transform:rotate(180deg)}
.facilityCardBody{display:none;padding:0 14px 14px}
.facilityCard.expanded>.facilityCardBody{display:block}

/* Match sessions cards (admin.html's renderMatches) — same session-choice/slotEditCard shell as
   session-setup's own cards, stacked full-width (not .session-grid — a match carries far more
   content than a slot: player seats, court pickers) rather than tiled. Separated from the action
   buttons/notice text above by its own top margin (not just each card's own spacing, which the
   first card doesn't get from the card below it). */
#matches{margin-top:16px}
.matchCard{margin-top:12px}
.matchCard:first-child{margin-top:0}

/* Attendance form (admin.html) — reuses .sessionForm for its container, but each attendee is now
   its OWN card (background/border/radius), stacked with a gap instead of the old flat list of
   border-top-separated rows — much easier to tell where one player's details end and the next
   one's begin. Each card is a plain column: .attendeeTop (either checkbox+name+Replace, or —
   .substituting — an inline "who's filling in" editor, see startSubstitute()/cancelSubstitute())
   above a single "Order criteria" link — the reason/games/voted-slots detail itself lives in the
   per-player popup (showAttendeeDetail/#attendeeDetailDialog), not inline on every card at once. */
#attendanceGrid{counter-reset:attendeeRow;display:flex;flex-direction:column;gap:8px;margin-top:12px}
.attendeeRow{display:flex;flex-direction:column;gap:6px;padding:10px 12px;background:var(--card);border:1px solid var(--line);border-radius:12px;counter-increment:attendeeRow}
/* Numbered by priority (earliest signup for the date first — see byPriority() in admin.html) via
   a CSS counter rather than a JS-rendered digit, so the number survives startSubstitute()/
   cancelSubstitute() swapping a row's *inner* HTML in place — counter-increment lives on
   .attendeeRow itself (never touched by that swap), but the counter is DISPLAYED via
   .attendeeTop's own ::before, in the exact same flex row as checkbox/name/Replace (or the
   substituting editor). That's what actually aligns the number with them: they now share one line
   box, whatever height it ends up being (dominated by the Replace/Cancel button's own padding),
   instead of the number being aligned against the height of the WHOLE card the way a plain
   flex-start on the outer row would — which is exactly what previously left it sitting near the
   top, well above the checkbox/name. */
.attendeeRow .attendeeTop{display:flex;align-items:center;gap:8px}
.attendeeRow .attendeeTop::before{content:counter(attendeeRow)".";flex-shrink:0;width:1.6em;text-align:right;color:var(--muted);font-weight:800;font-size:13px}
.attendeeRow .attendeeName{flex:1;font-weight:700}
.attendeeRow .attendeeTop>span.meta{flex:1}
.attendeeRow .typeaheadWrap{flex:1}
/* The "Order criteria" link sits on its own line under .attendeeTop, flush left under the row's
   own number rather than indented under the name — the form-level .helpLink above it keeps its
   own larger margin, this override only applies once nested in a card. align-self:flex-start
   stops it stretching to the card's full width the way a flex-column child does by default,
   which otherwise left its (center-aligned, being a <button>) text looking centered under the
   name rather than actually flush left. */
.attendeeRow .helpLink{margin:0;align-self:flex-start}
/* The "Order criteria" popup's own body (admin.html's showAttendeeDetail) — plenty of width here,
   unlike the tight card row, so lines wrap normally instead of needing the card's nowrap+scroll
   treatment. */
.attendeeDetailMeta{display:flex;flex-direction:column;gap:8px}
.attendeeDetailMeta>span{display:block}

/* Typeahead (admin.html) — replaces every native <input list>+<datalist> field. The wrap is a
   plain block at 100% width so it drops into any layout (fieldGrid column, flex row, inline
   before a <br>) exactly the way the bare <input> it replaces used to; min-width:0 guards the
   same CSS-grid overflow bug already fixed elsewhere in this file (grid items don't shrink below
   their content's own intrinsic width by default). The suggestion list is positioned against
   this wrapper specifically (not the page), so it never needs to know where it ends up used. */
.typeaheadWrap{position:relative;display:block;width:100%;min-width:0}
/* The "Add match" form's 4 player seats (admin.html's #addMatchPlayers) — each .typeaheadWrap is
   already its own full-width block, but with no gap between them they ran straight into each
   other with no visual separation. */
#addMatchPlayers{display:flex;flex-direction:column;gap:8px}
.typeaheadList{display:none;position:absolute;left:0;right:0;top:100%;margin-top:4px;
  max-height:240px;overflow-y:auto;background:#fff;border:1px solid var(--line);border-radius:10px;
  box-shadow:0 10px 26px rgba(0,0,0,.14);z-index:40}
.typeaheadList.open{display:block}
.typeaheadOption{padding:9px 12px;font-size:13.5px;font-weight:600;cursor:pointer;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.typeaheadOption:hover,.typeaheadOption.active{background:var(--accent2)}

/* admin.html's Match sessions Publish/Share row deliberately does NOT use an anchored dropdown
   (position:absolute panel toggled by a class) any more — one was built and, for reasons never
   fully pinned down (renaming away from a "share"-in-classname ad-blocker collision fixed the
   click itself, but the panel still failed to visually render for at least one real user despite
   every computed style — display, opacity, visibility, position, size — checking out completely
   normal), never rendered reliably. Replaced with plain always-visible buttons instead (below),
   the same proven-reliable shape "Update match-ups" already used — nothing to toggle, nothing to
   position, nothing for an extension or an unexplained rendering quirk to interfere with. */

/* Reveal-password toggle (admin.html's login + change-password fields) — same "plain block
   wrapper at 100% width" shape as .typeaheadWrap above, so it drops into the existing layout
   without disturbing it; the input keeps its normal full-width sizing, just with room carved out
   on the right for the toggle button sitting on top of it. */
.pwWrap{position:relative;display:block;width:100%;min-width:0}
.pwWrap input{padding-right:40px}
.pwToggle{position:absolute;right:2px;top:50%;transform:translateY(-50%);width:auto;
  background:transparent;border:0;padding:6px;border-radius:8px;cursor:pointer;
  color:var(--muted);display:flex;align-items:center}
.pwToggle:hover{background:var(--bg)}
.pwToggle:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.typeaheadHint{color:var(--muted);font-weight:600;font-size:12px}
.attendeeRow button{flex-shrink:0}

/* Collapsible sections (players.html only — admin.html moved to tabs, below) — native
   <details>/<summary>, no JS needed for the open/close behavior itself. Each section is its
   own card; .detailsBody carries the padding .card normally has, since the <details> itself is
   padding:0 so the summary can be full-width clickable. */
.sectionList{display:flex;flex-direction:column;gap:14px}
/* #playerArea (players.html) wraps the actual per-section <details> cards one level below
   .sectionList — its own display is toggled between 'none' and '' by loadState(), so this flex
   layout only takes effect once '' lets the stylesheet default show through. Without it, .card
   margins are zero and the cards render flush against each other. */
#playerArea{display:flex;flex-direction:column;gap:14px}
details.card{padding:0;overflow:hidden}
details.card>summary{
  list-style:none;cursor:pointer;padding:18px;display:flex;align-items:center;justify-content:space-between;
  font-size:19px;font-weight:700;color:var(--text);
}
details.card>summary::-webkit-details-marker{display:none}
details.card>summary::after{content:'⌄';font-size:16px;color:var(--muted);transition:transform .15s ease;margin-left:10px;flex-shrink:0}
details.card[open]>summary::after{transform:rotate(180deg)}
details.card>summary:hover{background:#f7faf7}
.summaryCount{color:var(--muted);font-weight:600;font-size:14px}
/* Title + an optional empty-state subnote (Match sessions/history before there's data) as two
   clean, independently-styled lines instead of one run-on "Title (0) — some sentence" string —
   each line still never wraps and truncates with an ellipsis if it somehow doesn't fit, but
   they read as a deliberate heading + caption pair rather than jammed-together text. */
details.card>summary>.summaryTextBlock{display:flex;flex-direction:column;gap:3px;min-width:0;overflow:hidden}
.summaryTitle{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:19px;font-weight:700;color:var(--text)}
.summarySubnote{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:13px;font-weight:600;color:var(--muted)}

/* Match sessions/history (players.html) before there's anything to show — not expandable
   (the summary's own onclick blocks the toggle), so the chevron and hover feedback that imply
   "there's more here" are dropped too. */
details.card.disabled>summary{cursor:default;color:var(--muted)}
details.card.disabled>summary:hover{background:transparent}
details.card.disabled>summary::after{display:none}
.detailsBody{padding:0 18px 18px}


/* Tabbed layout (admin.html only) — one panel visible at a time instead of a scrolling stack
   of collapsible cards. Plain class-toggling, no library. */
.tabBar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}
/* Narrow viewports get a single scrollable row instead of wrapping onto 2-3 rows of pills —
   wrapping pushes content down and makes similarly-sized buttons harder to tell apart/tap
   precisely; a horizontal-scroll strip reads as one compact, swipeable nav bar instead, closer
   to how native mobile apps handle a tab set wider than the screen. */
@media(max-width:680px){
  .tabBar{flex-wrap:nowrap;overflow-x:auto;padding-bottom:4px;-webkit-overflow-scrolling:touch;scrollbar-width:none}
  .tabBar::-webkit-scrollbar{display:none}
  .tabBtn{flex-shrink:0}
}
.tabBtn{
  background:var(--card);border:1px solid var(--line);border-radius:999px;
  padding:10px 16px;font:inherit;font-weight:800;font-size:14px;cursor:pointer;color:var(--muted);
}
.tabBtn .summaryCount{color:inherit;opacity:.75}
.tabBtn.active{background:var(--accent);color:#fff;border-color:var(--accent)}
.tabBtn.active .summaryCount{color:#fff;opacity:.85}
.tabPanel{display:none}
.tabPanel.active{display:block}

/* Printing (Download PDF) — hide everything except the Match sessions content, so
   window.print() -> "Save as PDF" produces a clean schedule instead of the whole app chrome.
   The Download PDF button switches to that tab (admin.html) / opens that <details>
   (players.html) first, since non-visible content doesn't render to print either. */
@media print{
  .no-print{display:none!important}
  body{background:#fff}
  .wrap{max-width:100%;padding:0}
  .card{box-shadow:none;border:none;padding:0}
  details.card>summary::after{display:none}
  .session{border:1px solid #ccc;break-inside:avoid}
  .printOnly{display:block}
}

/* mets-doubles.html — a static graphic (assets/homepage.png) with the Admin/Players boxes
   drawn into it, plus two percentage-positioned invisible links laid over those exact box
   regions. Percentages (not px) so the overlays track the image's own scaling at any viewport
   width — measured directly from the source PNG's box borders (1024x1536): left box
   x 60-495/y 801-1306, right box x 529-963/y 801-1306. Capped at 480px wide — at the image's
   tall 2:3 aspect ratio, letting it stretch to .wrap's full 1220px would make it enormous. The
   image's own background is transparent (not a white rectangle), so it sits directly on the
   page's own --bg color instead of showing as a mismatched white card. */
.homepageImage{position:relative;max-width:480px;margin:0 auto}
.homepageImage img{width:100%;height:auto;display:block}
.homepageLink{position:absolute;border-radius:12px;cursor:pointer;transition:background .15s ease}
.homepageLink:hover{background:rgba(25,120,68,.08)}
.homepageLink:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
/* mets-doubles.html's own way back up to the group picker — sits above the homepage graphic,
   left-aligned like the "Dashboard" links on admin.html/players.html rather than centered with
   the graphic below it, so it reads as page chrome, not part of the branded poster. */
.groupBackLink{display:inline-block;margin-bottom:14px}

.homepageLinkAdmin{left:5.9%;top:52.2%;width:42.5%;height:32.9%}
.homepageLinkPlayers{left:51.7%;top:52.2%;width:42.4%;height:32.9%}
/* index.html's single clickable region — the MetsApp Doubles card baked into
   assets/homepage-hero.png (1404x1120). Measured directly from the source PNG's card border:
   x 11-1385/y 397-1101. The "More groups" placeholder and feature row below are now their own
   real HTML (see .moreGroupsCard/.featureRow) rather than baked into this image, so they have no
   overlay here. */
.homepageLinkGroup{left:0.8%;top:35.4%;width:97.9%;height:62.9%}
.headerLink{text-decoration:none}

/* index.html only — "More groups — coming soon" and the feature-icon row used to be baked into
   the homepage hero graphic; pulled out into real HTML/CSS so they aren't tied to a fixed image
   composition and can grow (e.g. a real second group) independently of the hero artwork. Same
   480px column as .homepageImage so the whole page reads as one continuous width. */
.moreGroupsCard{
  max-width:480px;margin:16px auto 0;padding:30px 20px;text-align:center;
  border:2px dashed var(--line);border-radius:18px;background:var(--card);
}
.moreGroupsPlus{
  width:44px;height:44px;line-height:40px;margin:0 auto 12px;border:2px dashed var(--line);
  border-radius:50%;font-size:24px;color:var(--muted);
}
.moreGroupsTitle{font-weight:850;font-size:18px}
.moreGroupsSub{color:var(--muted);margin-top:3px;font-size:14px}

.featureRow{
  max-width:480px;margin:18px auto 0;display:grid;grid-template-columns:repeat(4,1fr);gap:8px;
}
@media(max-width:480px){.featureRow{grid-template-columns:repeat(2,1fr);gap:16px 8px}}
.featureItem{text-align:center;padding:4px}
.featureItem svg{width:24px;height:24px;color:var(--accent);margin-bottom:6px}
.featureTitle{font-size:12.5px;font-weight:800}
.featureSub{font-size:11px;color:var(--muted);margin-top:2px}

/* Wraps the "← Dashboard" badge (+ admin.html's "Players page →" badge, see .headerNav) above
   the h1/sub — on players.html this also keeps the two header halves (this block vs .headerRight)
   as the two flex children header{} expects, so a 3rd top-level child doesn't break space-between
   when Log out is conditionally hidden before login. */
.headerLeft{display:flex;flex-direction:column;align-items:flex-start;gap:6px}

/* admin.html only — "← Dashboard" and "Players page →" share one row here instead of sitting
   at opposite corners of the whole header competing with the <h1> for space, which is what let
   the two badges end up different effective widths/wrap independently on a narrow screen
   despite sharing the same .badge CSS. */
.headerNav{display:flex;gap:8px;flex-wrap:wrap}

/* Right side of header{} — the Help button (both pages) plus players.html's Log out button.
   header{}'s own space-between only expects two top-level children, so both live in this one
   wrapper rather than as separate siblings. */
.headerRight{display:flex;gap:8px;align-items:flex-start;flex-shrink:0}

/* Help center popup (players.html + admin.html) — a native <dialog>, opened via
   showModal()/close() from initHelpCenter()/openHelpCenter()/closeHelpCenter() in app.js. Two
   tabs share one scrollable body so the dialog's own height doesn't jump when switching between
   them. */
.helpDialog{
  border:none;border-radius:18px;padding:0;width:min(520px,92vw);max-height:85vh;
  box-shadow:0 20px 50px rgba(0,0,0,.25);color:var(--text);font-family:inherit;
}
.helpDialog::backdrop{background:rgba(10,20,12,.45)}
.helpDialogHead{display:flex;align-items:center;justify-content:space-between;padding:10px 10px 10px 18px;border-bottom:1px solid var(--line)}
.helpTabs{display:flex;gap:6px}
.helpTab{background:transparent;color:var(--muted);padding:8px 12px;border-radius:999px;font-size:14px}
.helpTab.active{background:var(--accent2);color:var(--accent)}
.helpClose{background:transparent;color:var(--muted);padding:6px 10px;font-size:20px;line-height:1;border-radius:8px}
.helpClose:hover{background:var(--bg)}
.helpDialogBody{padding:6px 18px 18px;overflow-y:auto;max-height:calc(85vh - 56px)}
.helpEntry{padding:12px 0;border-bottom:1px solid var(--line)}
.helpEntry:last-child{border-bottom:none}
.helpEntryTitle{font-weight:800;margin-bottom:4px}
.helpEntryBody{color:var(--muted);font-size:14px;line-height:1.5}
