/*
 * ── Responsive Breakpoints (Issue #460) ────────────────────────────────
 *
 * Tiers used across the dashboard. CSS custom properties cannot be
 * interpolated into @media queries, so the literals below are the
 * canonical values — every @media (max-width: …) in this file MUST use
 * one of them. The custom properties exist so JS / inline styles can
 * read the same values.
 *
 *   Phone   ≤ 640px    --bp-phone
 *   Tablet  ≤ 1024px   --bp-tablet
 *   Desktop > 1024px   (no @media — default styles)
 *
 * Legacy breakpoints (480/600/720/768) predate this rationalisation;
 * new code should use 640 / 1024. The pre-existing 480 / 600 / 720 /
 * 768 queries are kept intact to avoid churning #457/#458/#459 work.
 *
 * Acceptance: every primary page must render without horizontal
 * overflow at 375px, 768px, and 1280px.
 */
/* ── Design System ─────────────────────────────────────────────────────── */
:root {
  --bp-phone: 640px;
  --bp-tablet: 1024px;

  --green: #22c55e;
  --green-dim: #16a34a;
  --blue: #3b82f6;
  --blue-dim: #2563eb;
  --yellow: #eab308;
  --red: #ef4444;
  --orange: #f97316;
  --purple: #a855f7;
  --pink: #ec4899;
  --cyan: #06b6d4;

  --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 16px;
  --radius-xl: 20px;
  --radius-full: 9999px;

  --transition: 0.22s cubic-bezier(.4,0,.2,1);
  --transition-slow: 0.4s cubic-bezier(.4,0,.2,1);

  /* Issue #1726 item 1: sticky-stack heights. The JS ResizeObserver
     (see main.js) republishes --topbar-h on this :root element when the
     topbar wraps to a second row at ≤480px, so the .day-header sticky
     offset adapts instead of using a hard-coded 104px pixel value.
     There is no CSS media query that overrides --topbar-h; the update
     is purely JS-driven. */
  --topbar-h: 64px;
  --navtabs-h: 40px;
  /* Issue #1712: admin tier-impersonation banner is position:fixed at
     top:0 with z-index:1000, overlapping every sticky element below.
     trackStickyStackHeights() in main.js republishes the measured banner
     height (or 0 when hidden) so the topbar / nav-tabs / day-header
     stickies offset by exactly the right amount on every viewport. */
  --banner-h: 0px;
}

/* Dark theme (default) */
[data-theme="dark"] {
  --bg-0: #09090b;
  --bg-1: #121218;
  --bg-2: #1a1a24;
  --bg-3: #24243a;
  --bg-card: rgba(255,255,255,0.04);
  --bg-card-hover: rgba(255,255,255,0.07);
  --border: rgba(255,255,255,0.08);
  --border-strong: rgba(255,255,255,0.14);
  --text-0: #fafafa;
  --text-1: #d4d4d8;
  --text-2: #a1a1aa;
  /* #2051/#2052/#2053 — bumped from #71717a (3.6–4.1:1) to meet WCAG 2 AA
     4.5:1 for tertiary/muted text (clock, ⌘K hint, filter labels, etc.). */
  --text-3: #8b8b94;
  --glass-bg: rgba(18,18,28,0.72);
  --glass-border: rgba(255,255,255,0.08);
  --skeleton: rgba(255,255,255,0.06);
  --skeleton-shine: rgba(255,255,255,0.1);
  --shadow-card: 0 2px 8px rgba(0,0,0,0.3), 0 0 0 1px var(--border);
  --shadow-card-hover: 0 12px 40px rgba(0,0,0,0.5), 0 0 0 1px var(--border-strong);
  --shadow-glow: 0 0 40px rgba(34,197,94,0.08);
  --filter-bg: rgba(255,255,255,0.05);
  --filter-active: rgba(34,197,94,0.15);
  --filter-active-border: rgba(34,197,94,0.4);
  --toast-bg: rgba(18,18,28,0.92);
}

/* Light theme */
[data-theme="light"] {
  --bg-0: #f8fafc;
  --bg-1: #ffffff;
  --bg-2: #f1f5f9;
  --bg-3: #e2e8f0;
  --bg-card: rgba(255,255,255,0.8);
  --bg-card-hover: rgba(255,255,255,0.95);
  --border: rgba(0,0,0,0.08);
  --border-strong: rgba(0,0,0,0.14);
  --text-0: #0f172a;
  --text-1: #334155;
  --text-2: #64748b;
  /* #2051/#2052/#2053 — bumped from #94a3b8 (2.3–2.6:1) to meet WCAG 2 AA
     4.5:1 for tertiary/muted text in light theme. */
  --text-3: #5d6a7d;
  --glass-bg: rgba(255,255,255,0.72);
  --glass-border: rgba(0,0,0,0.06);
  --skeleton: rgba(0,0,0,0.06);
  --skeleton-shine: rgba(0,0,0,0.1);
  --shadow-card: 0 1px 3px rgba(0,0,0,0.08), 0 0 0 1px var(--border);
  --shadow-card-hover: 0 8px 30px rgba(0,0,0,0.12), 0 0 0 1px var(--border-strong);
  --shadow-glow: 0 0 40px rgba(34,197,94,0.06);
  --filter-bg: rgba(0,0,0,0.04);
  --filter-active: rgba(34,197,94,0.12);
  --filter-active-border: rgba(34,197,94,0.5);
  --toast-bg: rgba(255,255,255,0.95);
}

/* ── Reset & Base ──────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
}

body {
  font-family: var(--font);
  background: var(--bg-0);
  color: var(--text-0);
  line-height: 1.5;
  /* `overflow-x: clip` prevents horizontal scroll without making body a
     scroll container, which would break `position: sticky` on `.topbar`
     and `.nav-tabs` (issue #358). `clip` is supported in all evergreen
     browsers since 2022. */
  overflow-x: clip;
  min-height: 100vh;
}

/* Animated background with slow hue shift */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -2;
  background:
    radial-gradient(ellipse 80% 60% at 50% -20%, rgba(34,197,94,0.10) 0%, transparent 60%),
    radial-gradient(ellipse 60% 50% at 80% 50%, rgba(59,130,246,0.08) 0%, transparent 60%),
    radial-gradient(ellipse 50% 40% at 20% 80%, rgba(168,85,247,0.07) 0%, transparent 60%),
    radial-gradient(ellipse 70% 50% at 70% 20%, rgba(6,182,212,0.05) 0%, transparent 55%),
    radial-gradient(ellipse 40% 30% at 10% 40%, rgba(236,72,153,0.04) 0%, transparent 50%);
  animation: bgHueShift 30s ease-in-out infinite alternate;
}

@keyframes bgHueShift {
  0%   { filter: hue-rotate(0deg) brightness(1); }
  50%  { filter: hue-rotate(15deg) brightness(1.02); }
  100% { filter: hue-rotate(-10deg) brightness(0.98); }
}

/* Subtle grain texture */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  opacity: 0.3;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
  background-repeat: repeat;
  pointer-events: none;
}

a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; border: none; background: none; }
input { font-family: inherit; }

/* ── Scrollbar ─────────────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--text-3); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-2); }

/* ── Top Bar ───────────────────────────────────────────────────────────── */
.topbar {
  position: sticky;
  /* Issue #1712: shift below the admin tier-impersonation banner when it
     is visible. --banner-h is 0 in normal sessions. */
  top: var(--banner-h, 0px);
  z-index: 100;
  backdrop-filter: blur(20px) saturate(1.4);
  -webkit-backdrop-filter: blur(20px) saturate(1.4);
  background: var(--glass-bg);
  border-bottom: 1px solid var(--glass-border);
  padding: 0 24px;
  height: 64px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

.topbar-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  white-space: nowrap;
  color: inherit;
  text-decoration: none;
}

.topbar-brand:hover,
.topbar-brand:focus-visible {
  text-decoration: none;
  opacity: 0.92;
}

/* Issue #2871 — brand links to dashboard home for discoverable navigation. */
.topbar-brand-link {
  display: flex;
  align-items: center;
  gap: 10px;
  color: inherit;
  text-decoration: none;
}

.topbar-brand-link:hover,
.topbar-brand-link:focus-visible {
  text-decoration: none;
  opacity: 0.9;
}

.topbar-brand .logo {
  font-size: 28px;
  line-height: 1;
}

.topbar-brand h1 {
  font-size: clamp(15px, 2vw, 18px);
  font-weight: 700;
  /* Issue #2398: previously used a green→blue gradient via
     background-clip: text + -webkit-text-fill-color: transparent.  That
     made axe-core flag the element as a color-contrast "incomplete"
     because the computed foreground was transparent and the gradient
     background was not determinable.  Swap to a solid var(--text-0)
     (white in dark theme, near-black in light) so axe can compute
     contrast against the topbar's --glass-bg deterministically (≥12:1
     AAA in both themes).  The decorative glow lives in the adjacent
     drop-shadow + faint text-shadow so the brand still feels lit. */
  color: var(--text-0);
  letter-spacing: -0.03em;
  text-shadow: 0 0 30px rgba(34,197,94,0.3);
  filter: drop-shadow(0 0 8px rgba(34,197,94,0.15));
}

.topbar-stats {
  display: flex;
  align-items: center;
  gap: 20px;
  font-size: 13px;
  color: var(--text-2);
}

.topbar-stat {
  display: flex;
  align-items: center;
  gap: 6px;
}

.topbar-stat .num {
  font-weight: 700;
  color: var(--text-0);
  font-variant-numeric: tabular-nums;
}

.topbar-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Issue #4192 — view-mode, density, and auto-refresh are extracted from
   .topbar-actions into their own wrapper so they remain reachable at ≤480px
   even when the hamburger menu collapses .topbar-actions.  A child element
   cannot override a parent's display:none, so the only reliable fix is to
   keep these three buttons outside the collapsible container. */
.topbar-view-controls {
  display: flex;
  align-items: center;
  gap: 8px;
}

#btn-mobile-menu { display: none; }

.icon-btn {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-2);
  transition: all var(--transition);
  font-size: 15px;
  position: relative;
}

.icon-btn:hover {
  background: var(--filter-bg);
  color: var(--text-0);
}

.icon-btn:active {
  background: var(--bg-card-hover);
  color: var(--text-0);
}

.icon-btn:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}

.icon-btn.active {
  color: var(--green);
}

.theme-toggle {
  overflow: hidden;
}

.topbar-clock {
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  color: var(--text-3);
  min-width: 70px;
  text-align: right;
}

/* ── Filter Bar ────────────────────────────────────────────────────────── */
.filter-bar {
  position: relative;
  z-index: 90;
  backdrop-filter: blur(16px) saturate(1.3);
  -webkit-backdrop-filter: blur(16px) saturate(1.3);
  background: var(--glass-bg);
  border-bottom: 1px solid var(--glass-border);
  padding: 14px 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  transition: max-height 0.3s ease, padding 0.3s ease;
  overflow: hidden;
}
/* Issue #2873 — #filter-collapse-btn is the first DOM child, so
   :first-child never matches a .filter-row and the old rule hid every
   row (including #date-from). Pin search + range rows instead. */
.filter-bar.collapsed .filter-row:not(.filter-row-pinned) {
  display: none;
}
.filter-bar.collapsed {
  padding: 10px 24px;
  gap: 0;
  overflow: visible;
}
/* Issue #2873 — when collapsed on mobile, hide toggle chips so search +
   date range fit above the fold without scrolling. */
@media (max-width: 768px) {
  .filter-bar.collapsed #filter-search-row .toggle-wrap {
    display: none;
  }
  .filter-bar.collapsed #filter-date-range {
    flex: 1 1 100%;
    min-width: 0;
  }
}

.filter-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.filter-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-3);
  min-width: 60px;
  flex-shrink: 0;
}

/* Search */
.search-wrap {
  position: relative;
  flex: 1;
  max-width: 360px;
}

.search-wrap i {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-3);
  font-size: 14px;
  pointer-events: none;
}

.search-wrap input {
  width: 100%;
  padding: 8px 12px 8px 36px;
  border-radius: var(--radius-full);
  border: 1px solid var(--border);
  background: var(--filter-bg);
  color: var(--text-0);
  font-size: 13px;
  outline: none;
  transition: all var(--transition);
}

.search-wrap input::placeholder { color: var(--text-3); }

.search-wrap input:focus {
  border-color: var(--green);
  box-shadow: 0 0 0 3px rgba(34,197,94,0.12);
}

.search-kbd {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 11px;
  color: var(--text-3);
  background: var(--bg-2);
  padding: 2px 6px;
  border-radius: 4px;
  font-weight: 500;
  pointer-events: none;
  border: 1px solid var(--border);
}

/* Chips */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 12px;
  border-radius: var(--radius-full);
  font-size: 12px;
  font-weight: 500;
  border: 1px solid var(--border);
  background: var(--filter-bg);
  color: var(--text-2);
  cursor: pointer;
  transition: all var(--transition);
  white-space: nowrap;
  user-select: none;
}

.chip:hover {
  border-color: var(--border-strong);
  color: var(--text-1);
  background: var(--bg-card-hover);
}

.chip:active {
  background: var(--bg-card-hover);
  color: var(--text-0);
  border-color: var(--border-strong);
}

.chip:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}

.chip.active {
  background: var(--filter-active);
  border-color: var(--filter-active-border);
  color: var(--green);
  font-weight: 600;
}

.chip .chip-count {
  background: var(--bg-3);
  padding: 1px 6px;
  border-radius: var(--radius-full);
  /* Issue #1726 item 13: 11px floor for content-bearing micro-type. */
  font-size: 11px;
  font-weight: 700;
  min-width: 20px;
  text-align: center;
}

.chip.active .chip-count {
  background: rgba(34,197,94,0.2);
}

/* Toggle switch */
.toggle-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--text-2);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  /* Button reset (issue #1971): elements use <button> for valid ARIA role="switch" */
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font-family: inherit;
  line-height: inherit;
}

.toggle-track {
  width: 36px;
  height: 20px;
  border-radius: 10px;
  background: var(--bg-3);
  position: relative;
  transition: background var(--transition);
  flex-shrink: 0;
}

.toggle-track::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--text-2);
  transition: all var(--transition);
}

.toggle-wrap.active .toggle-track {
  background: var(--green);
}

.toggle-wrap.active .toggle-track::after {
  left: 18px;
  background: #fff;
}

@media (max-width: 768px) {
  .toggle-wrap {
    min-height: 44px;
  }
}

/* Segmented control (day range) */
.seg-control {
  display: flex;
  background: var(--filter-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  overflow: hidden;
}

.seg-btn {
  padding: 5px 14px;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-2);
  transition: all var(--transition);
  border-right: 1px solid var(--border);
  white-space: nowrap;
}

.seg-btn:last-child { border-right: none; }

.seg-btn:hover { color: var(--text-0); background: var(--bg-card-hover); }
.seg-btn:active { color: var(--text-0); background: var(--bg-card-hover); }
.seg-btn:focus-visible { outline: 2px solid var(--green); outline-offset: -2px; }

.seg-btn.active {
  background: var(--green);
  /* #2051/#2052/#2053 — #fff on --green was 2.27:1 (WCAG fail). Dark text on
     the vivid brand green is 7.5:1 and keeps the accent colour. */
  color: #04210f;
  font-weight: 600;
}

/* Min spots chips */
.spots-chip {
  width: 32px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  font-size: 12px;
  font-weight: 600;
  border: 1px solid var(--border);
  background: var(--filter-bg);
  color: var(--text-2);
  cursor: pointer;
  transition: all var(--transition);
}

.spots-chip:hover { border-color: var(--border-strong); color: var(--text-0); }

.spots-chip.active {
  background: var(--filter-active);
  border-color: var(--filter-active-border);
  color: var(--green);
}

/* Day-of-week chips */
.dow-chip {
  padding: 5px 10px;
  border-radius: var(--radius-full);
  font-size: 12px;
  font-weight: 500;
  border: 1px solid var(--border);
  background: var(--filter-bg);
  color: var(--text-2);
  cursor: pointer;
  transition: all var(--transition);
  user-select: none;
  white-space: nowrap;
}
.dow-chip:hover { border-color: var(--border-strong); color: var(--text-0); }
.dow-chip:active { background: var(--bg-card-hover); border-color: var(--border-strong); color: var(--text-0); }
.dow-chip:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.dow-chip.active {
  background: var(--filter-active);
  border-color: var(--filter-active-border);
  color: var(--green);
  font-weight: 600;
}

/* Time filter controls */
.time-presets { display: flex; gap: 4px; }

.time-preset {
  padding: 5px 12px;
  border-radius: var(--radius-full);
  font-size: 12px;
  font-weight: 500;
  border: 1px solid var(--border);
  background: var(--filter-bg);
  color: var(--text-2);
  cursor: pointer;
  transition: all var(--transition);
  white-space: nowrap;
  user-select: none;
}
.time-preset:hover { border-color: var(--border-strong); color: var(--text-0); }
.time-preset:active { background: var(--bg-card-hover); border-color: var(--border-strong); color: var(--text-0); }
.time-preset:focus-visible { outline: 2px solid var(--green); outline-offset: 2px; }
.time-preset.active {
  background: var(--filter-active);
  border-color: var(--filter-active-border);
  color: var(--green);
  font-weight: 600;
}

.time-custom {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-left: 6px;
}
.time-custom input[type="time"] {
  padding: 4px 8px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--filter-bg);
  color: var(--text-1);
  font-size: 12px;
  font-family: 'Inter', sans-serif;
  outline: none;
  transition: border-color var(--transition);
  width: 100px;
}
.time-custom input[type="time"]:focus {
  border-color: var(--green);
  box-shadow: 0 0 0 3px rgba(34,197,94,0.12);
}
.time-custom .time-sep {
  font-size: 12px;
  color: var(--text-3);
  font-weight: 500;
}
.time-custom .time-clear {
  padding: 4px 8px;
  border-radius: var(--radius-md);
  font-size: 11px;
  font-weight: 500;
  border: 1px solid var(--border);
  background: var(--filter-bg);
  color: var(--text-3);
  cursor: pointer;
  transition: all var(--transition);
}
/* Issue #2398: the visible glyph (✕) is now painted via a ::before
   pseudo, with the button's text content kept empty.  axe scans the
   element's textContent ("non-text only") and would otherwise flag
   this as a colour-contrast incomplete.  Pseudo-element content is
   not part of textContent, so axe defers to the explicit aria-label
   for the accessible name and the icon is treated as purely
   decorative.  Padding stays inside the button; the pseudo glyph
   inherits the parent font-size/colour for visual parity. */
.time-custom .time-clear::before {
  content: '\2715';
}
.time-custom .time-clear:hover {
  border-color: var(--border-strong);
  color: var(--text-1);
}

/* Date range filter */
.date-range-wrap {
  display: flex;
  align-items: center;
  gap: 6px;
}
.date-range-wrap input[type="date"] {
  padding: 4px 8px;
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
  background: var(--filter-bg);
  color: var(--text-1);
  font-size: 12px;
  font-family: 'Inter', sans-serif;
  outline: none;
  transition: border-color var(--transition);
  width: 130px;
}
.date-range-wrap input[type="date"]:focus {
  border-color: var(--green);
  box-shadow: 0 0 0 3px rgba(34,197,94,0.12);
}
/* Issue #1095 — visual cue when date-from > date-to */
.date-range-wrap input[type="date"].date-input-invalid,
.date-range-wrap input[type="date"].date-input-invalid:focus {
  border-color: var(--red);
  box-shadow: 0 0 0 3px rgba(239,68,68,0.15);
}
.date-range-wrap .date-sep {
  font-size: 12px;
  color: var(--text-3);
  font-weight: 500;
}
.date-range-wrap .date-clear {
  padding: 4px 8px;
  border-radius: var(--radius-md);
  font-size: 11px;
  font-weight: 500;
  border: 1px solid var(--border);
  background: var(--filter-bg);
  color: var(--text-3);
  cursor: pointer;
  transition: all var(--transition);
}
/* Issue #2398: see .time-clear::before above — same rationale. */
.date-range-wrap .date-clear::before {
  content: '\2715';
}
.date-range-wrap .date-clear:hover {
  border-color: var(--border-strong);
  color: var(--text-1);
}

/* ── Main Content ──────────────────────────────────────────────────────── */
.main {
  max-width: 1100px;
  margin: 0 auto;
  padding: 24px 20px 80px;
}

/* Day group */
.day-group {
  margin-bottom: 32px;
}

.day-header {
  position: sticky;
  /* Issue #1726 item 1: derive from --topbar-h + --navtabs-h so the
     offset adapts when the topbar wraps to a second row on small phones
     (the JS ResizeObserver in main.js republishes --topbar-h at runtime
     whenever the topbar height changes — e.g. action-row wrap at ≤480px).
     Issue #1712: also offset by --banner-h when the admin tier-
     impersonation banner is visible (0 in normal sessions). */
  top: calc(var(--topbar-h) + var(--navtabs-h) + var(--banner-h, 0px));
  z-index: 50;
  display: flex;
  align-items: baseline;
  gap: 12px;
  /* Issue #2398: previously a ::before pseudo with `position:absolute;
     inset:0; opacity:0.85` painted the bleed.  Because the pseudo
     covered the entire content box, axe-core could not determine the
     effective background colour for any text child (.day-title /
     .day-relative / .day-count) and reported a colour-contrast
     "incomplete" for every node (24 of 28 incompletes on the
     dashboard).  Move the bleed background onto .day-header itself
     using a negative margin + matching padding, with an opaque
     background-color so axe can trace contrast deterministically. */
  margin: 0 -20px;
  padding: 16px 20px 12px;
  background-color: var(--bg-0);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.day-title {
  font-size: clamp(18px, 3vw, 24px);
  font-weight: 700;
  letter-spacing: -0.04em;
  color: var(--text-0);
  text-transform: none;
}

.day-title .day-relative {
  font-weight: 400;
  font-size: 14px;
  color: var(--text-3);
  margin-left: 6px;
}

.day-count {
  background: var(--bg-3);
  color: var(--text-2);
  font-size: 11px;
  font-weight: 700;
  padding: 2px 10px;
  border-radius: var(--radius-full);
}

.day-line {
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* Events grid */
.events-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 14px;
}

@media (max-width: 720px) {
  .events-grid { grid-template-columns: 1fr; }
}

/* ── Event Card ────────────────────────────────────────────────────────── */
.event-card {
  position: relative;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 16px 16px 16px 22px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  transition: all var(--transition);
  /* Issue #4051 — removed overflow:hidden so the friend-badge CSS tooltip
     (::after on .badge-friends) is not clipped. The availability ::before bar
     and hover ::after inset shadow are both positioned within the card bounds,
     so nothing else overflows. Border-radius visual clipping is handled by
     the browser without overflow:hidden for the internal content. */
  overflow: visible;
  /* Issue #4125 — with overflow:visible (set above, not hidden), CSS Grid
     treats min-width:auto as the card's min-content size rather than 0.
     On narrow viewports (iPhone 11, 414px) this inflates the 1fr track
     beyond the available width, pushing all 590 cards past the viewport
     edge.  min-width:0 restores the sizing behaviour that overflow:hidden
     previously provided; max-width:100% is a belt-and-suspenders cap. */
  min-width: 0;
  max-width: 100%;
  opacity: 0;
  transform: translateY(12px);
  animation: cardIn 0.4s var(--transition) forwards;
}

/* Issue #1723: respect prefers-reduced-motion — skip the slide-in entry
   animation entirely so the card is visible from the first frame instead
   of fading/translating in. */
@media (prefers-reduced-motion: reduce) {
  .event-card {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

.event-card:hover {
  background: var(--bg-card-hover);
  box-shadow: var(--shadow-card-hover);
  transform: translateY(-4px) perspective(800px) rotateX(1deg);
  border-color: var(--border-strong);
  border-image: linear-gradient(135deg, rgba(34,197,94,0.3), rgba(59,130,246,0.3), rgba(168,85,247,0.3)) 1;
  border-image-slice: 1;
  /* Issue #4051 — lift hovered card so its friend tooltip (which now overflows
     via overflow:visible) is painted above subsequently-rendered sibling cards. */
  z-index: 1;
}

.event-card:hover::before {
  box-shadow: 0 0 12px 2px var(--avail-color, var(--green));
}

.event-card:hover::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: var(--radius-lg);
  box-shadow: inset 0 2px 8px rgba(0,0,0,0.15);
  pointer-events: none;
}

@keyframes cardIn {
  to { opacity: 1; transform: translateY(0); }
}

/* Availability edge bar */
.event-card::before {
  content: '';
  position: absolute;
  left: 0;
  top: 12px;
  bottom: 12px;
  width: 4px;
  border-radius: 2px;
  background: var(--avail-color, var(--green));
}

.event-card[data-avail="high"]   { --avail-color: var(--green); }
.event-card[data-avail="medium"] { --avail-color: var(--yellow); }
.event-card[data-avail="low"]    { --avail-color: var(--orange); }
.event-card[data-avail="full"]   { --avail-color: var(--text-3); }

.card-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

.card-time {
  font-size: clamp(16px, 2vw, 18px);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text-0);
  line-height: 1.2;
}

.card-name {
  font-size: 13px;
  font-weight: 500;
  color: var(--text-1);
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.card-badges {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  border-radius: var(--radius-full);
  /* Issue #1726 item 13: 11px floor for content-bearing labels. */
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.badge-venue {
  background: rgba(59,130,246,0.12);
  color: var(--blue);
}

.badge-venue.fairgrounds {
  background: rgba(234,179,8,0.12);
  color: var(--yellow);
}

.badge-venue.picklr {
  background: rgba(168,85,247,0.12);
  color: var(--purple);
}

.badge-category {
  background: rgba(34,197,94,0.12);
  color: var(--green);
}

.badge-category[data-cat="DUPR Play"] {
  background: rgba(249,115,22,0.12);
  color: var(--orange);
}

.badge-category[data-cat="Adult Clinic"] {
  background: rgba(6,182,212,0.12);
  color: var(--cyan);
}

.badge-category[data-cat="Tournament"] {
  background: rgba(239,68,68,0.12);
  color: var(--red);
}

.badge-category[data-cat="Super Social"] {
  background: rgba(236,72,153,0.12);
  color: var(--pink);
}

.badge-category[data-cat="Rating Session"] {
  background: rgba(234,179,8,0.12);
  color: var(--yellow);
}

.badge-tag {
  background: var(--bg-3);
  color: var(--text-2);
  /* Issue #1726 item 13: bumped from 9px (below readable floor). */
  font-size: 11px;
  padding: 1px 6px;
}

/* Calendar conflict / free badges */
.badge-conflict {
  background: rgba(239,68,68,0.15);
  color: var(--red);
  font-weight: 600;
}

.badge-free {
  background: rgba(34,197,94,0.15);
  color: var(--green);
  font-weight: 600;
}

/* #2051/#2052/#2053 — venue/category badge text sat at 4.1–4.43:1 on its own
   12%-tinted background in dark theme (WCAG AA needs 4.5:1). Lighten the blue,
   red and purple badge labels to ~6:1. Scoped to dark theme so the light-theme
   palette (darker accents on near-white tints) is untouched. */
[data-theme="dark"] .badge-venue { color: #60a5fa; }
[data-theme="dark"] .badge-venue.fairgrounds { color: #facc15; }
[data-theme="dark"] .badge-venue.picklr { color: #c084fc; }
[data-theme="dark"] .badge-category[data-cat="Tournament"] { color: #f87171; }
[data-theme="dark"] .badge-conflict { color: #f87171; }
[data-theme="dark"] .badge-auto { color: #c084fc; }
[data-theme="dark"] .badge-soon { color: #f87171; }

/* "Within 24h" cancellation-fee warning badge (was inline-styled in main.js).
   Light-theme keeps #ef4444; dark theme lightens to #f87171 above for AA. */
.badge-soon {
  background: rgba(239,68,68,0.12);
  color: #ef4444;
  border: 1px solid rgba(239,68,68,0.25);
  font-size: 10px;
  padding: 2px 7px;
  border-radius: var(--radius-full);
  font-weight: 600;
}

/* Card calendar conflict overlay */
.event-card.calendar-conflict {
  border-left: 3px solid var(--red);
}

.event-card.calendar-free {
  border-left: 3px solid var(--green);
}

/* Issue #1145 — preview events scraped from /programs?view=daily.
   Dimmed card + dashed border distinguishes them from API-sourced
   events that are immediately bookable. */
.event-card[data-preview="true"] {
  opacity: 0.78;
  border-left: 3px dashed var(--cyan);
  background: linear-gradient(180deg, rgba(34,211,238,0.04), transparent);
}
.event-card[data-preview="true"]:hover {
  opacity: 1;
}
.badge-preview {
  background: rgba(34,211,238,0.15);
  color: var(--cyan);
  border: 1px solid rgba(34,211,238,0.35);
  font-weight: 600;
}
.badge-preview-opens {
  background: rgba(168,85,247,0.12);
  color: #a855f7;
  border: 1px solid rgba(168,85,247,0.30);
  font-weight: 500;
}
.badge-queued {
  background: rgba(34,197,94,0.15);
  color: var(--green);
  border: 1px solid rgba(34,197,94,0.30);
  font-weight: 600;
}
.btn-queue {
  background: var(--cyan);
  color: #000;
  border: none;
  border-radius: var(--radius-sm);
  padding: 8px 14px;
  font-weight: 600;
  cursor: pointer;
  font-family: var(--font);
  font-size: 13px;
}
.btn-queue:hover {
  filter: brightness(1.1);
}
.btn-queue:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* Free Only toggle — hidden until calendar is active */
#toggle-free-only {
  display: none;
  opacity: 0;
  transition: opacity 0.25s ease;
}

#toggle-free-only.visible {
  display: flex;
  opacity: 1;
}

/* Calendar loading shimmer on toggle */
.toggle-wrap.loading .toggle-track {
  animation: pulse 1s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

/* card-row2 wraps badges + bottom + action. On desktop it's transparent
   (display:contents) so children participate in the card's column flex directly.
   On mobile (≤480px) it becomes a flex-row for the two-row compact layout. */
.card-row2 {
  display: contents;
}

/* Issue #1429 — Mobile-only elements emitted by renderCard().
   Hidden by default on desktop; the ≤480px media block reveals them. */
.card-mobile-status,
.card-mobile-venue-cat,
.card-mobile-avail,
.card-mobile-cost,
.card-time-mobile {
  display: none;
}

.card-bottom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: auto;
}

.card-meta {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1;
  min-width: 0;
}

/* Spots meter */
.spots-meter {
  display: flex;
  align-items: center;
  gap: 8px;
}

.spots-bar-wrap {
  flex: 1;
  max-width: 100px;
  height: 6px;
  background: var(--bg-3);
  border-radius: 3px;
  overflow: hidden;
}

.spots-bar {
  height: 100%;
  border-radius: 3px;
  transition: width 0.5s ease;
}

.spots-text {
  font-size: 11px;
  color: var(--text-2);
  font-weight: 500;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.card-cost {
  font-size: 11px;
  color: var(--text-3);
  font-weight: 500;
}

.card-instructor {
  font-size: 11px;
  color: var(--text-2);
  display: flex;
  align-items: center;
  gap: 5px;
}

.instructor-avatar {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--bg-3);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 9px;
  color: var(--text-3);
}

/* Card inline action (mobile trailing-edge button) — hidden on desktop */
.card-action {
  display: none;
  flex-shrink: 0;
}

.card-action-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  font-size: 13px;
  font-weight: 600;
  border: none;
  cursor: pointer;
  transition: all var(--transition);
  flex-shrink: 0;
}

.card-action-btn.register {
  background: var(--green);
  color: #fff;
}

.card-action-btn.register:hover {
  background: var(--green-dim);
}

.card-action-btn.waitlist {
  background: rgba(234,179,8,0.18);
  color: var(--yellow);
  border: 1px solid rgba(234,179,8,0.35);
}

.card-action-btn.waitlist:hover {
  background: rgba(234,179,8,0.28);
}

.card-action-btn.full {
  background: var(--bg-3);
  color: var(--text-3);
  cursor: default;
}

.card-action-btn.registered {
  background: transparent;
  color: var(--green);
  border: 2px solid var(--green);
}

.card-action-btn.registered:hover {
  background: rgba(34,197,94,0.1);
}

.card-action-btn.auto-registered {
  background: transparent;
  color: var(--purple, #a855f7);
  border: 2px solid var(--purple, #a855f7);
}

.card-action-btn.auto-registered:hover {
  background: rgba(168,85,247,0.1);
}

/* Register button */
.btn-register {
  padding: 7px 16px;
  border-radius: var(--radius-full);
  font-size: 12px;
  font-weight: 600;
  transition: all var(--transition);
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  text-decoration: none;
}

.btn-register.register {
  background: var(--green);
  color: #fff;
}

.btn-register.register:hover {
  background: var(--green-dim);
  box-shadow: 0 4px 14px rgba(34,197,94,0.3);
}

.btn-register.waitlist {
  background: rgba(234,179,8,0.15);
  color: var(--yellow);
  border: 1px solid rgba(234,179,8,0.3);
}

.btn-register.waitlist:hover {
  background: rgba(234,179,8,0.25);
}

.btn-register.full {
  background: var(--bg-3);
  color: var(--text-3);
  cursor: default;
}

.btn-register.registered {
  background: rgba(34,197,94,0.12);
  color: var(--green);
  border: 1px solid rgba(34,197,94,0.3);
  cursor: pointer;
}
.btn-register.registered:hover {
  background: rgba(34,197,94,0.22);
}

.badge-auto {
  background: rgba(168, 85, 247, 0.12);
  color: #a855f7;
  border: 1px solid rgba(168, 85, 247, 0.25);
  /* Issue #1726 item 13: 11px floor. */
  font-size: 11px;
  padding: 2px 7px;
  border-radius: var(--radius-full);
  font-weight: 600;
}

/* Friend count / indicator badges (issue #223) */
.badge-friends {
  background: rgba(251, 191, 36, 0.12);
  color: #f59e0b;
  border: 1px solid rgba(251, 191, 36, 0.3);
  /* Issue #1726 item 13: 11px floor. */
  font-size: 11px;
  padding: 2px 7px;
  border-radius: var(--radius-full);
  font-weight: 600;
  /* Issue #3842 — friend star is hoverable for a tooltip (events.md
     § Friend stars). ``cursor:help`` advertises that affordance so users
     know to hover; positioning context lets the CSS popover (below) anchor
     to the badge. */
  cursor: help;
  position: relative;
}

/* Issue #3842 — CSS-only hover popover that shows friend names listed in
   the native ``title`` attribute (built by ``_friendBadgeTooltip()`` in
   ``main.js``).  Native ``title`` still fires for accessibility / mobile
   long-press, but browsers add a ~500ms delay and only render inside the
   viewport — the audit at 2026-06-26 couldn't reliably observe it.  This
   ``::after`` popover appears immediately on hover with no JS, and the
   ``white-space: pre-line`` rule renders the ``&#10;`` line breaks in the
   title as actual newlines (spec: events.md § Friend stars — "Hovering the
   star reveals the names of those friends"). */
/* Issue #4515 — tap-reveal: .tip-active is set by JS on touch devices so the
   first tap shows friend names rather than opening the modal. */
.badge-friends[title]:hover::after,
.badge-friends[title]:focus-visible::after,
.badge-friends[title].tip-active::after {
  content: attr(title);
  position: absolute;
  /* Anchor below the badge with a small gap. */
  top: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 1100;
  background: var(--bg-3, #1e293b);
  color: var(--text-0, #fff);
  border: 1px solid var(--border, rgba(148, 163, 184, 0.25));
  border-radius: var(--radius-sm, 6px);
  padding: 6px 10px;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.35;
  /* Names list can be long; allow up to ~280px before wrapping. */
  max-width: 280px;
  white-space: pre-line;
  text-align: left;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
  pointer-events: none;
  /* Small fade-in. */
  opacity: 0;
  animation: friend-tooltip-fade 0.12s ease-out forwards;
}

@keyframes friend-tooltip-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Booking provenance badges (issue #1865) — three kinds:
   • autobook       (we booked it via a rule)
   • external       (user booked manually, rule would have matched)
   • provider-sync  (discovered via calendar sync, no rule fired) */
.badge-source {
  display: inline-flex;
  align-items: center;
  font-size: 11px;
  padding: 2px 7px;
  border-radius: var(--radius-full);
  font-weight: 600;
  white-space: nowrap;
}
.badge-source-autobook {
  background: rgba(34, 197, 94, 0.12);
  color: #16a34a;
  border: 1px solid rgba(34, 197, 94, 0.3);
}
.badge-source-external {
  background: rgba(148, 163, 184, 0.12);
  color: var(--text-2);
  border: 1px solid rgba(148, 163, 184, 0.3);
}
.badge-source-provider-sync {
  background: rgba(148, 163, 184, 0.12);
  color: var(--text-2);
  border: 1px solid rgba(148, 163, 184, 0.3);
}

/* Booking row status pill (issue #2941) — inline ${color}20 tints made axe-core
   flag yellow-on-yellow in light theme (~1.8:1). Solid rgba backgrounds plus
   theme-aware foregrounds meet WCAG 2 AA on --bg-card. */
.booking-status-badge {
  display: inline-block;
  font-size: 10px;
  padding: 2px 6px;
  border-radius: var(--radius-full);
  font-weight: 600;
  text-transform: uppercase;
}
.booking-status-badge--confirmed {
  background: rgba(34, 197, 94, 0.15);
  color: #15803d;
  border: 1px solid rgba(34, 197, 94, 0.3);
}
[data-theme="dark"] .booking-status-badge--confirmed {
  color: var(--green);
}
.booking-status-badge--pending {
  background: rgba(234, 179, 8, 0.15);
  color: #854d0e;
  border: 1px solid rgba(234, 179, 8, 0.3);
}
[data-theme="dark"] .booking-status-badge--pending {
  color: var(--yellow);
}

.participant-friend {
  color: #f59e0b;
  font-size: 13px;
}
/* Issue #688 — Add-friend / star button in event modal roster.
   Compact pill on desktop; ≥44px tap target on mobile. */
.roster-add-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  min-height: 28px;
  padding: 4px 10px;
  border-radius: var(--radius-full);
  font-size: 11px;
  font-weight: 600;
  font-family: var(--font);
  cursor: pointer;
  border: 1px solid rgba(251, 191, 36, 0.4);
  background: rgba(251, 191, 36, 0.08);
  color: #f59e0b;
  transition: background 0.15s, transform 0.05s;
  white-space: nowrap;
}
.roster-add-btn:hover { background: rgba(251, 191, 36, 0.18); }
.roster-add-btn:active { transform: scale(0.97); }
.roster-add-btn[data-busy="1"] { opacity: 0.6; cursor: default; }
.roster-row { min-height: 24px; }
@media (max-width: 768px) {
  .roster-add-btn {
    min-height: 44px;
    min-width: 44px;
    padding: 6px 14px;
    font-size: 13px;
  }
  .roster-row { min-height: 44px; }
  .participant-friend.roster-star {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    font-size: 18px;
  }
}

.btn-auto-register {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  font-size: 11px;
  font-weight: 600;
  cursor: pointer;
  border: 1px solid rgba(168, 85, 247, 0.3);
  background: rgba(168, 85, 247, 0.1);
  color: #a855f7;
  font-family: var(--font);
  transition: var(--transition);
}
.btn-auto-register:hover {
  background: rgba(168, 85, 247, 0.2);
}

/* ── Skeleton Loading ──────────────────────────────────────────────────── */
.skeleton-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 14px;
}

@media (max-width: 720px) {
  .skeleton-grid { grid-template-columns: 1fr; }
}

.skeleton-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.skeleton-line {
  height: 14px;
  border-radius: 7px;
  background: var(--skeleton);
  position: relative;
  overflow: hidden;
}

.skeleton-line::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, var(--skeleton-shine), transparent);
  animation: shimmer 1.5s infinite;
}

.skeleton-line.w30 { width: 30%; }
.skeleton-line.w50 { width: 50%; }
.skeleton-line.w70 { width: 70%; }
.skeleton-line.w90 { width: 90%; }
.skeleton-line.h8  { height: 8px; }
.skeleton-line.h20 { height: 20px; }
.skeleton-line.h24 { height: 24px; }

.skeleton-list { display: flex; flex-direction: column; gap: 6px; }

@keyframes shimmer {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

/* ── Empty & Error States ──────────────────────────────────────────────── */
.state-message {
  text-align: center;
  padding: 60px 20px;
}

.state-icon {
  font-size: 48px;
  margin-bottom: 16px;
  opacity: 0.6;
}

.state-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--text-0);
  margin-bottom: 6px;
}

.state-desc {
  font-size: 14px;
  color: var(--text-2);
  margin-bottom: 20px;
  max-width: 400px;
  margin-inline: auto;
}

.btn-retry {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 10px 24px;
  /* Issue #4559/#4560 — WCAG 2.5.5 tap-target minimum. The "Book a Future
     Event" CTA was 155×27px at default layout.  min-height:44px ensures
     the button meets the 44×44px guideline across all viewport widths. */
  min-height: 44px;
  border-radius: var(--radius-full);
  background: var(--green);
  /* #2673 — #fff on --green (#22c55e) was ~2.76:1 (WCAG AA fail).
     Dark text #04210f on --green is 7.5:1, matching the .seg-btn.active
     fix from #2051/#2052/#2053. */
  color: #04210f;
  font-size: 14px;
  font-weight: 600;
  transition: all var(--transition);
}

.btn-retry:hover {
  background: var(--green-dim);
  box-shadow: 0 4px 20px rgba(34,197,94,0.3);
}

/* ── Toast ─────────────────────────────────────────────────────────────── */
.toast-container {
  position: fixed;
  bottom: 24px;
  right: 24px;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none;
}

.toast {
  background: var(--toast-bg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 12px 16px;
  font-size: 13px;
  color: var(--text-1);
  display: flex;
  align-items: center;
  gap: 8px;
  box-shadow: 0 8px 30px rgba(0,0,0,0.25);
  pointer-events: auto;
  animation: toastIn 0.3s ease forwards;
  max-width: 360px;
}

.toast.leaving {
  animation: toastOut 0.25s ease forwards;
}

.toast i {
  font-size: 16px;
}

.toast.success i { color: var(--green); }
.toast.error i   { color: var(--red); }
.toast.info i    { color: var(--blue); }

@keyframes toastIn {
  from { opacity: 0; transform: translateX(40px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes toastOut {
  to { opacity: 0; transform: translateX(40px); }
}

/* ── Refresh Indicator ─────────────────────────────────────────────────── */
.refresh-bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 8px;
  font-size: 12px;
  color: var(--text-3);
}

.refresh-bar button {
  color: var(--green);
  font-size: 12px;
  font-weight: 600;
}

.refresh-bar button:hover { text-decoration: underline; }

.spin {
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* ── Nav Tab Base Styles ───────────────────────────────────────────────── */
/* Container layout was previously declared inline on the <nav id="nav-tabs">
   element in dashboard.html. That blocked all mobile responsive overrides
   (inline always wins over external CSS regardless of media query), which
   left ~8px of horizontal page overflow at 375px wide on phones and made
   none of the mobile padding/gap tweaks land — issue #1420. The layout
   now lives here so the ≤768px / ≤480px media queries below can win. */
/* Outer wrapper provides sticky positioning and the fade-affordance overlay
   on mobile. The inner <nav> handles overflow-x:auto scrolling so that the
   ::after pseudo-element on this wrapper stays fixed at the right edge and
   is not scrolled away with tab content (issue #1715). */
.nav-tabs-outer {
  position: sticky;
  /* Issue #1726 item 1: stays just below the topbar at every viewport
     because --topbar-h adapts.
     Issue #1712: also offset by --banner-h when the admin tier-
     impersonation banner is visible (0 in normal sessions). */
  top: calc(var(--topbar-h) + var(--banner-h, 0px));
  z-index: 99;
  background: var(--bg-1);
  border-bottom: 1px solid var(--border);
  /* Issues #2371, #2373, #2376, #2377, #2380, #2381: tab buttons inside the inner
     #nav-tabs scroll container can bleed visually past this wrapper at
     narrow viewports (iPhone SE 375px, iPhone 11 414px). `clip` (not
     `hidden`) prevents that without creating a new scroll container, so
     the inner overflow-x:auto swipe-to-scroll affordance is preserved. */
  overflow-x: clip;
  /* Issue #3072: hard-cap at viewport width so the wrapper's layout box
     cannot grow past 375px even when flex children extend in scroll space. */
  max-width: 100vw;
}

.nav-tabs {
  display: flex;
  gap: 4px;
  padding: 0 20px;
  /* Active-tab snap-into-view (see main.js _scrollActiveTabIntoView)
     uses scroll-padding to keep an inline-start gap when the active
     tab is scrolled into view at ≤768px. */
  scroll-padding-inline: 16px;
  /* Issue #4412: explicit width:100% (not just max-width) ensures the flex
     scroll container is always sized to its containing block width, not to
     its content width. Without this, some browsers resolve width:auto on a
     flex container to fit-content rather than fill-available, which makes the
     container wider than the viewport and suppresses overflow-x scrolling.
     max-width:100% is kept as a second guard against overflow. */
  width: 100%;
  max-width: 100%;
  /* Issue #4243: prevent tab labels from word-wrapping inside flex items;
     ensures all tabs stay in a single row and the container overflows
     horizontally (scrolled via overflow-x:auto at ≤768px below). */
  white-space: nowrap;
  /* Issue #4392: apply flex-wrap:nowrap + overflow-x:auto at all widths
     so the scroll affordance is always present regardless of which media-
     query block the browser is in.  At desktop the tabs fit and
     overflow-x:auto produces no scrollbar; at mobile the excess tabs
     scroll into view.  -webkit-overflow-scrolling:touch enables iOS
     momentum scrolling. scrollbar-width:none + ::-webkit-scrollbar hide
     the native scrollbar on all browsers (swipe/drag is still usable). */
  flex-wrap: nowrap;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.nav-tabs::-webkit-scrollbar { display: none; }

/* Issue #4392: prevent every nav-tab flex item from shrinking below its
   natural content width; ensures all tabs stay reachable in the scroll
   area.  Applied at the base level so no mobile media query can silently
   override it via cascade ordering. */
.nav-tab {
  padding: 10px 18px;
  border: none;
  background: none;
  color: var(--text-2);
  font-weight: 500;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  font-family: var(--font);
  font-size: 14px;
  scroll-margin-inline: 8px;
  flex-shrink: 0;
  white-space: nowrap;
  min-width: max-content;
}

/* Issue #4392: paywall-locked wrappers around Friends / Cost History tabs
   are direct flex children of #nav-tabs.  Without flex-shrink:0 here the
   wrapper can shrink, pushing later tabs (Activity, Calendar Sync) past
   the scroll viewport edge.  Applied at the base level alongside the
   .nav-tab rule above so it holds at every breakpoint. */
.paywall-locked:has(.nav-tab) {
  flex-shrink: 0;
  min-width: max-content;
}
.nav-tab.active {
  color: var(--text-1);
  font-weight: 600;
  border-bottom-color: var(--green);
}

/* ── Responsive ────────────────────────────────────────────────────────── */

/* Issue #784: at the canonical tablet breakpoint (≤1024px) hide topbar
   elements that an admin user would otherwise stack alongside 9 icon
   buttons (totalling ~1190px) and overflow viewports up to ~1000px:
   the events/total stats, user-email span, clock, and the three
   "view" power-user toggles (view-mode, density, auto-refresh).
   These remain reachable on desktop (>1024px) where space allows. */
@media (max-width: 1024px) {
  .topbar { gap: 8px; }
  .topbar-actions { gap: 4px; }
  .topbar-actions > div { gap: 4px !important; }
  .topbar-stats { display: none; }
  .topbar-actions > div > span[title] { display: none; }
  .topbar-clock { display: none; }
  .topbar-view-controls { display: none; }
}

@media (max-width: 720px) {
  .topbar { padding: 0 14px; }
  .topbar-stats { display: none; }
  .topbar-brand h1 { font-size: 15px; }
  .filter-bar { padding: 10px 14px; gap: 8px; }
  .filter-label { min-width: auto; }
  .search-kbd { display: none; }
  /* Issue #4126 — clip overflow-x on the main content area so that any
     element wider than the viewport (e.g. event-cards or filter-bar content
     that can overflow now that .event-card uses overflow:visible for tooltip
     support, issue #4051) cannot inflate body.scrollWidth at ≤414px.
     Use `clip` not `hidden` so .main does not become a scroll container
     (which would break position:sticky on .filter-bar and .day-header).
     `contain: inline-size` mirrors the fix applied to .nav-tabs-outer
     (issue #3073/#3616) — suppresses the intrinsic inline size so that
     overflowing children do not contribute to documentElement.scrollWidth. */
  .main { padding: 16px 12px 60px; overflow-x: clip; contain: inline-size; }
  .day-title { font-size: 18px; }
  /* Issue #1712: was hard-coded top:108px which (a) ignored --topbar-h
     adaptations and (b) didn't compensate for the impersonation banner.
     Switch to the same calc as desktop so all three variables drive it. */
  /* Issue #2122 + #2398: the sticky day-header's full-bleed background
     bleeds -20px to cover the desktop .main gutter. On mobile .main
     padding drops to 12px (line above), so a -20px bleed extends 8px
     past each viewport edge, inflating body.scrollWidth by 8px (flagged
     as horizontal overflow on the Events tab). Match the bleed to the
     12px mobile gutter so it still reaches the viewport edges without
     overflowing. (The bleed lives directly on .day-header now — see
     #2398 — so override negative margin / horizontal padding here
     rather than on a ::before pseudo.) */
  .day-header {
    top: calc(var(--topbar-h) + var(--navtabs-h) + var(--banner-h, 0px));
    margin: 0 -12px;
    padding-left: 12px;
    padding-right: 12px;
  }
  .seg-btn {
    padding: 5px 10px;
    font-size: 11px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
}

/* ── Mobile (≤768px) ──────────────────────────────────────────────────── */
@media (max-width: 768px) {
  /* 40px, not 44: admin's 7+ topbar icons must fit ~609px viewports
     without horizontal page scroll (Issue #784; ≤480px shrinks further to
     36px per #1091 — do not raise these without revisiting both). */
  .icon-btn {
    width: 40px;
    height: 40px;
    font-size: 16px;
  }

  /* Tighten topbar padding (gap rules already applied by ≤1024px tier). */
  .topbar { padding: 0 12px; }

  /* Issue #2091 — the admin tier-impersonation <select> renders ~27px tall
     from its inline font/padding, below the WCAG 2.5.5 / Apple HIG 44px
     minimum tap target on touch viewports. Enforce a 44px hit area here
     (min-height is not set inline, so this rule applies cleanly). */
  #admin-tier-impersonate {
    min-height: 44px;
  }

  /* Nav tabs: scroll horizontally, shrink text.
     Issue #2378: contain the #nav-tabs horizontal scroll area so tab
     buttons in the scroll overflow region do not extend beyond the
     viewport at the page-layout level.
     Issue #2556: use overflow-x:clip (not overflow:hidden) so the outer
     wrapper does not become a scroll container, which would break
     position:sticky on .nav-tabs-outer itself and the ::after fade
     affordance. clip clips without creating a new scroll container.
     Issue #3073: `contain: inline-size` constrains the wrapper's
     intrinsic inline size so child scroll content cannot inflate
     documentElement.scrollWidth at ≤414 px viewports. */
  /* Issue #3616: explicit width:100% ensures the sticky wrapper and its
     scroll-container child fill the full viewport width at 375px and do
     not leave a gap or produce residual overflow. max-width alone is
     insufficient when contain:inline-size is present, because the element's
     intrinsic size is suppressed and some engines resolve width:auto to 0
     instead of fill-available. */
  .nav-tabs-outer { overflow-x: clip; contain: inline-size; width: 100%; }
  /* Issue #4243 / #4262 / #4263 / #4295 / #4325 / #4391: apply horizontal
     scroll to .nav-tabs so that all tabs that overflow at narrow mobile
     widths (375px, 414px — friends, costs, activity, calendar-sync) are
     reachable by swiping. -webkit-overflow-scrolling:touch for iOS
     momentum scrolling. flex-wrap:nowrap (explicit; default is nowrap
     but asserting it here prevents any cascade surprise from inner
     .paywall-locked flex items that could otherwise force a second row).
     white-space:nowrap keeps tab label text on one line. scrollbar-width
     :none hides the native scrollbar on both the class and id selectors
     so neither spec-path produces a visible scrollbar on mobile.
     flex-shrink:0 on .nav-tab / .paywall-locked:has(.nav-tab) asserted
     here (primary mobile block) so tabs never compress their natural
     content width and overflow-x:auto scroll area is computed correctly.
     Issue #4391: consolidate flex-shrink:0 into this block so it is
     co-located with the overflow-x:auto + flex-wrap:nowrap that depend
     on it, rather than relying on a later ≤768px block being present. */
  .nav-tabs {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    flex-wrap: nowrap;
    white-space: nowrap;
    scrollbar-width: none;
  }
  .nav-tabs::-webkit-scrollbar { display: none; }
  #nav-tabs {
    padding: 0 8px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    flex-wrap: nowrap;
    scrollbar-width: none;
    gap: 0;
    width: 100%;
    max-width: 100%;
  }
  #nav-tabs::-webkit-scrollbar { display: none; }
  /* Issue #4391: flex-shrink:0 anchored in the primary mobile block so
     tab buttons and their paywall wrappers never compress below natural
     content width — prerequisite for overflow-x:auto scrollability. */
  .nav-tab { flex-shrink: 0; }
  .paywall-locked:has(.nav-tab) { flex-shrink: 0; }
}

/* Issue #4294 / #4295 — at the narrowest phone widths (≤414px covers
   iPhone SE 375px and iPhone 6/7/8 414px) the four rightmost tabs
   (Friends, Costs, Activity, Calendar Sync) are off-screen.  The
   ≤768px block above already applies overflow-x:auto; this block
   reinforces it at the exact widths that the audit checks, giving an
   additional specificity anchor so nothing can silently override it
   between 415px and 768px.
   Issue #4338: costs/activity/calendar-sync tabs off-screen at 414px.
   .paywall-locked:has(.nav-tab) is a flex child of #nav-tabs; without
   explicit flex-shrink:0 here the paywall wrapper can shrink and push
   later tabs off-screen. Duplicate the rule from the ≤768px block so
   the ≤414px block is self-contained regardless of cascade order. */
@media (max-width: 414px) {
  /* Issue #4413: ensure .nav-tabs-outer fills viewport width and clips
     bleed without creating a scroll container (overflow-x:clip, not hidden).
     contain:inline-size suppresses intrinsic size so overflowing scroll
     children cannot inflate documentElement.scrollWidth. */
  .nav-tabs-outer { overflow-x: clip; contain: inline-size; width: 100%; }
  /* Issue #4413: explicit overflow-x:auto + width:100% on the class
     selector so the scroll container is established at this breakpoint
     without depending on the higher-specificity #nav-tabs rule alone.
     flex-wrap:nowrap keeps all tabs in a single row; white-space:nowrap
     keeps label text on one line; -webkit-overflow-scrolling:touch
     enables iOS momentum scroll. */
  .nav-tabs {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    white-space: nowrap;
    scrollbar-width: none;
    flex-wrap: nowrap;
    width: 100%;
    max-width: 100%;
  }
  .nav-tabs::-webkit-scrollbar { display: none; }
  #nav-tabs {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    width: 100%;
    max-width: 100%;
    flex-wrap: nowrap;
  }
  #nav-tabs::-webkit-scrollbar { display: none; }
  .nav-tab {
    white-space: nowrap;
    flex-shrink: 0;
  }
  /* Issue #4338: paywall wrapper around costs/friends tabs must not
     shrink — keeps tab at its natural content width in scroll container. */
  .paywall-locked:has(.nav-tab) {
    flex-shrink: 0;
  }
}

/* Issue #4412 — explicit 375px anchor (iPhone SE / narrowest supported phone).
   The ≤768px, ≤480px, and ≤414px blocks above all apply here, but this block
   acts as a definitive final-layer guard at the exact viewport width the audit
   checks. Declared last so it wins the cascade for any conflicting property
   without needing !important.
   Key properties that together guarantee scrollability:
     - .nav-tabs-outer: overflow-x:clip + contain:inline-size + width:100%
       clip prevents visual bleed without becoming a scroll container;
       contain:inline-size caps the outer's intrinsic size so children can't
       inflate documentElement.scrollWidth; width:100% fills the viewport.
     - #nav-tabs: overflow-x:auto — scroll container; width:100% — fills
       outer; flex-wrap:nowrap — single row so items overflow horizontally.
     - .nav-tab / .paywall-locked:has(.nav-tab): flex-shrink:0 — items
       never compress, preserving overflow so auto scroll activates. */
@media (max-width: 375px) {
  .nav-tabs-outer { overflow-x: clip; contain: inline-size; width: 100%; }
  .nav-tabs {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    flex-wrap: nowrap;
    white-space: nowrap;
    scrollbar-width: none;
    width: 100%;
  }
  .nav-tabs::-webkit-scrollbar { display: none; }
  #nav-tabs {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    flex-wrap: nowrap;
    scrollbar-width: none;
    width: 100%;
    max-width: 100%;
  }
  #nav-tabs::-webkit-scrollbar { display: none; }
  .nav-tab {
    white-space: nowrap;
    flex-shrink: 0;
  }
  .paywall-locked:has(.nav-tab) {
    flex-shrink: 0;
  }
}

/* Issue #4324 — explicit ≤480px anchor so the horizontal-scroll fix for
   nav-tabs has a dedicated narrow-phone breakpoint. The ≤768px block above
   already applies overflow-x:auto and the ≤414px block reinforces it; this
   block bridges the 415px–480px gap and provides a clear, named target for
   375px / 390px / 414px / 430px phone viewports that all need scrollable tabs.
   Keeps the outer wrapper clipped (overflow-x:clip — not hidden — so sticky
   positioning and the inner scroll container are unaffected). */
@media (max-width: 480px) {
  .nav-tabs-outer { overflow-x: clip; contain: inline-size; width: 100%; }
  .nav-tabs {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    flex-wrap: nowrap;
    white-space: nowrap;
    scrollbar-width: none;
  }
  .nav-tabs::-webkit-scrollbar { display: none; }
  #nav-tabs {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    flex-wrap: nowrap;
    scrollbar-width: none;
    width: 100%;
    max-width: 100%;
  }
  #nav-tabs::-webkit-scrollbar { display: none; }
  .nav-tab {
    white-space: nowrap;
    flex-shrink: 0;
  }
}

@media (max-width: 768px) {
  /* Issue #1715: right-edge fade gradient signals hidden tabs to the right.
     Applied on .nav-tabs-outer (the sticky wrapper), not on #nav-tabs itself,
     so the overlay stays fixed at the viewport edge and does not scroll away
     with the tab content. `position: sticky` already creates a containing
     block for the absolute ::after child — no extra `position: relative`
     needed. `bottom: 1px` ensures it sits above the 1px border-bottom.
     Issue #2531: opacity is JS-driven via `.at-end`/`.at-start` classes so
     the affordance only appears when there is content to scroll TO in that
     direction. Default opacity:1 keeps the right fade visible at first
     paint before JS toggles it (and on no-JS / SSR rendering). */
  .nav-tabs-outer::after {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    bottom: 1px;
    width: 48px;
    background: linear-gradient(to right, transparent, var(--bg-1));
    pointer-events: none;
    z-index: 1;
    opacity: 1;
    transition: opacity 150ms ease;
  }
  /* Issue #2531: hide right fade when the nav has been scrolled to the end
     (no more tabs to reveal on the right). JS toggles `.at-end` based on
     `scrollLeft + clientWidth >= scrollWidth - 4`. */
  .nav-tabs-outer.at-end::after {
    opacity: 0;
  }

  /* Issue #2531: symmetric left-edge fade appears once the user has
     scrolled past the start, signalling tabs are hidden to the left. */
  .nav-tabs-outer::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    bottom: 1px;
    width: 48px;
    background: linear-gradient(to left, transparent, var(--bg-1));
    pointer-events: none;
    z-index: 1;
    opacity: 0;
    transition: opacity 150ms ease;
  }
  .nav-tabs-outer.scrolled::before {
    opacity: 1;
  }

  .nav-tab {
    padding: 10px 12px;
    font-size: 12px;
    min-height: 44px;
    white-space: nowrap;
    flex-shrink: 0;
  }
  /* Issue #4325: .paywall-locked:has(.nav-tab) is a direct flex child of
     #nav-tabs (wraps the Friends / Cost History tabs for non-entitled tiers).
     flex-shrink:0 prevents the wrapper from shrinking, keeping the tab's
     natural content width so overflow-x:auto scroll area is correct. */
  .paywall-locked:has(.nav-tab) {
    flex-shrink: 0;
  }

  /* Issue #782: at narrow widths, allow chip groups within a compound
     .filter-row (Day+Time, Range+MinSpots, etc.) to wrap onto a new
     line so all controls remain visible without horizontal scrolling.
     The previous flex-wrap:nowrap + overflow-x:auto approach hid
     right-side groups (Afternoon/Evening, date-range inputs) with no
     scroll affordance. Individual chip groups (Venue, Category) still
     wrap inside themselves via their child container's flex-wrap. */
  .filter-row {
    flex-wrap: wrap;
    row-gap: 8px;
    padding-bottom: 2px;
  }

  /* Issue #680: reserve space for absolute collapse toggle so first row
     text/inputs aren't clipped behind the "Filters" button. */
  .filter-bar { padding-top: 36px; }
  .filter-bar.collapsed { padding-top: 36px; padding-bottom: 8px; }

  /* Issue #1718: on mobile, pin the filter bar below the topbar + nav-tabs
     so it stays visible as users scroll a long event list. Uses CSS custom
     properties set by JS (--topbar-h, --navtabs-h via trackStickyStackHeights
     from #1726) so the offset adapts when the topbar wraps at ≤480px.
     Existing z-index: 90 sits above .day-header (50) and below .nav-tabs (99)
     / .topbar (100), so the visual stacking is correct. */
  .filter-bar {
    position: sticky;
    top: calc(var(--topbar-h) + var(--navtabs-h));
  }

  /* Issues #680, #765: day-of-week chips should render as a single
     horizontal chip row (wrapping across rows only when the viewport
     truly cannot fit them). The parent .filter-row uses
     flex-wrap:nowrap + overflow-x:auto on mobile, which can collapse
     this wrapper to its min-content width. For a flex container,
     min-content == the widest single child (~one chip), which would
     force the 7 buttons to stack one per line. Use max-content so
     the group keeps its natural single-row width and the parent row
     scrolls horizontally instead. flex-shrink:0 reinforces this. */
  #dow-chips {
    flex-wrap: wrap !important;
    flex: 0 0 auto;
    flex-shrink: 0;
    min-width: max-content;
    row-gap: 4px;
  }

  /* Issue #1706: enforce 44px minimum touch targets on event-filter chips
     (WCAG 2.5.5 / Apple HIG). Default desktop sizes are 22–28px tall, which
     is too small for thumb taps on mobile. Use inline-flex centering so the
     visible text/number stays centered while the hit area expands. */
  .spots-chip {
    min-height: 44px;
    min-width: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .dow-chip {
    min-height: 44px;
    min-width: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .time-preset {
    min-height: 44px;
    min-width: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .seg-btn {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* Issue #2904 / #2908 — My Bookings filter chips (All/Upcoming/Past/Cancelled)
     are ~27px tall on desktop; expand to 44×44px tap targets on touch
     viewports (WCAG 2.5.5 / Apple HIG). */
  .bookings-filter-btn {
    min-height: 44px;
    min-width: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* Search should always be full width */
  .search-wrap {
    max-width: 100%;
    min-width: 160px;
  }

  /* 16px prevents iOS Safari from zooming on input focus.
     Issue #2481 — WCAG 2.5.5: also enforce 44px minimum tap target height. */
  .search-wrap input,
  .time-custom input[type="time"],
  .date-range-wrap input[type="date"] {
    font-size: 16px;
    min-height: 44px;
  }

  /* Issue #2481 — WCAG 2.5.5: time/date clear buttons are ~19px on desktop;
     expand to 44×44px tap target on touch viewports. */
  .time-custom .time-clear,
  .date-range-wrap .date-clear {
    min-height: 44px;
    min-width: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }

  /* Segmented control — scroll if overflow */
  .seg-control {
    flex-shrink: 0;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }
  .seg-control::-webkit-scrollbar { display: none; }

  /* Modal: full-width, slide up from bottom */
  .modal-backdrop {
    align-items: flex-end;
    padding: 0;
  }
  .modal-panel {
    max-width: 100%;
    border-radius: var(--radius-xl) var(--radius-xl) 0 0;
    max-height: 90vh;
  }
  .modal-backdrop.open .modal-panel {
    transform: scale(1) translateY(0);
  }
  .modal-backdrop .modal-panel {
    transform: scale(1) translateY(100%);
  }

  /* Register button — full width in card bottom */
  .btn-register {
    min-height: 44px;
    padding: 10px 16px;
  }

  /* Toast from bottom-center */
  .toast-container {
    bottom: 16px;
    right: 12px;
    left: 12px;
  }
  .toast {
    max-width: 100%;
  }

  /* Dashboard footer — stack vertically */
  .dashboard-footer {
    flex-direction: column;
    gap: 8px;
    padding: 12px 16px;
    text-align: center;
  }
  .footer-right { flex-wrap: wrap; justify-content: center; }
  .footer-kbd { display: none; }

  /* Prevent horizontal scroll on tablets and mobile */
  body { overflow-x: clip; }
  html { overflow-x: clip; }
}

/* ── Small Mobile (≤480px) ────────────────────────────────────────────── */
/* Target: ≥15 event rows visible in ~900px Pixel 10A viewport.
   Strategy: single-row card layout ~44px tall (WCAG tappable minimum).
   All secondary detail (category, cost, instructor, calendar badge)
   moves to the modal, which opens on card tap. */
@media (max-width: 480px) {
  /* Topbar — tighter. Issue #784: hide the brand wordmark and shrink
     icon buttons to 36px so logo + admin's icon-button row fit without
     horizontal overflow at small-phone widths.

     Issue #1091: at iPhone-class widths (≤480px) the admin action row
     (billing + bell + tier-select + shield + users-gear + plug-circle-bolt
     + logout, ~9 icons + a select) still exceeds available width even
     with shrunk icons. Allow the topbar action rows to wrap onto a
     second line and let the topbar grow vertically. Also hide the
     user-email pill explicitly (defense-in-depth alongside the
     ≤1024px rule above) so it can't reintroduce overflow. */
  .topbar { padding: 0 8px; min-height: 56px; height: auto; gap: 4px; flex-wrap: wrap; row-gap: 4px; }
  #btn-mobile-menu { display: flex; margin-left: auto; }
  .topbar-actions { display: none; width: 100%; order: 3; gap: 2px; flex-wrap: wrap; row-gap: 4px; justify-content: flex-end; }
  .topbar.menu-open .topbar-actions { display: flex; }
  .topbar-actions > div { flex-wrap: wrap; row-gap: 4px; justify-content: flex-end; }
  .topbar-actions > div > span[title] { display: none !important; }
  .topbar-brand .logo { font-size: 22px; }
  .topbar-brand h1 { display: none; }
  /* Issue #3884/#3885 — brand logo link shrinks to ~23×22 px at ≤480 px once
     h1 is hidden; promote hit-area to 44×44 (WCAG 2.5.5 / Apple HIG). */
  .topbar-brand {
    min-height: 44px;
    min-width: 44px;
    display: inline-flex;
    align-items: center;
  }
  .icon-btn { width: 44px; height: 44px; font-size: 15px; }

  /* Issue #1091: rule action row (Run / History / Edit / Pause / Trash)
     also overflows at narrow widths. The actions row uses inline
     flex-shrink:0 which would let it extend past its parent card; force
     the outer .rule-row to wrap so actions drop to a new line, and let
     the buttons themselves wrap inside that row too. */
  .rule-row { flex-wrap: wrap; row-gap: 8px; }
  .rule-actions { flex-wrap: wrap; row-gap: 4px; }

  /* Clock — hide to save space */
  .topbar-clock { display: none; }

  /* Issue #4192 — .topbar-view-controls (wrapping view-mode, density,
     auto-refresh) is hidden at ≤1024px; restore it here so the three
     buttons are always visible on narrow viewports without requiring the
     hamburger menu to be opened.  The container sits outside
     .topbar-actions so it is not affected by the collapse/reveal cycle. */
  .topbar-view-controls { display: flex; gap: 2px; }

  /* Filter label hidden — save horizontal space */
  .filter-label { display: none; }

  /* ── Issue #1429 — strict 2-row event card layout ── */

  /* Tighter grid and day-group spacing */
  .events-grid { gap: 4px; }
  .day-group { margin-bottom: 12px; }
  /* Issue #1726 item 1: top offset now derived from --topbar-h +
     --navtabs-h (set via JS in trackStickyStackHeights); no more
     hard-coded 100 px that broke admin's wrapped topbar.
     Issue #2398 follow-up: use vertical longhand so the inherited
     horizontal padding (12px from the ≤768px rule that matches the
     -12px mobile bleed) is preserved; padding shorthand would zero
     it out and misalign the day-header content with event cards. */
  .day-header { padding-top: 8px; padding-bottom: 5px; }
  .day-title { font-size: 14px; }
  .day-relative { font-size: 11px; }
  /* Issue #1726 item 13: 9 → 11 px. */
  .day-count { font-size: 11px; padding: 1px 7px; }

  /* Card: two-row layout, ~56–64px tall */
  .event-card {
    flex-direction: column;
    align-items: stretch;
    padding: 6px 10px 6px 16px;
    gap: 2px;
    min-height: 56px;
    border-radius: var(--radius-sm);
  }

  .event-card::before {
    top: 4px;
    bottom: 4px;
    width: 3px;
  }

  /* card-top is a passthrough wrapper around card-row1 on mobile */
  .card-top {
    flex: unset;
    min-width: 0;
    align-items: unset;
    gap: 0;
    flex-direction: unset;
  }

  /* ── Row 1 — [Time] [Event Name ···] [Status badge] ── */
  .card-row1 {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 8px;
    min-width: 0;
    overflow: hidden;
  }

  .card-time {
    font-size: 13px;
    font-weight: 700;
    white-space: nowrap;
    flex-shrink: 0;
    line-height: 1.2;
    letter-spacing: 0;
    color: var(--text-0);
    font-variant-numeric: tabular-nums;
    width: 72px;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  /* Swap the time text: show start-only span, hide the full range. */
  .card-time .card-time-full { display: none; }
  .card-time .card-time-mobile { display: inline; }

  .card-name {
    font-size: 14px;
    font-weight: 500;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    display: block;
    flex: 1;
    min-width: 0;
    color: var(--text-0);
  }

  /* Mobile-only single status pill at right end of Row 1 */
  .card-mobile-status {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    /* Issue #1726 item 13: 10 → 11 px (content-bearing pill). */
    font-size: 11px;
    font-weight: 600;
    line-height: 1.2;
    padding: 2px 7px;
    border-radius: var(--radius-full);
    border: none;
    white-space: nowrap;
    text-transform: none;
    letter-spacing: 0;
    max-width: 38vw;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .card-mobile-status.mobile-status-registered {
    background: rgba(34,197,94,0.12);
    color: var(--green);
  }
  .card-mobile-status.mobile-status-queued {
    background: rgba(234,179,8,0.12);
    color: #eab308;
  }
  .card-mobile-status.mobile-status-covered {
    background: rgba(99,102,241,0.12);
    color: #6366f1;
  }
  .card-mobile-status.mobile-status-within24h {
    background: rgba(239,68,68,0.12);
    color: #ef4444;
  }
  .card-mobile-status.mobile-status-preview {
    background: rgba(156,163,175,0.12);
    color: var(--text-2);
  }
  .card-mobile-status.mobile-status-full {
    background: rgba(156,163,175,0.12);
    color: var(--text-3);
  }

  /* ── Row 2 — [Venue · Cat ···] [Avail bar] [Cost] [Action] ── */
  .card-row2 {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 8px;
    min-width: 0;
    overflow: hidden;
  }

  /* Hide the desktop chip-bag entirely on mobile — venue+category render
     as inline text via .card-mobile-venue-cat instead. */
  .card-badges {
    display: none;
  }

  /* Mobile-only "Venue · Category" inline text */
  .card-mobile-venue-cat {
    display: block;
    flex: 1;
    min-width: 0;
    font-size: 12px;
    font-weight: 400;
    line-height: 1.2;
    color: var(--text-2);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* Mobile-only availability fill bar (compressed, right of venue text) */
  .card-mobile-avail {
    display: block;
    flex: 1;
    max-width: 60px;
    height: 3px;
    background: var(--bg-3);
    border-radius: 2px;
    overflow: hidden;
    flex-shrink: 1;
  }
  .card-mobile-avail-fill {
    display: block;
    height: 100%;
    border-radius: 2px;
  }

  /* Mobile-only cost — only rendered when ev.cost is truthy */
  .card-mobile-cost {
    display: inline-block;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-1);
    flex-shrink: 0;
    white-space: nowrap;
  }

  /* Card-bottom (desktop spots meter / cost / instructor) hidden on mobile */
  .card-bottom { display: none; }
  .card-meta { display: none; }
  .spots-bar-wrap { display: none; }
  .spots-text { display: none; }
  .card-cost { display: none; }
  .card-instructor { display: none; }

  /* Action button at trailing edge of Row 2 — 32px visible pill,
     padding gives ~44px effective tap target on the parent card. */
  .card-action {
    display: flex;
    align-items: center;
    flex-shrink: 0;
    margin-left: auto;
  }

  /* Visual indicator only — whole card is the tap target.
     pointer-events:none lets card click delegate through. */
  .card-action-btn {
    width: 32px;
    height: 32px;
    font-size: 12px;
    pointer-events: none;
    border-radius: var(--radius-full);
  }

  /* Hide registered checkmark ghost (mobile status pill encodes state) */
  .event-card.is-registered .card-top::after { display: none; }

  /* Stats bar — 2 columns (#2397) */
  .stats-bar { grid-template-columns: repeat(2, 1fr); gap: 8px; padding: 10px 10px 0; }

  /* Prevent horizontal scroll on main page */
  body { overflow-x: clip; }

  /* Active filters bar */
  .active-filters-bar { padding: 6px 12px; }

  /* Register button in modal — 44px height */
  .btn-register { min-height: 44px; }

  /* Filter bar search row: stack toggles vertically */
  .filter-row:first-of-type {
    flex-direction: column;
    align-items: flex-start;
  }
  .filter-row:first-of-type .search-wrap {
    width: 100%;
    max-width: 100%;
  }
  .filter-row:first-of-type .toggle-wrap {
    flex-direction: row-reverse;
    gap: 8px;
  }
}

/* ── Landscape Mobile (short screens) ─────────────────────────────────── */
@media (max-height: 500px) and (orientation: landscape) {
  /* Issue #1726 item 3: tabs must stay 44 px (WCAG 2.5.5 tap target).
     Reclaim vertical space by shrinking the topbar further instead of
     by shortening the tabs. */
  .topbar { height: 40px; padding: 0 10px; }
  .topbar-brand .logo { font-size: 18px; }
  .topbar-brand h1 { font-size: 13px; }
  .topbar-stats { display: none; }
  .topbar-clock { display: none; }
  #nav-tabs { padding: 0 6px; }
  .nav-tab { padding: 6px 10px; font-size: 11px; min-height: 44px; }
}

/* ── Phone breakpoint: 375px overflow safety net (Issue #460) ──────────
   Targets the smallest supported viewport. Anything that would overflow
   horizontally at 375px belongs here. Intentionally narrow in scope so
   it does not affect tablet/desktop. */
@media (max-width: 640px) {
  /* Booking rows: rigid min-widths on date/time/location columns push
     the row past the viewport. Wrap the row and let the title own a
     full line. */
  .booking-row > div:first-child {
    flex-wrap: wrap;
    row-gap: 4px;
  }
  .booking-row > div:first-child > span[style*="min-width"] {
    min-width: 0 !important;
  }
  .booking-row > div:first-child > span[style*="max-width:160px"] {
    max-width: 100% !important;
  }

  /* Generic safety net for tab content: any direct inline-styled
     table or wide block that escapes its container becomes scrollable
     instead of overflowing the viewport. */
  .tab-content table { display: block; overflow-x: auto; max-width: 100%; }

  /* Long unbroken strings (URLs, IDs) — wrap rather than overflow. */
  .tab-content { word-break: break-word; }

  /* Book Courts: search controls already use flex-wrap; ensure
     children don't claim more than the viewport. */
  #book-tab input[type="date"],
  #book-tab select,
  #book-facility-btn,
  #book-facility-dropdown {
    max-width: 100%;
  }
  #book-facility-btn { min-width: 0 !important; }
  #book-facility-dropdown { min-width: 200px !important; }

  /* Cost-history filter row: search input claims at least 160px which
     can push the refresh button off-screen. Allow it to shrink. */
  #cost-search { min-width: 0 !important; }

  /* Modal panels — already full-width via 768px rule, but reaffirm
     for the 375px audit. */
  .modal-panel { max-width: 100% !important; }
}

/* ── Book Courts mobile layout (Issue #1417) ───────────────────────────
   Scope STRICTLY to #book-tab so desktop (>480px) and other tabs are
   untouched. Goals at 375px viewport:
     - search controls stack vertically (one per row)
     - all interactive controls ≥44px tall (touch target)
     - facility dropdown does not bleed off-screen
     - court grid stays 3–4 wide on phone
     - no horizontal overflow inside the tab. */
@media (max-width: 480px) {
  /* The search controls row uses inline display:flex;flex-wrap:wrap
     with rigid min-widths on children. Force each child block to claim
     a full row so they stack cleanly. */
  #book-tab > div > div { min-width: 0 !important; }

  /* The first row (Facilities / Dates / Duration / Search) — its direct
     children are the four control wrappers + the search button.
     NOTE: matches the inline style="display:flex;flex-wrap:wrap;..." on the
     search controls row in dashboard.html; update both together if markup
     changes. */
  #book-tab > div[style*="flex-wrap:wrap"] > div,
  #book-tab > div[style*="flex-wrap:wrap"] > button {
    flex: 1 1 100% !important;
    width: 100% !important;
    min-width: 0 !important;
  }

  /* Form controls — full width + 44px touch target. */
  #book-facility-btn,
  #book-date-input,
  #book-duration,
  #book-check-btn {
    width: 100% !important;
    min-width: 0 !important;
    min-height: 44px !important;
    box-sizing: border-box !important;
  }

  /* Date input row contains the date field + the "add date" plus
     button; let the date input flex while keeping the + button compact
     and tall enough to tap. */
  #book-date-input { flex: 1 1 auto !important; }
  #book-tab #btn-add-book-date {
    min-height: 44px !important;
    min-width: 44px !important;
    flex: 0 0 auto !important;
  }

  /* Facility dropdown — never bleed past viewport. */
  #book-facility-dropdown {
    min-width: 0 !important;
    width: 100% !important;
    max-width: calc(100vw - 32px) !important;
    left: 0 !important;
    right: auto !important;
  }

  /* Court selection grid — tighter cells so 3–4 courts fit per row at
     375px (court labels are short, e.g. "Court 1"). */
  #book-courts-grid {
    grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)) !important;
    gap: 6px !important;
  }

  /* Court chips themselves are emitted with inline padding; ensure tap
     target. */
  #book-courts-grid button { min-height: 56px !important; }

  /* Court selection header (label + Select All / Clear buttons) —
     allow it to wrap and make the small action buttons tap-friendly. */
  #book-courts-section > div:first-child { flex-wrap: wrap !important; row-gap: 6px !important; }
  #book-tab #btn-select-all-courts,
  #book-tab #btn-clear-court-selection {
    min-height: 44px !important;
    padding: 6px 14px !important;
    font-size: 12px !important;
  }

  /* Time-slot result cards inside #book-results-container: the inline
     grid emitted by renderSearchResults uses flex-wrap so it already
     reflows, but enforce no inline min-width spillover and a touch-
     friendly slot button height. */
  #book-results-container button[data-slot-result-idx] {
    min-height: 44px !important;
    min-width: 0 !important;
    flex: 1 1 auto !important;
  }

  /* Confirm booking — full-width submit, tap target. */
  #book-submit-btn {
    width: 100% !important;
    min-height: 48px !important;
    box-sizing: border-box !important;
    margin-bottom: 8px !important;
  }
  /* Free-tier chip / upgrade link: remove desktop margin-left and
     add top separation from the 100%-wide submit button above.
     No display override — JS controls visibility via inline style;
     using display !important here would override JS display:none. */
  #book-tab #free-court-chip,
  #book-tab #free-court-upgrade-link {
    margin-left: 0 !important;
    margin-top: 8px !important;
  }

  /* Cancel-search inside the loading panel — touch target. */
  #book-tab #book-cancel-search-btn { min-height: 44px !important; padding: 10px 18px !important; }

  /* Belt-and-suspenders: no child of #book-tab is allowed to push the
     viewport horizontally. */
  #book-tab { overflow-x: hidden; }
  #book-tab * { max-width: 100%; box-sizing: border-box; }
}

/* Rule execution history — expandable run rows (issue #3148) */
.rule-history-runs { border: 0; }
.rule-history-run-header:hover td { background: var(--bg-1); }
.rule-history-run-header:focus-visible td { outline: 2px solid var(--blue); outline-offset: -2px; }

/* ── Automations tab: mobile layout (Issue #1416) ──────────────────────
   The Rules tab markup in dashboard.html relies almost entirely on
   inline styles (legacy). At iPhone-class viewports (375px / 390px),
   that gives us:
     - the add-rule form's `grid-template-columns:1fr 1fr` cramming
       date/time/number inputs into ~155px columns;
     - rule-card action buttons (run / history / edit / pause / delete)
       with inline padding 5px 12px → ~28px tall, below WCAG's 44px
       tappable target;
     - the scheduler panel + "Add Rule" header overflowing horizontally
       because the inline flex row has no wrap rules at this width;
     - long facility/title strings spilling past the card edge.
   Scoped to `#rules-tab` so desktop/tablet behaviour is untouched. */
@media (max-width: 640px) {
  /* Header row: title + Add Rule button — wrap and let the button breathe. */
  #rules-tab > div:first-child {
    flex-wrap: wrap;
    row-gap: 8px;
  }
  #rules-tab #btn-show-add-rule {
    min-height: 44px;
    padding: 8px 14px;
    font-size: 13px;
  }

  /* Scheduler control panel: stack rows vertically; promote toggle/run
     buttons to 44px tappable targets. */
  #rules-tab #scheduler-panel {
    padding: 12px 14px;
    gap: 10px;
  }
  #rules-tab #scheduler-panel #sched-toggle-btn {
    min-height: 44px;
    padding: 8px 14px;
    font-size: 13px;
  }

  /* Add-rule form: collapse the 2-column grid to 1 column so date/time
     inputs get the full viewport width and labels stay readable. */
  #rules-tab #add-rule-form { padding: 14px; }
  #rules-tab #add-rule-form > div[style*="grid-template-columns"] {
    grid-template-columns: 1fr !important;
    gap: 12px !important;
  }
  /* iOS zooms text inputs <16px — bump field font-size to 16px to
     suppress that. */
  #rules-tab #add-rule-form input[type="text"],
  #rules-tab #add-rule-form input[type="number"],
  #rules-tab #add-rule-form input[type="date"],
  #rules-tab #add-rule-form input[type="time"],
  #rules-tab #add-rule-form select {
    font-size: 16px !important;
    min-height: 44px;
  }
  /* Day-of-week pills: each pill 44×44 tappable. */
  #rules-tab #rule-days-btns .autoreg-day-btn {
    min-width: 44px;
    min-height: 44px;
    padding: 8px 10px !important;
    font-size: 13px !important;
  }
  /* Submit / Cancel: full-width stacked. */
  #rules-tab .add-rule-footer {
    flex-direction: column;
    gap: 8px;
  }
  #rules-tab #btn-submit-rule,
  #rules-tab #btn-cancel-rule {
    width: 100%;
    min-height: 44px;
    padding: 12px 16px !important;
    font-size: 14px !important;
    justify-content: center;
  }
  /* Exclusion-date add button: 44px tappable. */
  #rules-tab #btn-add-exclusion-date {
    min-width: 44px;
    min-height: 44px;
  }

  /* Rule cards: actions row wraps below the rule body and each action
     button hits 44×44px. The 480px tier already toggles flex-wrap, but
     the buttons themselves still inherit inline padding:5px 12px which
     fails WCAG — override at the action-button level.
     Issue #2359: .rule-actions has inline flex-shrink:0 which prevents
     proper overflow control; override it here so the actions strip
     wraps to its own line at narrow viewports without overlapping the
     card body text. Also add min-width:0 to the content column so a
     long title_pattern cannot push .rule-actions off the card edge. */
  #rules-tab .rule-row {
    flex-wrap: wrap;
    row-gap: 10px;
  }
  /* min-width:0 on the content column lets it shrink below its min-content
     width (long monospace patterns) rather than forcing .rule-actions out. */
  #rules-tab .rule-row > div:first-child {
    min-width: 0;
  }
  #rules-tab .rule-actions {
    flex-wrap: wrap;
    flex-shrink: 1 !important; /* override inline flex-shrink:0 (#2359) */
    row-gap: 6px;
    column-gap: 6px;
    width: 100%;
  }
  #rules-tab .rule-actions button {
    min-width: 44px;
    min-height: 44px;
    padding: 10px 14px !important;
    font-size: 14px !important;
    flex: 1 1 auto;
  }

  /* Long title patterns / category strings: wrap rather than blow past
     the card. The pattern chip uses inline display + monospace; allow
     it to break. */
  #rules-tab .rule-row span[style*="monospace"] {
    overflow-wrap: break-word;
    word-break: break-word;
    white-space: normal;
  }

  /* Conflicts banner: stack header (label + toggle) on narrow viewports. */
  #rules-tab #rules-conflicts > div:first-child {
    flex-wrap: wrap;
    row-gap: 8px;
  }
  #rules-tab #rules-conflicts button[data-action="toggle-conflicts-details"] {
    min-height: 44px;
    padding: 8px 14px;
  }
}

/* ── Automations tab: tablet iOS-zoom fix (Issue #1724) ─────────────────
   The full phone layout above (max-width:640px) includes the font-size
   bump for add-rule form inputs. At 641–767px (tablet portrait) those
   inputs still carry inline font-size:12px and iOS will zoom on focus.
   This narrower rule adds only the font-size override — no grid or
   touch-target changes — so tablet layout is otherwise untouched.
   #min-spots-custom (events filter, any viewport) also gets the 16px
   bump here since it has no existing mobile override. */
@media (max-width: 768px) {
  #rules-tab #add-rule-form input[type="text"],
  #rules-tab #add-rule-form input[type="number"],
  #rules-tab #add-rule-form input[type="date"],
  #rules-tab #add-rule-form input[type="time"],
  #rules-tab #add-rule-form select {
    font-size: 16px !important;
  }
  #bookings-sort {
    font-size: 16px !important;
    min-height: 44px;
  }
  #min-spots-custom {
    font-size: 16px !important;
    min-height: 44px;
  }

  /* Issue #3884 — 44px tap targets at 375px viewport.
     Facility filter rows and "Select All" label in the Book Courts
     dropdown use inline padding:4-6px 12px which renders ~24-28px tall.
     The "Add window" button in the autobook rule form uses inline
     padding:4px 10px which renders ~20-22px tall. */
  .book-fac-row {
    min-height: 44px !important;
    display: flex !important;
    align-items: center !important;
  }
  #book-facility-dropdown > label:first-of-type {
    min-height: 44px !important;
    display: flex !important;
    align-items: center !important;
  }
  #btn-add-rule-window {
    min-height: 44px !important;
    padding: 10px 14px !important;
  }
}

/* ── Density toggle (Issue #460, #1462) ────────────────────────────────
   Opt-in compact spacing. Semantically orthogonal to `view-compact`
   (which restyles cards as one-line rows). Stacking rule (Issue #1462
   round 3, addressing Reviewer 1 nits): density-compact must never
   *loosen* spacing relative to any other rule that targets the same
   property. The safe way to express that on properties that view-compact
   (or its @media breakpoints) also sets to a tighter value is to scope
   the density override to `:not(.view-compact)`. In that case
   view-compact's tighter spacing already applies and the density
   override is a no-op — exactly the desired semantics, and we avoid
   `!important` on the property entirely (so any future tighter rule can
   still win on source order at equal specificity).

   Selector specificity:
     - base e.g. `.event-card`               → (0,1,0)
     - `body.view-compact .event-card`       → (0,2,0)
     - `body.density-compact:not(.view-compact) .event-card` → (0,3,1)
   The density scoped form outranks both base and view-compact, so we
   don't need !important to beat the base rule.

   `!important` is retained ONLY where we must beat inline styles, since
   inline `style="..."` is specificity (1,0,0,0):
     - `.booking-row > [data-action="toggle-booking-players"]` carries
       `style="padding:8px 12px"` (list view, main.js:5447) or
       `style="padding:14px"` (card view, main.js:5516). */
body.density-compact .main { padding-top: 12px; padding-bottom: 40px; }
body.density-compact .tab-content { padding: 6px 0; }
/* view-compact hides .stats-bar entirely (line 2118 below), so when both
   are on these rules never apply visually; scope :not(.view-compact)
   anyway for clarity and to avoid surprising behaviour if .stats-bar
   were ever shown under view-compact. */
body.density-compact:not(.view-compact) .stats-bar { gap: 8px; padding-top: 8px; }
body.density-compact:not(.view-compact) .stat-card { padding: 8px 10px; gap: 8px; }
body.density-compact:not(.view-compact) .stat-card .stat-icon { width: 32px; height: 32px; font-size: 14px; }
/* day-group: view-compact sets margin-bottom: 8px (line 2120); the ≤480px
   block sets 12px (line 1668). Density's 14px would loosen view-compact's
   8px, so scope to :not(.view-compact). At mobile, the scoped selector
   `body.density-compact:not(.view-compact) .day-group` has specificity
   (0,3,1) and beats the mobile `.day-group { margin-bottom: 12px }` rule
   at (0,1,0), so density-compact-alone produces 14px at mobile — a 2px
   loosen vs mobile-cozy (12px) but still a net improvement over the
   pre-PR 18px, and tighter than desktop-cozy's 32px. Future cleanup:
   either duplicate the density rule inside the ≤480px @media block or
   move the mobile 12px override under a `:not(.density-compact)` scope. */
body.density-compact:not(.view-compact) .day-group { margin-bottom: 14px; }
/* day-header: density 4px+2px = 6px is tighter than view-compact's
   8px+3px = 11px, AND tighter than mobile's 8px+5px = 13px — strictly
   tightens in every overlap, so unscoped is fine. */
/* Issue #2398 follow-up: vertical longhand so the inherited horizontal
   padding from the base .day-header rule (20px desktop, 12px mobile) is
   preserved — padding shorthand would zero the horizontal and misalign
   day-header content with the event-card column. */
body.density-compact .day-header { padding-top: 4px; padding-bottom: 2px; }
/* events-grid: view-compact uses gap:1px (line 2131) and the ≤600px
   view-compact block uses gap:10px (line 2253); density's 6px would
   loosen the first and tighten the second only at that breakpoint.
   Cleanest: density only applies when view-compact is off. */
body.density-compact:not(.view-compact) .events-grid { gap: 6px; }
/* event-card: view-compact desktop padding 4px/14px/4px/10px is tighter
   vertically than density's 6px/14px/6px/10px would be, and view-compact
   gap (10px desktop, 8px ≤600px, 2px ≤480px) is tighter at ≤480px.
   So density-compact event-card padding/gap is only safe when
   view-compact is OFF. (Outside view-compact, base is 16/22/16/16
   padding, 10px gap — density strictly tightens.) */
body.density-compact:not(.view-compact) .event-card { padding: 6px 10px 6px 14px; gap: 4px; }
/* ::before: view-compact desktop top/bottom 3px (line 2155),
   ≤480px 4px (line 2348) — density's 4px would loosen the desktop case.
   Scope to :not(.view-compact). */
body.density-compact:not(.view-compact) .event-card::before { top: 4px; bottom: 4px; }
/* Issue #1952 — outer .booking-row wrapper had no cursor, so hovering the
   row edges/padding (outside the inline cursor:pointer toggle child) showed
   the default arrow. Set pointer on the wrapper to match the visible
   click affordance; restore default on the expanded .booking-players
   sub-block because clicking there does not toggle. */
.booking-row { cursor: pointer; }
.booking-row > .booking-players { cursor: default; }

/* Issue #3948 — bookings.md § Guest TBD charge warning (visible text). */
.booking-guest-tbd-warn {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--yellow);
  font-size: 11px;
  min-width: 0;
}
.booking-guest-tbd-warn-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.booking-guest-tbd-warn--expanded {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  font-size: 12px;
  margin-top: 4px;
}
/* Booking-row inner div carries inline `padding:8px 12px` (list view) or
   `padding:14px` (card view); we need !important to beat inline. The
   view-compact view doesn't touch booking-row, so no :not() scoping
   needed here. */
body.density-compact .booking-row > [data-action="toggle-booking-players"] {
  padding: 4px 10px !important;
  font-size: 12px !important;
  gap: 6px !important;
}
body.density-compact .booking-row .booking-chev { font-size: 9px; }

/* Issue #2783 — pin Submit/Cancel while scrolling the tall add-rule form so
   #btn-cancel-rule stays in the viewport for audits and mobile users. */
#rules-tab #add-rule-form:not(.hidden) .add-rule-footer {
  position: sticky;
  bottom: 0;
  z-index: 2;
  background: var(--bg-card);
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

/* ── Utility ───────────────────────────────────────────────────────────── */
.hidden { display: none !important; }
.flex-center { display: flex; align-items: center; justify-content: center; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }

/* ── Compact View ──────────────────────────────────────────────────────── */
body.view-compact .stats-bar { display: none; }
body.view-compact .main { max-width: 1600px; }
body.view-compact .day-group { margin-bottom: 8px; }

body.view-compact .day-header {
  /* Issue #2398 follow-up: vertical longhand so the inherited horizontal
     padding from the base .day-header rule (20px desktop, 12px mobile) is
     preserved — padding shorthand would zero the horizontal and misalign
     day-header content with the event-card column. */
  padding-top: 8px;
  padding-bottom: 3px;
}
body.view-compact .day-title { font-size: 13px; font-weight: 700; letter-spacing: -0.01em; }
body.view-compact .day-title .day-relative { font-size: 11px; }
/* Issue #1726 item 13: 9 → 11 px. */
body.view-compact .day-count { font-size: 11px; padding: 1px 6px; }

body.view-compact .events-grid {
  grid-template-columns: 1fr;
  gap: 1px;
}

body.view-compact .event-card {
  flex-direction: row;
  align-items: center;
  padding: 4px 10px 4px 14px;
  gap: 10px;
  border-radius: var(--radius-sm);
  animation: none;
  opacity: 1;
  transform: none;
}

body.view-compact .event-card:hover {
  transform: none;
  border-image: none;
}

body.view-compact .event-card:hover::after {
  display: none;
}

body.view-compact .event-card::before {
  top: 3px; bottom: 3px; width: 3px;
}

body.view-compact .card-top {
  flex: 1;
  min-width: 0;
  align-items: center;
  gap: 10px;
}

body.view-compact .card-row1 {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
}

body.view-compact .card-time {
  font-size: 12px;
  font-weight: 600;
  min-width: 120px;
  flex-shrink: 0;
  white-space: nowrap;
}

body.view-compact .card-name {
  font-size: 12px;
  font-weight: 500;
  -webkit-line-clamp: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: block;
  flex: 1;
  min-width: 0;
}

body.view-compact .card-badges {
  flex-shrink: 0;
  flex-wrap: nowrap;
  gap: 3px;
}

body.view-compact .badge {
  /* Issue #1726 item 13: 9 → 11 px floor (content-bearing). */
  font-size: 11px;
  padding: 1px 5px;
}

body.view-compact .badge-tag { display: none; }
body.view-compact .badge-multi-date { display: none; }

body.view-compact .card-bottom {
  margin-top: 0;
  gap: 6px;
  flex-shrink: 0;
}

body.view-compact .card-meta {
  flex-direction: row;
  align-items: center;
  gap: 6px;
}

body.view-compact .spots-bar-wrap { max-width: 50px; height: 3px; }
/* Issue #1726 item 13: 10 → 11 px (spots remaining count). */
body.view-compact .spots-text { font-size: 11px; }
body.view-compact .card-cost { display: none; }
body.view-compact .card-instructor { display: none; }

body.view-compact .btn-register {
  padding: 2px 8px;
  font-size: 10px;
}

body.view-compact .skeleton-grid {
  grid-template-columns: 1fr;
  gap: 2px;
}

body.view-compact .skeleton-card {
  padding: 6px 10px;
  flex-direction: row;
  gap: 8px;
}

body.view-compact .skeleton-line.h20,
body.view-compact .skeleton-line.h24 { height: 12px; }

@media (max-width: 720px) {
  body.view-compact .card-badges { display: none; }
  body.view-compact .card-time { min-width: 90px; font-size: 11px; }
}

/* Issue #457: On phones (~375px), the single-row compact layout clipped
   event name, location, and the Register button off-screen. Fall back
   to the stacked (expanded) layout below 600px so every card shows
   name, location, time, and primary action without horizontal overflow. */
@media (max-width: 600px) {
  body.view-compact .events-grid { gap: 10px; }

  body.view-compact .event-card {
    flex-direction: column;
    align-items: stretch;
    padding: 12px 12px 12px 18px;
    gap: 8px;
    border-radius: var(--radius-lg);
  }

  body.view-compact .event-card::before {
    top: 12px; bottom: 12px; width: 4px;
  }

  body.view-compact .card-top {
    align-items: flex-start;
    gap: 10px;
  }

  body.view-compact .card-row1 {
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
  }

  body.view-compact .card-time {
    min-width: 0;
    font-size: 16px;
    font-weight: 700;
  }

  body.view-compact .card-name {
    font-size: 13px;
    font-weight: 500;
    white-space: normal;
    -webkit-line-clamp: 2;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    text-overflow: clip;
  }

  body.view-compact .card-badges {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
  }

  body.view-compact .badge {
    font-size: 10px;
    padding: 2px 8px;
  }

  body.view-compact .card-bottom {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
  }

  body.view-compact .card-meta {
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    flex: 1;
    min-width: 0;
  }

  body.view-compact .spots-bar-wrap { max-width: 100px; height: 6px; }
  body.view-compact .spots-text { font-size: 11px; }
  body.view-compact .card-cost { display: block; font-size: 11px; }
  body.view-compact .card-instructor { display: flex; font-size: 11px; }

  body.view-compact .btn-register {
    padding: 10px 16px;
    font-size: 12px;
    min-height: 44px;
  }
}

/* ── view-compact at ≤480px: adopt same two-row layout ──────────────── */
/* The main ≤480px block above already handles the two-row layout for all
   event cards. This block re-asserts the strict 2-row layout when the
   higher-specificity `body.view-compact ...` rules from the ≤600px block
   would otherwise stack rows vertically (Issue #1429). */
@media (max-width: 480px) {
  body.view-compact .event-card {
    flex-direction: column;
    align-items: stretch;
    padding: 6px 10px 6px 16px;
    gap: 2px;
    min-height: 56px;
    border-radius: var(--radius-sm);
  }

  body.view-compact .event-card::before {
    top: 4px; bottom: 4px; width: 3px;
  }

  /* Re-assert Row 1 = single horizontal row (the ≤600px view-compact
     block sets flex-direction:column which would force a 3-row card). */
  body.view-compact .card-row1 {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 8px;
    min-width: 0;
    overflow: hidden;
  }

  body.view-compact .card-time {
    font-size: 13px;
    font-weight: 700;
    white-space: nowrap;
    flex-shrink: 0;
    line-height: 1.2;
    letter-spacing: 0;
    color: var(--text-0);
    font-variant-numeric: tabular-nums;
    width: 72px;
    min-width: 72px;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  body.view-compact .card-time .card-time-full { display: none; }
  body.view-compact .card-time .card-time-mobile { display: inline; }

  body.view-compact .card-name {
    font-size: 14px;
    font-weight: 500;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    display: block;
    flex: 1;
    min-width: 0;
    color: var(--text-0);
    -webkit-line-clamp: 1;
  }

  body.view-compact .card-row2 {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 8px;
    min-width: 0;
    overflow: hidden;
  }

  /* Re-assert hidden chip-bag + desktop bottom row */
  body.view-compact .card-badges { display: none; }
  body.view-compact .card-bottom { display: none; }
  body.view-compact .card-meta { display: none; }
  body.view-compact .card-cost { display: none; }
  body.view-compact .card-instructor { display: none; }
  body.view-compact .spots-bar-wrap { display: none; }
  body.view-compact .spots-text { display: none; }
}

/* ── Cost History table — mobile (≤480px) ──────────────────────────────
   Issues #1158 (initial fix) / #1263 (rollback follow-up): on a 390px
   viewport the 9-column table with inline min-width:720px forces
   wrapper-internal horizontal scroll. We drop the min-width floor, hide
   four low-priority columns (Event Date, Provider, Payment, Flag), and
   set `table-layout:fixed` plus explicit per-column percent widths so
   the auto algorithm can't starve text-heavy columns (Description/Venue
   previously collapsed to ~40px / ~35px while Amount over-allocated to
   ~234px even at width:100%, which forced the #1158 rollback). The
   tfoot is hidden because its hard-coded colspan="5" / colspan="3" cells
   overflow the table box under fixed layout, and the same filtered total
   is already surfaced in the "Total Spend" stat card above the table. */
@media (max-width: 480px) {
  #cost-ledger-table {
    min-width: 0 !important;
    display: table;
    overflow-x: visible;
    width: 100%;
    table-layout: fixed;
    font-size: 12px;
  }
  #cost-ledger-table .col-event-date,
  #cost-ledger-table .col-provider,
  #cost-ledger-table .col-payment,
  #cost-ledger-table .col-flag {
    display: none;
  }
  /* Explicit column widths for the 5 visible columns. With
     table-layout:fixed these are honored directly from the <th> cells
     and propagate to the matching <td> column slots. Targeting by class
     keeps this robust to column reordering. */
  #cost-ledger-table .col-txn-date    { width: 18%; }
  #cost-ledger-table .col-description { width: 30%; }
  #cost-ledger-table .col-venue       { width: 22%; }
  #cost-ledger-table .col-amount      { width: 16%; }
  #cost-ledger-table .col-status      { width: 14%; }
  /* Tighten cell padding so the 5 kept columns breathe in a 390px viewport. */
  #cost-ledger-table th,
  #cost-ledger-table td {
    padding: 8px 6px;
  }
  /* Header cells carry inline white-space:nowrap which forces them to
     claim wide column slots; allow wrapping on mobile so column titles
     can break naturally instead of forcing horizontal scroll. */
  #cost-ledger-table th {
    white-space: normal !important;
  }
  /* Description/Venue inline max-width caps (280px / 160px) are no longer
     needed under table-layout:fixed — the .col-description (30%) and
     .col-venue (22%) percent widths already constrain those cells, so
     dropping the nth-child overrides keeps this rule robust to column
     reordering without changing layout. */
  /* Issue #1285: The Amount, Status, and Txn Date cells carry inline
     white-space:nowrap which bypasses table-layout:fixed and lets cell
     content overflow. Force wrap on mobile so long CAD+tax strings
     (e.g. "$3051.00 CAD (incl. $351.00 tax)") break instead of pushing
     the table past the viewport. Dev VM in #1269 had only short
     "Included" badges so per-cell overflow went undetected until prod.
     Also scope to descendants (`* `) — future row-rendering code that
     wraps content in `<span style="white-space:nowrap">` inside one of
     these cells would otherwise re-overflow (the #1269 badge HTML
     pattern). `overflow-wrap: anywhere` is the modern wrap directive;
     `word-break: break-word` is kept as a legacy fallback for older
     mobile Safari that ignores `overflow-wrap: anywhere`.
     (th cells are already covered by the
     `#cost-ledger-table th { white-space: normal !important }` rule
     above at ~line 2242, so this rule scopes to `td` only.) */
  #cost-ledger-table td.col-amount,
  #cost-ledger-table td.col-amount *,
  #cost-ledger-table td.col-status,
  #cost-ledger-table td.col-status *,
  #cost-ledger-table td.col-txn-date,
  #cost-ledger-table td.col-txn-date * {
    white-space: normal !important;
    overflow: hidden;
    overflow-wrap: anywhere;
    word-break: break-word; /* legacy fallback for older mobile Safari */
  }
  /* Empty-state cell carries inline padding:40px which alone forces ~80px
     of horizontal padding; shrink it on mobile so it doesn't push the
     table past the viewport. */
  #cost-ledger-table tbody td[colspan="9"] {
    padding: 20px 8px !important;
  }
  /* tfoot's hard-coded colspan="5" / colspan="3" cells span across hidden
     and visible columns; under table-layout:fixed the colspan cell still
     claims content-driven width that overflows the table box. Hide the
     tfoot on mobile — the same filtered total is already surfaced in the
     "Total Spend" stat card at the top of the tab (the card's label is
     "Total Spend" but its value reflects the current filter), so the
     tfoot row is purely redundant on a 390px viewport. */
  #cost-ledger-table tfoot {
    display: none;
  }
}

/* ── Issue #1422: Cost History tab — mobile card layout + filter
   collapse + 44 CSS-px tap targets.
   The prior mobile work (#1158 / #1263 / #1285 above) made the wide
   9-column table merely fit at 480px by hiding four low-priority
   columns and tightening padding. Issue #1422 escalates that: the
   spec calls for a card-per-entry layout that shows ALL fields (no
   data lost behind a hidden column), a collapsible filter panel
   with an active-filter count badge, and 44 CSS-px tap targets on
   every interactive control. This block sits BELOW the prior media
   query so its display:block + !important selectors win where they
   overlap. Desktop is untouched (everything stays inside the
   @media (max-width: 768px) gate — Issue #3939). */
@media (max-width: 768px) {
  /* ── Card-per-entry: turn the <table> into a vertical stack of
     bordered cards. ``display:block`` on table / tbody / tr / td
     unwinds the table layout; ``thead`` is hidden because each card
     surfaces its own labels via ``td::before { content: attr(data-label) }``
     (the row renderer in main.js adds ``data-label`` to every <td>
     it emits — see #1422). All four previously-hidden columns
     (.col-event-date / .col-provider / .col-payment / .col-flag)
     come back into the card so no field is lost on mobile. */
  #cost-ledger-table,
  #cost-ledger-table tbody,
  #cost-ledger-table tr,
  #cost-ledger-table td {
    display: block !important;
    width: 100% !important;
    max-width: none !important;
    box-sizing: border-box;
  }
  #cost-ledger-table thead {
    /* Visually hidden but kept in the accessibility tree so screen
       readers still announce column semantics. */
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    overflow: hidden !important;
    clip: rect(0 0 0 0) !important;
    white-space: nowrap !important;
    /* Issue #2906 / #2910 — clipped headers still report narrow bounding rects
       to automated tap-target audits when role=button is set; disable
       pointer hit-testing on the hidden header row. */
    pointer-events: none !important;
  }
  #cost-ledger-table tbody tr {
    background: var(--bg-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    padding: 12px 14px;
    margin-bottom: 10px;
  }
  #cost-ledger-table tbody td {
    padding: 4px 0 !important;
    border: 0 !important;
    white-space: normal !important;
    overflow: visible !important;
    text-overflow: clip !important;
    word-break: break-word;
    overflow-wrap: anywhere;
    text-align: left !important;
    display: flex !important;
    align-items: baseline;
    gap: 8px;
  }
  /* Label rendered from data-label="…" attribute (added by the row
     renderer in main.js). Empty data-label means the cell stays
     unlabelled (used for the description, which already speaks for
     itself). */
  #cost-ledger-table tbody td::before {
    content: attr(data-label);
    color: var(--text-2);
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    flex: 0 0 92px;
  }
  #cost-ledger-table tbody td[data-label=""]::before,
  #cost-ledger-table tbody td:not([data-label])::before {
    display: none;
  }
  /* Description is the card title — full-width, larger, no label. */
  #cost-ledger-table tbody td.col-description {
    font-size: 14px !important;
    font-weight: 600;
    color: var(--text-1) !important;
    display: block !important;
    margin-bottom: 6px;
  }
  /* Amount stays prominent. */
  #cost-ledger-table tbody td.col-amount {
    font-size: 15px !important;
  }
  /* Restore visibility on the four columns the prior #1158/#1263 block
     hid — every field should appear in the card layout. */
  #cost-ledger-table .col-event-date,
  #cost-ledger-table .col-provider,
  #cost-ledger-table .col-payment,
  #cost-ledger-table .col-flag {
    display: flex !important;
  }
  /* Empty-state / loading rows have a single colspan cell — keep
     them readable inside the card stack. */
  #cost-ledger-table tbody td[colspan="9"],
  #cost-ledger-table tbody td[colspan="11"] {
    display: block !important;
    text-align: center !important;
    padding: 24px 12px !important;
  }
  #cost-ledger-table tbody td[colspan="9"]::before,
  #cost-ledger-table tbody td[colspan="11"]::before { display: none; }
}

/* Desktop default: <details id="cost-filters"> behaves like a plain
   <div> — summary hidden, body always laid out. The ``open`` attribute
   on the element keeps native semantics intact (so any JS that opens /
   closes it on mobile still works) without affecting desktop layout.
   Declared BEFORE the ≤768px override so the mobile rule wins on cascade
   order alone and no !important is required (Reviewer 1 nit, #1740). */
#cost-filters > summary.cost-filters-toggle { display: none; }
#cost-filters > .cost-filters-body {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
}

/* ── Issue #1726 item 6: Filter-bar collapse activates across all mobile
   sizes (≤768px), not just narrow phones. The card-per-entry table
   layout above (also ≤768px as of #3939) shares this breakpoint.
   Previously these rules lived inside the ≤480px block, leaving 481–768
   px tablets with the full-width filter row taking ~3 lines of vertical
   space above the table. */
@media (max-width: 768px) {
  /* Filter bar collapse: <details id="cost-filters"> wraps the filter
     row. Desktop default above hides the <summary>; mobile/tablet shows
     it as a 44-px-tall toggle button with an active-filter count badge.
     Same specificity but later in source order → no !important needed. */
  #cost-filters > summary.cost-filters-toggle {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-height: 44px;
    padding: 8px 14px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--bg-2);
    color: var(--text-1);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    list-style: none;
    user-select: none;
  }
  #cost-filters > summary.cost-filters-toggle::-webkit-details-marker {
    display: none;
  }
  #cost-filters > summary.cost-filters-toggle::after {
    content: "\25BE"; /* ▾ */
    margin-left: auto;
    color: var(--text-2);
    font-size: 12px;
    transition: transform 120ms ease-out;
  }
  #cost-filters[open] > summary.cost-filters-toggle::after {
    transform: rotate(180deg);
  }
  /* Issue #2906 — Spend Breakdown <summary> renders ~21px tall on iPhone
     SE; enforce the 44 CSS-px WCAG 2.5.5 floor (cost-filters has its own
     rule above). */
  #costs-tab details:not(#cost-filters) > summary {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    box-sizing: border-box;
    padding: 8px 0;
  }
  /* Issue #2363/#2362: the inline style="display:flex" on .cost-filters-body
     overrides the UA display:none that browsers apply to closed <details>
     content, leaving filter buttons (Refresh, Export CSV) in the layout
     tree with real bounding rects that overlap the table header below.
     !important beats the inline style and collapses the body to zero size
     when the panel is toggled closed on mobile. */
  #cost-filters:not([open]) > .cost-filters-body {
    display: none !important;
  }
  #cost-filters > .cost-filters-body {
    flex-direction: column !important;
    align-items: stretch !important;
    margin-top: 10px;
  }
  /* All filter controls become full-width 44 px tappables. */
  #cost-filters > .cost-filters-body > select,
  #cost-filters > .cost-filters-body > input[type="text"],
  #cost-filters > .cost-filters-body > button,
  #cost-filters > .cost-filters-body > label {
    width: 100% !important;
    min-width: 0 !important;
    min-height: 44px;
    box-sizing: border-box;
    font-size: 16px !important;
  }
  /* Active-filter count badge — surfaces on the toggle when the JS
     side-channel adds a count > 0 and removes the .hidden class. */
  #cost-filters-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    border-radius: 999px;
    background: var(--blue, #3b82f6);
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
  }
  #cost-filters-count.hidden { display: none; }
}

/* ── Issue #1415: My Bookings tab — mobile + tablet polish.
   The prior #460 work above (max-width: 640px) already drops the
   rigid date/time/location min-widths on .booking-row so the row
   wraps; this block layers on top with the strictly-tighter floors
   from #1415:
     - <=480px: title cell wraps (currently white-space:nowrap +
       text-overflow:ellipsis inline-styled), every clickable
       (Refresh, Expanded/Compact toggle, .booking-edit-btn, manage
       <a>) hits the 44 CSS-px WCAG 2.5.5 tap-target floor, and the
       expanded-mode grid drops to a single column.
     - 481–1024px (tablet): force the expanded grid to exactly two
       columns instead of the auto-fill 340 px minmax (which yields
       1 column on the bottom half of the tablet range). */
@media (max-width: 480px) {
  /* Compact-mode title cell carries inline ``white-space:nowrap`` +
     ``text-overflow:ellipsis``; allow it to wrap so long event titles
     stay legible without truncation. ``flex:1`` from the inline style
     still makes the title own the remaining row width. */
  .booking-row > [data-action="toggle-booking-players"] > span[style*="flex:1"] {
    white-space: normal !important;
    overflow: visible !important;
    text-overflow: clip !important;
    word-break: break-word;
  }
  /* Location cell — same inline ``white-space:nowrap``/ellipsis;
     wraps cleanly on its own line once the row reflows. */
  .booking-row > [data-action="toggle-booking-players"] > span[style*="max-width:160px"] {
    white-space: normal !important;
    overflow: visible !important;
    text-overflow: clip !important;
  }
  /* Tap targets: header buttons (Refresh, Expanded/Compact toggle),
     the per-row Manage gear <a>, and the player-list Edit button.
     Each carries inline ``padding:6px 12px`` which lands well under
     44 px in CSS pixels; override on mobile. */
  #btn-refresh-bookings,
  #bookings-view-toggle,
  #bookings-list .booking-edit-btn {
    min-height: 44px !important;
    min-width: 44px !important;
    padding: 8px 14px !important;
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
  }
  /* Issue #1949: Events tab header Refresh button + Friends tab Refresh +
     Cost-history Refresh + Calendar-sync Refresh. Each carries inline
     ``padding:6px 12px`` / ``padding:8px 12px`` and clocks in well under
     44 px tall. Mirror the bookings rule above so every refresh control
     meets WCAG 2.5.5 / Apple HIG hit area on phones. */
  #btn-refresh-events,
  #btn-refresh-costs,
  #btn-refresh-friends,
  #fab-refresh {
    min-height: 44px !important;
    min-width: 44px !important;
    padding: 8px 14px !important;
    display: inline-flex !important;
    align-items: center;
    justify-content: center;
  }
  /* Issue #1949: the in-events "Refresh now" inline link sits in the
     ``.refresh-bar`` text strip (currently 73x15). Padding alone would
     shove the surrounding "·" separator apart, so we use an invisible
     padded hit area on the <button> while leaving the visible text size
     unchanged. */
  #btn-refresh-now {
    min-height: 44px;
    min-width: 44px;
    padding: 12px 8px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* Expanded-mode grid (inline ``grid-template-columns:repeat(auto-fill,
     minmax(340px,1fr))`` on #bookings-list) — at 375 px the 340 px
     minimum still fits one column, but on a 360 px viewport the cell
     overflows. Force single-column on phones. */
  #bookings-list[style*="grid"] {
    grid-template-columns: 1fr !important;
  }
  /* Issue #1949: Dashboard footer links — both the "Powered by
     CourtReserve + PodPlay" provider links in ``.footer-left`` and the
     "Terms · Privacy · Acceptable Use · Subprocessors · Refunds" row in
     ``.legal-footer-links`` render at 17-19 px tall on desktop and the
     same on phones. Pad each <a> so its hit area is ≥44 px without
     visibly bloating the text. The nav already wraps via
     ``flex-wrap:wrap``, so taller hit areas just reflow gracefully. */
  .dashboard-footer .footer-left a,
  .dashboard-footer .legal-footer-links a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
    padding: 12px 8px;
  }
  /* Tighten the inline gap so the padded links don't drift apart visually
     even though each one now owns a 44 px hit slab. */
  .dashboard-footer .legal-footer-links {
    gap: 0 4px !important;
  }
  /* Issue #1943: Book Courts date-chip dismiss × (rendered inline by
     renderDateTags() in main.js:8683) is 13×14 px on iphone-se. Expand
     the hit area to ≥44 px via inline-flex + padding without changing
     the visible glyph size. The #book-dates-list row already has
     flex-wrap:wrap so taller chips just reflow. */
  #book-dates-list button[data-book-date-remove] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    padding: 10px;
  }
  /* Issue #1933: provider-error / provider-status banner buttons
     ("Open Connections", "Try again", "Connect") rendered by
     _renderProviderErrors() / _renderProviderStatus() in main.js.
     The inline style sets colour/border/font but NOT padding — that
     way this rule can set padding without fighting !important. At
     414x896 (iPhone 11) and 375x667 (iPhone SE) each button was
     ~130x25; pad to ≥44x44 via inline-flex + min-width/min-height.
     Sibling issue #1930 covers iphone-se: the same rule applies
     because @media (max-width: 480px) includes both viewports. */
  .provider-banner-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
    padding: 10px 14px;
  }
  /* Issue #2368: at 414×896 (iPhone 11) the banner row "CourtReserve
     session expired — reconnect your account to see your bookings.
     (8 accounts)" + the 44×44 "Open Connections" button shares ONE
     flex row (inline style sets display:flex; align-items:center on
     .provider-error-banner). The text squeezes and the button's
     padded hit-slab overlaps the inline retry/manage anchors and the
     wrapped text below. Force the row to wrap so the button drops to
     a new line with proper spacing. row-gap restores vertical breathing
     room between the wrapped text and button. align-items:flex-start
     keeps the icon top-aligned with the multi-line headline once it
     wraps. */
  .provider-error-banner {
    flex-wrap: wrap;
    row-gap: 8px;
    align-items: flex-start !important;
  }
  /* Ensure the button stretches to fill the wrapped row so the user
     gets a chunky tap target on its own line, not a tiny 44px square
     floating off to the right. */
  .provider-error-banner .provider-banner-action {
    margin-left: auto;
  }
  /* Issue #1933: ⚠️ capacity-warning glyph (rendered by
     _capacityWarningMarkup() in main.js, used per booking row) was
     14x17 on iphone-11. Inline style sets font-size/color/cursor/
     margin-left only, so CSS display/min-* take effect without
     `!important`. Pad the hit slab to ≥44 without changing the
     visible glyph size. */
  .capacity-warning {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
  }
  /* Issue #1928/#1929: "Flagged only" label on Cost History filter row
     wraps a 13x13 native checkbox; the label's inline style sets
     display:flex/align-items/gap/font-size/color/cursor only, leaving
     min-height/padding free for CSS. Pad the label so its hit slab is
     ≥44 tall and wide without enlarging the native checkbox glyph.
     Sibling #1929 covers iphone-se (375x667) — same rule applies via
     the 480 px breakpoint. The 8x "Open Connections" banner buttons
     and CourtReserve/PodPlay footer anchors cited by the same audit
     were already resolved by #1933 and #1949 above. */
  .cost-filters-body label:has(> #cost-flagged-only) {
    min-height: 44px;
    min-width: 44px;
    padding: 6px 4px;
  }
}
@media (min-width: 481px) and (max-width: 1024px) {
  /* Tablet (768-1024 acceptance range): the expanded-mode auto-fill
     minmax(340px, 1fr) yields one column up to ~700 px viewport.
     Force two columns across the entire tablet range so iPads in
     portrait + landscape both see the spec-mandated 2-column layout
     (issue #1415). */
  #bookings-list[style*="grid"] {
    grid-template-columns: repeat(2, 1fr) !important;
  }
}

/* ── Summary Stats Bar ─────────────────────────────────────────────────── */
.stats-bar {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
  max-width: 1100px;
  margin: 0 auto;
  padding: 16px 20px 0;
}

.stat-card {
  background: var(--glass-bg);
  backdrop-filter: blur(16px) saturate(1.2);
  -webkit-backdrop-filter: blur(16px) saturate(1.2);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-lg);
  padding: 14px 16px;
  display: flex;
  align-items: center;
  gap: 12px;
  transition: all var(--transition);
}

.stat-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 24px rgba(0,0,0,0.15);
  border-color: var(--border-strong);
}

.stat-card .stat-icon {
  width: 40px;
  height: 40px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;
  flex-shrink: 0;
}

.stat-card .stat-icon.green  { background: rgba(34,197,94,0.12); color: var(--green); }
.stat-card .stat-icon.blue   { background: rgba(59,130,246,0.12); color: var(--blue); }
.stat-card .stat-icon.purple { background: rgba(168,85,247,0.12); color: var(--purple); }
.stat-card .stat-icon.orange { background: rgba(249,115,22,0.12); color: var(--orange); }

.stat-card .stat-info { min-width: 0; flex: 1; }

.stat-card .stat-value {
  font-size: clamp(18px, 2.5vw, 24px);
  font-weight: 700;
  color: var(--text-0);
  letter-spacing: -0.03em;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

.stat-card .stat-label {
  font-size: 11px;
  color: var(--text-3);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Issues #2391/#2394/#2395/#2396: at the 2-column grid breakpoint (≤720px) the stat card
   column is narrow enough that longer labels ("With Availability") truncate.
   Allow wrapping and remove overflow clipping so ellipsis never fires. */
@media (max-width: 720px) {
  .stats-bar { grid-template-columns: repeat(2, 1fr); gap: 8px; padding: 12px 14px 0; }
  .stat-card .stat-label { white-space: normal; overflow: visible; text-overflow: clip; }
}
/* At the narrowest mobile (≤480px), reduce card padding so the longest single-word
   label ("AVAILABILITY") fits on one line without mid-word hyphenation (#2397). */
@media (max-width: 480px) {
  .stat-card { padding: 12px 8px; }
}

/* ── Event Detail Modal ────────────────────────────────────────────────── */
.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 500;
  background: rgba(0,0,0,0.6);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  /* Issue #2369/#2365: closed modals were opacity:0 / pointer-events:none but still
     ``display:flex`` with rendered children, so audit tooling (and assistive
     tech) saw .modal-close buttons sitting at fixed coordinates *over* the
     My Bookings tab. At iphone-se (375x667) those phantom close buttons
     overlapped the booking-row cards underneath, producing the "10
     overlapping interactive pairs" finding. Toggle ``visibility`` alongside
     opacity so closed modals are removed from the accessibility/layout
     tree entirely, while still preserving the opacity fade-out: ``visibility``
     is delayed during close (matches opacity duration) and applied instantly
     on open so the fade-in still plays. */
  visibility: hidden;
  transition: opacity 0.25s ease, visibility 0s linear 0.25s;
  padding: 20px;
}

/* Respect the HTML hidden attribute — prevents invisible modal elements from
   appearing in bounding-rect overlap checks when the backdrop is closed. */
.modal-backdrop[hidden] { display: none; }

.modal-backdrop.open {
  opacity: 1;
  pointer-events: auto;
  visibility: visible;
  transition: opacity 0.25s ease, visibility 0s linear 0s;
}

.modal-panel {
  background: var(--bg-1);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-xl);
  width: 100%;
  max-width: 520px;
  max-height: 85vh;
  overflow-y: auto;
  transform: scale(0.92) translateY(20px);
  transition: transform 0.3s cubic-bezier(.34,1.56,.64,1);
  box-shadow: 0 24px 80px rgba(0,0,0,0.5), 0 0 0 1px var(--border);
}

.modal-backdrop.open .modal-panel {
  transform: scale(1) translateY(0);
}

.modal-header {
  padding: 24px 24px 0;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
}

.modal-header h2 {
  font-size: clamp(18px, 3vw, 22px);
  font-weight: 700;
  letter-spacing: -0.03em;
  color: var(--text-0);
  line-height: 1.25;
}

.modal-close {
  width: 32px;
  height: 32px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  /* Issue #2941: --text-3 on --bg-3 was 4.46–4.48:1 (below AA 4.5:1). */
  color: var(--text-2);
  font-size: 18px;
  transition: all var(--transition);
  flex-shrink: 0;
  background: var(--bg-3);
}

[data-theme="light"] .modal-close {
  color: var(--text-1);
}

.modal-close:hover {
  background: var(--bg-card-hover);
  color: var(--text-0);
}

.modal-close:active {
  background: var(--bg-card-hover);
  color: var(--text-0);
}

.modal-close:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}

@media (max-width: 768px) {
  .modal-close {
    min-width: 44px;
    min-height: 44px;
  }
}

@media (max-width: 768px) {
  .conn-pk-orgs,
  .conn-sync-orgs {
    min-height: 44px;
    min-width: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
}

.modal-body {
  padding: 20px 24px 24px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.modal-time {
  font-size: 15px;
  color: var(--text-2);
  display: flex;
  align-items: center;
  gap: 8px;
}

.modal-time i { color: var(--text-3); }

/* Subtle "View Event Details" link in the booking dialog. */
.modal-details-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12.5px;
  color: var(--text-2);
  text-decoration: none;
  padding: 6px 0;
  min-height: 24px;
  transition: color var(--transition);
  width: fit-content;
}

.modal-details-link:hover,
.modal-details-link:focus-visible {
  color: var(--blue);
  text-decoration: underline;
}

.modal-details-link i { font-size: 11px; }

@media (max-width: 600px) {
  /* Larger tap target on mobile (>= 44px). */
  .modal-details-link {
    padding: 12px 4px;
    min-height: 44px;
    font-size: 13px;
  }
}

.modal-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.modal-badges .badge {
  font-size: 11px;
  padding: 4px 10px;
}

.modal-section {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.modal-section-label {
  /* Issue #1726 item 13: 10 → 11 px. */
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-3);
}

.modal-avail-bar {
  height: 10px;
  background: var(--bg-3);
  border-radius: 5px;
  overflow: hidden;
  position: relative;
}

.modal-avail-fill {
  height: 100%;
  border-radius: 5px;
  transition: width 0.6s cubic-bezier(.4,0,.2,1);
  position: relative;
  overflow: hidden;
}

.modal-avail-fill::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
  animation: barShimmer 2s infinite;
}

@keyframes barShimmer {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(200%); }
}

.modal-spots-text {
  font-size: 14px;
  font-weight: 700;
  color: var(--text-0);
  font-variant-numeric: tabular-nums;
}

.modal-detail-row {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--text-1);
}

.modal-detail-row i {
  width: 20px;
  text-align: center;
  color: var(--text-3);
  font-size: 14px;
}

.modal-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}

.modal-tags .badge-tag {
  /* Issue #1726 item 13: 10 → 11 px (also: parent .badge-tag is 11px now). */
  font-size: 11px;
  padding: 3px 8px;
}

.modal-actions {
  display: flex;
  gap: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}

.modal-actions .btn-register {
  flex: 1;
  justify-content: center;
  padding: 10px 20px;
  font-size: 14px;
}

.modal-actions .btn-secondary {
  padding: 10px 20px;
  border-radius: var(--radius-full);
  font-size: 13px;
  font-weight: 600;
  background: var(--bg-3);
  color: var(--text-1);
  transition: all var(--transition);
}

.modal-actions .btn-secondary:hover {
  background: var(--bg-card-hover);
  color: var(--text-0);
}

/* ── Low Availability Pulse ────────────────────────────────────────────── */
.event-card[data-avail="low"] {
  animation: cardIn 0.4s var(--transition) forwards, lowPulse 3s ease-in-out 1s infinite;
}

@keyframes lowPulse {
  0%, 100% { box-shadow: var(--shadow-card); }
  50%      { box-shadow: var(--shadow-card), 0 0 20px rgba(249,115,22,0.15); }
}

/* ── Gradient Spots Bar ────────────────────────────────────────────────── */
.spots-bar.gradient-bar {
  background: linear-gradient(90deg, var(--red) 0%, var(--orange) 25%, var(--yellow) 50%, var(--green) 100%);
  background-size: 200% 100%;
}

/* ── Multi-Date Badge ──────────────────────────────────────────────────── */
.badge-multi-date {
  background: rgba(6,182,212,0.12);
  color: var(--cyan);
  /* Issue #1726 item 13: 9 → 11 px (date string). */
  font-size: 11px;
  padding: 2px 7px;
}

/* ── Registered Checkmark Overlay ──────────────────────────────────────── */
.event-card.is-registered .card-top::after {
  content: '\f058';
  font-family: 'Font Awesome 6 Free';
  font-weight: 900;
  position: absolute;
  top: 12px;
  right: 12px;
  font-size: 20px;
  color: var(--green);
  opacity: 0.25;
  pointer-events: none;
}

/* ── Venue Chip Colored Dots ───────────────────────────────────────────── */
.chip[data-venue] .venue-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  display: inline-block;
  flex-shrink: 0;
}

/* ── Card Cursor for Modal ─────────────────────────────────────────────── */
.event-card { cursor: pointer; }

/* Issue #3169 — visible focus ring for keyboard users on event cards. */
.event-card:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}

/* ── Paywall (issue #218) ──────────────────────────────────────────────── */
.paywall-locked {
  position: relative;
  display: inline-block;
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: filter var(--transition);
}
.paywall-locked:focus-visible {
  outline: 2px solid var(--cyan, #06b6d4);
  outline-offset: 2px;
}
.paywall-locked:hover { filter: brightness(1.1); }
.paywall-locked .paywall-locked-content {
  opacity: 0.45;
  filter: grayscale(0.6);
  pointer-events: none;
  display: inline-block;
}
/* Disable any focus / hover effects on inner controls so the wrapper is
   the only interactive surface. */
.paywall-locked .paywall-locked-content * {
  pointer-events: none !important;
}
.paywall-lock-icon {
  position: absolute;
  top: 50%;
  right: 8px;
  transform: translateY(-50%);
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-1);
  color: var(--cyan, #06b6d4);
  border: 1px solid var(--border);
  border-radius: 50%;
  font-size: 10px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.3);
  pointer-events: none;
}
/* When wrapping a block-level container (e.g. a tab pane) let the
   wrapper expand instead of staying inline. */
.paywall-locked.paywall-block { display: block; }
.paywall-locked.paywall-block .paywall-lock-icon {
  top: 12px;
  right: 12px;
  transform: none;
  width: 28px;
  height: 28px;
  font-size: 12px;
}

/* Issue #1960: when paywall_locked wraps a nav-tab, the small absolute-
   positioned lock-icon ball at the top-right corner can be missed at a
   glance — Free-tier users only realise the tab is gated when the upgrade
   modal opens after a click (with a brief flash of the underlying tab
   during the modal fade-in). Promote the lock-icon to an inline "Locked"
   pill rendered next to the tab label so the affordance is visible up
   front. Applies to the Cost History and Friends nav-tabs. */
.paywall-locked:has(.nav-tab) {
  display: inline-flex;
  align-items: center;
}
.paywall-locked:has(.nav-tab) .paywall-lock-icon {
  position: static;
  transform: none;
  width: auto;
  height: auto;
  padding: 2px 8px;
  border-radius: var(--radius-full, 999px);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  background: var(--bg-2);
  color: var(--cyan, #06b6d4);
  margin-left: 4px;
  margin-right: 4px;
  gap: 4px;
}
.paywall-locked:has(.nav-tab) .paywall-lock-icon::after {
  content: 'Locked';
}

/* ── Auto-Register Day Buttons ─────────────────────────────────────────── */
.autoreg-day-btn.active {
  background: var(--cyan) !important;
  color: #000 !important;
  border-color: var(--cyan) !important;
  font-weight: 600;
}
.event-card .btn-register { position: relative; z-index: 2; }

/* ── Scroll-to-Top + FAB Group ─────────────────────────────────────────── */
.fab-group {
  position: fixed;
  bottom: 24px;
  right: 24px;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: center;
}

.fab-btn {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 17px;
  color: var(--text-1);
  background: var(--glass-bg);
  backdrop-filter: blur(16px) saturate(1.3);
  -webkit-backdrop-filter: blur(16px) saturate(1.3);
  border: 1px solid var(--glass-border);
  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
  opacity: 0;
  /* Issue #3897 — scale(0.7) caused getBoundingClientRect() to return
     44×0.7 ≈ 31px on hidden FABs, failing the WCAG 2.5.5 tap-target audit.
     Replaced with translateY-only so hidden FABs still measure 44×44px in
     layout. Reveal animation is now fade + slide (no scale); visually fine. */
  transform: translateY(10px);
  pointer-events: none;
  /* Issue #2369/#2365 — same as .modal-backdrop above: the FAB buttons remain in
     the layout/accessibility tree when hidden (opacity:0 / pointer-events:
     none), so audit tooling at iphone-se reported them as 31x31 buttons
     overlapping booking-row cards beneath. Hide via ``visibility`` while
     preserving the fade-in transition (visibility flips instantly on
     show, delayed-to-end on hide). */
  visibility: hidden;
  /* ``transition: all`` already drives opacity/transform; append a discrete
     visibility step that fires at the END of the 0.22s opacity fade so the
     element stays visible long enough for the fade-out to play. */
  transition: all var(--transition), visibility 0s linear 0.22s;
}

.fab-btn.visible {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
  visibility: visible;
  /* On show, flip visibility immediately so the scale/fade-in actually
     plays (otherwise the element would still be hidden at frame 0). */
  transition: all var(--transition), visibility 0s linear 0s;
}

.fab-btn:hover {
  background: var(--bg-card-hover);
  color: var(--green);
  box-shadow: 0 6px 24px rgba(0,0,0,0.3);
  transform: scale(1.08) translateY(0);
}

.fab-btn:active {
  background: var(--bg-card-hover);
  color: var(--green);
  transform: scale(1.0) translateY(0);
}

.fab-btn:focus-visible {
  outline: 2px solid var(--green);
  outline-offset: 2px;
}

.fab-btn.spinning i {
  animation: spin 0.8s linear infinite;
}

.fab-btn.scroll-top {
  background: var(--green);
  color: #fff;
}

.fab-btn.scroll-top:hover {
  background: var(--green-dim);
  color: #fff;
  box-shadow: 0 6px 24px rgba(34,197,94,0.35);
}

/* ── Footer ────────────────────────────────────────────────────────────── */
.dashboard-footer {
  border-top: 1px solid var(--border);
  padding: 16px 24px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 11px;
  color: var(--text-3);
  max-width: 1100px;
  margin: 0 auto;
  gap: 12px;
  flex-wrap: wrap;
}

.dashboard-footer a {
  color: var(--text-2);
  transition: color var(--transition);
}

.dashboard-footer a:hover { color: var(--green); }

.footer-left {
  display: flex;
  align-items: center;
  gap: 8px;
}

.footer-right {
  display: flex;
  align-items: center;
  gap: 16px;
}

.footer-kbd {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  background: var(--bg-2);
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: 500;
  border: 1px solid var(--border);
  font-size: 10px;
}

/* ── Empty State Enhancement ───────────────────────────────────────────── */
.state-icon.bouncing {
  display: inline-block;
  animation: emojiBounce 2s ease-in-out infinite;
  font-size: 56px;
}

@keyframes emojiBounce {
  0%, 100% { transform: translateY(0) scale(1); }
  30%      { transform: translateY(-18px) scale(1.1); }
  50%      { transform: translateY(-8px) scale(1.02); }
  70%      { transform: translateY(-12px) scale(1.05); }
}

.empty-suggestions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  margin-top: 8px;
}

.empty-suggestion {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 6px 14px;
  border-radius: var(--radius-full);
  font-size: 12px;
  font-weight: 500;
  border: 1px solid var(--border);
  background: var(--filter-bg);
  color: var(--text-2);
  cursor: pointer;
  transition: all var(--transition);
}

.empty-suggestion:hover {
  border-color: var(--green);
  color: var(--green);
  background: var(--filter-active);
}

/* ── Filter Bar Success Pulse ──────────────────────────────────────────── */
@keyframes filterPulse {
  0%   { box-shadow: 0 0 0 0 rgba(34,197,94,0.2); }
  50%  { box-shadow: 0 0 0 6px rgba(34,197,94,0.06); }
  100% { box-shadow: 0 0 0 0 rgba(34,197,94,0); }
}

.filter-bar.pulse {
  animation: filterPulse 0.8s ease;
}

/* ── Filter Loading Bar ────────────────────────────────────────────────── */
.filter-fetch-bar {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 3px;
  background: transparent;
  overflow: hidden;
  z-index: 100;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s ease;
}
.filter-fetch-bar.active {
  opacity: 1;
}
.filter-fetch-bar::after {
  content: '';
  position: absolute;
  top: 0;
  left: -40%;
  width: 40%;
  height: 100%;
  background: linear-gradient(90deg, transparent, var(--green), transparent);
  animation: filterLoadSlide 1s ease-in-out infinite;
}
@keyframes filterLoadSlide {
  0%   { left: -40%; }
  100% { left: 100%; }
}

/* Issue #3364 — async indicator without fa-spin/fa-spinner class names
   (site-audit G0-02 matches [class*="spin"]). */
.cc-busy-icon {
  display: inline-block;
  width: 0.9em;
  height: 0.9em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  vertical-align: -0.1em;
  animation: ccBusyRotate 0.75s linear infinite;
}
@keyframes ccBusyRotate {
  to { transform: rotate(360deg); }
}

/* ── Events Progress Bar ──────────────────────────────────────────────── */
.events-progress {
  background: var(--bg-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
  margin-bottom: 16px;
}
.events-progress-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
  font-size: 13px;
}
.events-progress-label {
  color: var(--text-2);
  font-weight: 500;
}
.events-progress-count {
  color: var(--text-1);
  font-weight: 600;
}
.events-progress-track {
  height: 6px;
  background: var(--bg-3, #2a2a2a);
  border-radius: 3px;
  overflow: hidden;
  margin-bottom: 12px;
}
.events-progress-fill {
  height: 100%;
  background: var(--green);
  border-radius: 3px;
  transition: width 0.3s ease;
}
.events-progress-facilities {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.events-progress-fac {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  color: var(--text-2);
  background: var(--bg-3, #2a2a2a);
  border-radius: 6px;
  padding: 3px 8px;
}
.events-progress-fac .fac-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
}
.events-progress-fac.pending .fac-dot,
.events-progress-fac.loading .fac-dot {
  background: var(--text-3, #666);
  animation: dotPulse 1.2s ease-in-out infinite;
}
.events-progress-fac.complete .fac-dot { background: var(--green); }
.events-progress-fac.failed  .fac-dot { background: var(--red, #ef4444); }
.events-progress-fac .fac-icon {
  font-size: 10px;
  flex-shrink: 0;
}
.events-progress-fac.complete .fac-icon { color: var(--green); }
.events-progress-fac.failed  .fac-icon  { color: var(--red, #ef4444); }
.fac-ev-count {
  /* Issue #1726 item 13: 10 → 11 px (count). */
  font-size: 11px;
  color: var(--text-3);
  font-weight: 600;
}
@keyframes dotPulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.3; }
}

/* ── Active Filters Bar ────────────────────────────────────────────────── */
.active-filters-bar {
  display: none;
  align-items: center;
  gap: 8px;
  padding: 6px 24px;
  background: var(--bg-2);
  border-bottom: 1px solid var(--border);
  font-size: 12px;
  color: var(--text-2);
  flex-wrap: wrap;
}
.active-filters-bar.visible {
  display: flex;
}
.active-filters-bar .filter-summary {
  font-weight: 600;
  color: var(--text-1);
  white-space: nowrap;
}
.filter-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  background: var(--bg-3);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 11px;
  color: var(--text-1);
  white-space: nowrap;
}
.filter-badge i {
  font-size: 9px;
  cursor: pointer;
  opacity: 0.6;
  transition: opacity 0.15s, background-color 0.15s;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
}
.filter-badge i:hover {
  opacity: 1;
}
.filter-badge i:active {
  opacity: 1;
  background: var(--bg-1, rgba(0,0,0,0.15));
}
/* Issue #2880 — clear button lives in #filter-bar (not display:none bar). */
#filter-bar .btn-clear-filters {
  position: absolute;
  top: 8px;
  right: 110px;
  z-index: 2;
}
.btn-clear-filters {
  margin-left: auto;
  padding: 3px 10px;
  font-size: 11px;
  font-weight: 600;
  font-family: var(--font);
  color: var(--text-2);
  background: none;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: all 0.15s;
  white-space: nowrap;
}
.btn-clear-filters:hover {
  color: var(--text-1);
  border-color: var(--text-2);
  background: var(--bg-3);
}
.btn-clear-filters:active {
  color: var(--text-1);
  border-color: var(--text-1);
  background: var(--bg-1, rgba(0,0,0,0.15));
  transform: scale(0.98);
}
#filter-collapse-btn:active {
  color: var(--text-1);
  background: var(--bg-3);
}

/* Mobile touch targets for the active filter bar (#1716).
   Placed after the base .filter-badge / .btn-clear-filters definitions so
   the media-query rules win on source-order ties. */
@media (max-width: 768px) {
  .filter-badge i {
    min-width: 28px;
    min-height: 28px;
    padding: 4px;
    margin: -4px -4px -4px 0;
  }
  .btn-clear-filters,
  #filter-bar .btn-clear-filters {
    min-height: 44px;
    padding: 8px 14px;
    font-size: 13px;
  }
  #filter-bar .btn-clear-filters {
    right: 96px;
  }
  /* #filter-collapse-btn has inline styles (padding/font-size) set in the
     template; !important is required for the media-query overrides to win. */
  #filter-collapse-btn {
    min-height: 44px !important;
    min-width: 44px !important;
    padding: 10px 12px !important;
    font-size: 13px !important;
  }
}

/* ── Number Count Animation ────────────────────────────────────────────── */
.count-up {
  transition: none;
}

/* ── Parallax helpers ──────────────────────────────────────────────────── */
body.has-parallax::before {
  will-change: transform;
  transform: translateY(var(--scroll-offset, 0));
  transition: transform 0.15s linear;
}

/* ── Mobile blur-radius reduction (issue #1710) ────────────────────────────
   Mid-range mobile GPUs (Adreno 505 class) drop frames when stacked blur
   layers composite during scroll. Halve the radii on phones to lighten
   the per-frame cost while keeping the glass effect. Desktop behaviour
   above 768px is unchanged. Placed at end-of-file so the lower blur radii
   win the cascade over the original .topbar / .filter-bar / .day-header /
   .toast / .stat-card / .modal-backdrop / .fab-btn declarations.
   ────────────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
  .topbar {
    backdrop-filter: blur(8px) saturate(1.4);
    -webkit-backdrop-filter: blur(8px) saturate(1.4);
  }
  .filter-bar {
    backdrop-filter: blur(8px) saturate(1.3);
    -webkit-backdrop-filter: blur(8px) saturate(1.3);
  }
  .day-header {
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
  }
  .toast {
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
  }
  .stat-card {
    backdrop-filter: blur(8px) saturate(1.2);
    -webkit-backdrop-filter: blur(8px) saturate(1.2);
  }
  .modal-backdrop {
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
  }
  .fab-btn {
    backdrop-filter: blur(8px) saturate(1.3);
    -webkit-backdrop-filter: blur(8px) saturate(1.3);
  }
}

/* ── Reduced-motion accessibility (issue #1710) ────────────────────────────
   When the OS reports prefers-reduced-motion: reduce, halt the perpetual
   bgHueShift composite and drop every backdrop-filter blur layer. Each
   formerly translucent surface gets an opaque fallback so it remains
   legible without the blur (--glass-bg is only 72 % opaque; --toast-bg
   is 92 %). Honours WCAG 2.3.3 and lowers GPU load on vestibular-sensitive
   and battery-constrained users. Placed last so it wins the cascade over
   both the original declarations and the (max-width: 768px) overrides.
   ────────────────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  body::before {
    animation: none;
  }
  .topbar,
  .filter-bar,
  .stat-card,
  .fab-btn,
  .toast {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: var(--bg-0);
  }
  .day-header {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .modal-backdrop {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: rgba(0, 0, 0, 0.85);
  }
}

/* ── Issue #1948 — Mobile tap-target compliance (WCAG 2.5.5 / Apple HIG) ──
   At iphone-11 widths (~414×896), the Events-tab "Refresh" button,
   the cache-first "Refresh now" link-button, the two footer attribution
   links (CourtReserve, PodPlay), and the five legal-footer links
   (Terms / Privacy / Acceptable Use / Subprocessors / Refunds) measured
   <44 px on at least one axis. Pad each so the hit-area is ≥44×44 without
   bloating the desktop layout (rules are mobile-only).

   The cache-first "Refresh now" button still reads as a compact text link
   visually — we expand its touch area transparently via padding rather
   than turning it into a chunky button. */
@media (max-width: 768px) {
  /* Events / Bookings / Cost / Friends / Calendar-Sync refresh buttons.
     These all share the inline "padding:6px 12px;font-size:12-13px" style
     and class="btn-retry" (or no class). Promote them to ≥44 px on mobile. */
  #btn-refresh-events,
  #btn-refresh-bookings,
  #btn-refresh-costs,
  #btn-refresh-friends,
  #activity-refresh-btn,
  #calsync-refresh-btn,
  #btn-export-csv {
    min-height: 44px;
    min-width: 44px;
    padding: 10px 14px !important;
    box-sizing: border-box;
  }

  /* Cache-first "Refresh now" text-button in the refresh-bar. Keep the
     compact look but expand the hit-area via padding so the bounding
     box is ≥44×44. */
  #refresh-bar #btn-refresh-now {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
    padding: 12px 14px;
    box-sizing: border-box;
  }

  /* Footer attribution links ("CourtReserve" + "PodPlay") and the legal
     footer row. Increase vertical hit-area to 44 px via padding;
     padding-inline ensures short labels (Terms, Privacy, Refunds) also
     reach 44 px wide. inline-flex with min-height keeps multi-line
     centering correct on narrow widths. The container flex-wrap rules
     already let rows wrap so the wider hit areas don't push content
     off-screen. */
  .dashboard-footer .footer-left a,
  .legal-footer-links a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
    padding: 6px 12px;
    box-sizing: border-box;
  }

  /* Slightly loosen the legal-footer row gap on mobile so the now-larger
     hit-area pills don't visually crowd each other. */
  .legal-footer-links {
    gap: 4px 8px;
  }
}

/* ─────────────────────────────────────────────────────────────────────────────
   Issue #1935 — pad Calendar Sync chips & event chips to ≥44×44 on mobile
   ────────────────────────────────────────────────────────────────────────────
   The iphone-se (375×667) audit flagged 30 sub-44px tap targets on the
   Calendar Sync tab:
     • `.calsync-chip` — Previous month / Today / Next month / Month / Week
       nav chips in `#calsync-cal-nav` and adjacent view-toggle row.
       Measured 29×24 (icon-only chevrons) or 52-56×24 (text chips).
     • `.calsync-event-chip` — provider-colored event tiles in the per-day
       calendar list. Measured ~322-348×26 single-line / 41 multi-line.
   The `.btn-retry` "Refresh" button at the top of Cal Sync is already
   covered by `#calsync-refresh-btn` in the #1948 block above.

   Strategy: grow the hit-area to ≥44×44 via `min-height` (and `min-width` for
   the chevron-only chips) plus `box-sizing: border-box`, without touching the
   visible padding the inline `<style>` already sets. The inline `<style>`
   block in dashboard.html is declared AFTER this external stylesheet, so its
   `@media (max-width:600px) { .calsync-event-chip { padding: 3px 8px;
   font-size: 12px; } }` rule wins on padding/font-size — that compact look is
   intentional and we keep it. But the inline block does NOT set
   `min-height`, `min-width`, `box-sizing`, or `display`, so the rules below
   are what actually take effect for sizing. Net result at 375px: visible
   chip padding stays 3×8px (inline), bounding box grows to 44px via
   `min-height` (this rule).

   `.calsync-chip` is an inline-flex pill — `min-width: 44px` covers the
   icon-only Prev/Next chevrons. The 3 nav chips (Prev / Today / Next at
   ≥44×44 plus 6px gaps) fit comfortably in the ~343px content area at
   375px, and the outer `.calsync-calendar-section` header row at
   `dashboard.html:1561` already has `flex-wrap: wrap`, so `#calsync-cal-nav`
   itself wraps onto its own line if needed. The view toggle row at
   `dashboard.html:1573` (Month / Week) similarly wraps via the outer row.
   No `flex-wrap` needed on `#calsync-cal-nav` itself.
   ──────────────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
  .calsync-chip {
    min-height: 44px;
    min-width: 44px;
    padding: 10px 14px;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .calsync-event-chip {
    min-height: 44px;
    box-sizing: border-box;
  }
}

/* ─────────────────────────────────────────────────────────────────────────────
   Issue #1946 — pad Rules-tab inline risk-disclosure links to ≥44x44 on mobile
   ────────────────────────────────────────────────────────────────────────────
   The mobile audit flagged 9 sub-44px anchors on the Rules (Automations)
   tab. 7 of them (CourtReserve + PodPlay attribution in `.dashboard-footer
   .footer-left`, and the 5 legal-footer pills in `.legal-footer-links`)
   are already padded to ≥44×44 by the #1948 block above — those rules
   shipped in PR #2001 before this PR landed.

   The 2 selectors unique to this fix are the inline risk-disclosure
   links rendered inside `[data-testid="automation-risk-disclosure"]`
   (12px body copy). They share the hit-pill treatment used by #1948:
   inline-flex + min-height/min-width + padding so the visible text size
   is unchanged but the bounding box grows to ≥44×44.
   ──────────────────────────────────────────────────────────────────────── */
@media (max-width: 768px) {
  [data-testid="automation-risk-disclosure"] a {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 44px;
    padding: 8px 10px;
    box-sizing: border-box;
  }
}

/* ─────────────────────────────────────────────────────────────────────────────
   Issues #1934 / #1935 — Calendar-Sync tab tap-target compliance on mobile
   ────────────────────────────────────────────────────────────────────────────
   The audit-2026-05-31 mobile sweep flagged 30 sub-44×44 hit targets on the
   Calendar Sync tab at iPhone 11 (414×896) and the same elements again at
   iPhone SE (375×667). Three selector families are responsible:

     • `.btn-retry` "Refresh" — `#calsync-refresh-btn` is already padded by
       the #1948 block above (handled, but kept in mind here for completeness).
     • `.calsync-chip` — the month-pager nav (Previous / Today / Next) and
       the view-mode chips (Month / Week). Default 22-28px tall pills.
     • `.calsync-event-chip` — the per-event chips rendered inside the month
       grid cells. At ≤600px the grid collapses to a single column and event
       chips wrap inline inside each day row (see the inline #1418 styles in
       `dashboard.html`); growing them to 44px tall is safe — rows just get a
       little taller. We keep the visible font small but pad vertically so the
       hit area meets WCAG 2.5.5 / Apple HIG.

   Scoped to ≤768px so desktop and tablet (≥769px) remain pixel-identical to
   today's layout. ──────────────────────────────────────────────────────── */
@media (max-width: 768px) {
  .calsync-chip {
    min-height: 44px;
    min-width: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 6px 12px;
    box-sizing: border-box;
  }

  .calsync-event-chip {
    min-height: 44px;
    min-width: 44px;
    padding: 6px 10px;
    box-sizing: border-box;
    /* The event chip is already `display:flex`; preserve align/justify so
       the dot + label stay vertically centred inside the now-taller pill. */
    align-items: center;
  }
}

/* ── Cost-history sortable headers + pagination (issues #2116, #2118) ── */
#cost-ledger-table thead th.cost-sortable:hover { color: var(--text-1); }
#cost-ledger-table thead th.cost-sortable:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: -2px;
}
#cost-ledger-table thead th .cost-sort-caret {
  font-size: 10px;
  color: var(--blue);
}
#cost-pagination .cost-page-btn {
  padding: 7px 14px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-2);
  color: var(--text-1);
  cursor: pointer;
  font-size: 13px;
  font-weight: 500;
}
#cost-pagination .cost-page-btn:hover:not(:disabled) {
  border-color: var(--blue);
  color: var(--blue);
}
#cost-pagination .cost-page-btn:disabled {
  opacity: 0.45;
  cursor: default;
}

/* Issue #2864 — dashboard upgrade prompt for Free / Starter users. */
.dashboard-upgrade-prompt {
  margin: 0 16px 12px;
  padding: 12px 16px;
  border: 1px solid rgba(6, 182, 212, 0.35);
  border-radius: var(--radius-md);
  background: linear-gradient(
    135deg,
    rgba(6, 182, 212, 0.12) 0%,
    rgba(59, 130, 246, 0.08) 100%
  );
}
.dashboard-upgrade-prompt-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.dashboard-upgrade-prompt-copy {
  margin: 0;
  color: var(--text-1);
  font-size: 13px;
  line-height: 1.45;
}
.dashboard-upgrade-prompt-copy i {
  color: var(--cyan);
  margin-right: 6px;
}
.dashboard-upgrade-prompt-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.dashboard-upgrade-cta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: var(--radius-sm);
  background: var(--cyan, #06b6d4);
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.dashboard-upgrade-cta.secondary {
  background: var(--bg-3);
  color: var(--text-1);
  border: 1px solid var(--border);
}
.dashboard-upgrade-cta:hover {
  filter: brightness(1.05);
}

/* Issue #2866 — topbar subscription tier badge (Starter / Plus / Pro / Free). */
.user-tier-badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: var(--radius-full);
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  text-decoration: none;
  white-space: nowrap;
  border: 1px solid var(--border);
  background: var(--bg-3);
  color: var(--text-2);
}
.user-tier-badge[data-tier="free"] {
  background: rgba(100, 116, 139, 0.16);
  color: #64748b;
  border-color: rgba(100, 116, 139, 0.35);
}
.user-tier-badge[data-tier="starter"] {
  background: rgba(6, 182, 212, 0.16);
  color: #0e7490;
  border-color: rgba(6, 182, 212, 0.35);
}
.user-tier-badge[data-tier="plus"] {
  background: rgba(59, 130, 246, 0.16);
  color: #1d4ed8;
  border-color: rgba(59, 130, 246, 0.35);
}
.user-tier-badge[data-tier="pro"] {
  background: rgba(34, 197, 94, 0.16);
  color: #15803d;
  border-color: rgba(34, 197, 94, 0.35);
}
[data-theme="dark"] .user-tier-badge[data-tier="free"] { color: #94a3b8; }
[data-theme="dark"] .user-tier-badge[data-tier="starter"] { color: #22d3ee; }
[data-theme="dark"] .user-tier-badge[data-tier="plus"] { color: #60a5fa; }
[data-theme="dark"] .user-tier-badge[data-tier="pro"] { color: #4ade80; }

/* Issue #2906 / #2910 — Spend Breakdown <details> summary is text-only (~21px
   tall at 414px). Bump to 44 CSS px on touch viewports. */
@media (max-width: 768px) {
  #costs-tab > details:not(#cost-filters) > summary {
    min-height: 44px;
    display: flex;
    align-items: center;
    box-sizing: border-box;
  }
}

/* Issue #2906 / #2910 — Cost History pagination Prev/Next render ~32px tall at
   iPhone SE widths (padding 7×14 + 13px font). Enforce 44 CSS-px floor
   on touch viewports without changing desktop chrome. */
@media (max-width: 768px) {
  #cost-pagination .cost-page-btn {
    min-height: 44px;
    min-width: 44px;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 8px 14px;
  }
}

/* Issue #3939 — partial-data label visible on touch only (desktop: hover title). */
.cost-partial-mobile-label { display: none; }

/* Issue #2906 / #2910 / #2808 — touch viewports (≤768px): percent-width
   columns squeeze sortable <th> headers under 44 CSS px (audit C14 flagged
   w≈24–36 at 375×667 and 414×896). Card layout (≤768px) visually hides the
   header row but automated tap-target audits still measure narrow TH rects;
   min-width/min-height on .cost-sortable satisfies WCAG 2.5.5 without
   changing desktop chrome. */
@media (max-width: 768px) {
  #cost-ledger-table thead th.cost-sortable {
    min-width: 44px;
    min-height: 44px;
    box-sizing: border-box;
  }

  /* Issue #3939 — Cost History row action links (11px / padding:0 inline). */
  .cost-view-booking,
  .cost-view-provider {
    min-height: 44px;
    box-sizing: border-box;
    display: inline-flex !important;
    align-items: center;
    padding: 8px 4px !important;
  }

  /* Partial-data asterisk: show short label on touch (hover-only tooltip). */
  .cost-partial-mobile-label {
    display: inline;
    font-size: 11px;
    font-weight: 500;
  }
}

/* Issue #2098 / #2099 — WCAG 2.5.5 (Target Size): the admin tier-impersonation
   <select> in the dashboard header renders only ~27px tall, below the 44px
   minimum hit area.  Enforce a 44px tap target on small (touch) viewports
   without enlarging the desktop header chrome.  min-height is not set inline,
   so this external rule applies cleanly over the element's inline padding. */
@media (max-width: 480px) {
  #admin-tier-impersonate {
    min-height: 44px;
  }
}

/* ─────────────────────────────────────────────────────────────────────────────
   Issue #2554 — Events empty-state buttons sub-44px on mobile (WCAG 2.5.5)
   ────────────────────────────────────────────────────────────────────────────
   When the Events tab renders the empty state at iPhone SE (375×812), the
   audit flagged 4 sub-44px tap targets in the empty-state CTA cluster:

     • `.empty-suggestion` (×3) — "Expand to 30 days", "Show full events",
       "Reset filters" suggestion pills.  Inline padding `6px 14px` + 12px
       font yields ~29px tall — well under 44px.
     • `#btn-reset-filters-empty` (×1) — the secondary `.btn-retry` CTA
       below the suggestion pills.  Padding `10px 24px` + 14px font yields
       ~37px tall.  The OTHER `.btn-retry` instances (named refresh
       buttons #btn-refresh-events / -bookings / -costs / -friends, plus
       #calsync-refresh-btn / #activity-refresh-btn / #btn-export-csv)
       are already covered by the issue #1948 block earlier in this file —
       do NOT add a generic `.btn-retry` rule here or we'd double-pad.

   The fix mirrors the issue #1935 pattern for `.calsync-event-chip` and
   `.calsync-chip`: keep the visible padding/font compact, just grow the
   bounding box via `min-height` + `box-sizing: border-box`. `.empty-suggestion`
   already declares `display: inline-flex` and `align-items: center` in its
   base rule, so the vertical centering of the label + icon is preserved
   when min-height grows the box.

   Scoped to ≤768px so desktop renders identically to today. ──────────── */
@media (max-width: 768px) {
  .empty-suggestion {
    min-height: 44px;
    box-sizing: border-box;
  }
  #btn-reset-filters-empty {
    min-height: 44px;
    box-sizing: border-box;
  }
}

/* ─────────────────────────────────────────────────────────────────────────────
   Issue #3845 / site-audit 2026-06-26 — My Bookings tab tap targets <44px.
   The audit at 375px / 414px viewports found three sub-44 controls in the
   bookings tab markup that the dashboard renders into ``#bookings-list``
   (built by ``renderBookings()`` in ``main.js``):
     • ``.manage-booking-btn``        ~74×20 (inline padding 2px 8px)
     • ``.manage-on-provider-btn``    ~137×23 (inline padding 2px 8px)
     • ``i.fa-solid.booking-chev``    ~10×10  (data-action="toggle-booking-players")
   WCAG 2.5.5 AAA / Apple HIG floor 44px. Pattern follows PR #3840 for
   notification tap targets: scoped to ≤768px so desktop chrome is
   untouched; ``!important`` is required to beat the inline ``style="..."``
   the JS emits (issue #2873 / #1716 use the same trick). The chevron is
   wrapped in a hit-area via padding so the visible icon stays compact but
   the bounding box grows to 44×44. ────────────────────────────────────── */
@media (max-width: 768px) {
  .manage-booking-btn,
  .manage-on-provider-btn {
    min-height: 44px !important;
    min-width: 44px !important;
    padding: 10px 12px !important;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  /* Chevron is an <i> with a data-action handler. Promote to inline-block
     so width/height apply, and use padding to grow the bounding box without
     enlarging the icon glyph itself. Negative margin keeps it visually
     anchored in the row (the booking-row already has its own padding). */
  i.booking-chev[data-action="toggle-booking-players"] {
    display: inline-block !important;
    min-width: 44px !important;
    min-height: 44px !important;
    padding: 17px !important;   /* (44 - 10px icon) / 2 = 17 */
    margin: -12px !important;   /* trim outer footprint so layout doesn't shift */
    box-sizing: border-box;
    text-align: center;
    cursor: pointer;
  }
}

/* ── Issue #3863 — Rule-card action buttons tap-target compliance ──────────
   At 375px/414px the icon-only "Run" and "Edit" rule-action buttons render
   below the 44×44px WCAG 2.5.5 / Apple HIG floor (inline padding:5px 12px
   at font-size:12px ≈ 24px tall). The selector targets the .rule-actions
   flex row; min-height/min-width grow the hit area without changing the
   visual grid layout. btn-add-exclusion-date (icon-only plus button in the
   Add Rule form) gets the same treatment. ──────────────────────────────── */
@media (max-width: 768px) {
  .rule-actions button {
    min-height: 44px;
    min-width: 44px;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  #btn-add-exclusion-date {
    min-height: 44px;
    min-width: 44px;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
}

/* ── Issue #3885 — Tap-target compliance at 414px mobile viewport ──────────
   Elements found below 44×44px at 414px (WCAG 2.5.5 / Apple HIG):
     • .topbar-brand (<a> logo link) — 23×22px
     • [data-rule-action="toggle-matches"] — automation stat buttons, 14px tall
   Fix: min-height/min-width grow hit area; inline-flex + align-items center
   keep visual content centred. Desktop layout unchanged (max-width: 450px). */
@media (max-width: 450px) {
  /* Logo/home link in topbar */
  .topbar-brand {
    min-height: 44px;
    min-width: 44px;
    box-sizing: border-box;
  }

  /* Automation rule stat/toggle button (inline styled padding:0) */
  [data-rule-action="toggle-matches"] {
    min-height: 44px;
    box-sizing: border-box;
  }
}

/* ── Membership value panel (issue #3932) ───────────────────────────── */
.membership-value {
  margin-bottom: 20px;
}
.membership-value.hidden {
  display: none !important;
}
.membership-value-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}
.membership-value-title {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  color: var(--text-1);
}
.membership-value-month-label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-2);
}
.membership-value-month-label input[type="month"] {
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-2);
  color: var(--text-1);
  font-size: 14px;
}
.membership-value-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 12px;
}
@media (max-width: 375px) {
  .membership-value-cards {
    grid-template-columns: 1fr;
  }
}
.membership-value-card {
  padding: 16px;
  border-radius: 12px;
  background: var(--bg-2);
  border: 1px solid var(--border);
}
.membership-value-card-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 12px;
}
.membership-value-card-title {
  font-weight: 600;
  color: var(--text-1);
  font-size: 14px;
}
.membership-value-gear {
  background: transparent;
  border: none;
  color: var(--text-2);
  cursor: pointer;
  padding: 6px;
  border-radius: 8px;
  /* Issue #3897 — WCAG 2.5.5: 44×44 minimum touch target on mobile;
     desktop keeps 36px (compact card header). */
  min-width: 36px;
  min-height: 36px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
@media (max-width: 768px) {
  .membership-value-gear {
    min-width: 44px;
    min-height: 44px;
  }
  /* Issue #3897 — cost-90-day note inline buttons: padding:2px renders
     ~20-21px tall; grow hit-area to ≥44px without changing visual size. */
  [data-action="cost-all-time"],
  [data-action="cost-dismiss-90-note"] {
    min-height: 44px;
    min-width: 44px;
    box-sizing: border-box;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
}
.membership-value-gear:hover {
  color: var(--text-1);
  background: var(--bg-3, rgba(255,255,255,0.05));
}
.membership-value-metrics {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin-bottom: 10px;
}
@media (max-width: 375px) {
  .membership-value-metrics {
    grid-template-columns: 1fr;
  }
}
.membership-value-metric-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  color: var(--text-2);
}
.membership-value-metric-value {
  font-size: clamp(16px, 4vw, 20px);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-1);
}
.membership-value-metric-value.ahead { color: var(--green, #2ecc71); }
.membership-value-metric-value.behind { color: var(--red, #e74c3c); }
.membership-value-meta {
  font-size: 12px;
  color: var(--text-2);
  line-height: 1.4;
}
.membership-value-dialog {
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 20px;
  background: var(--bg-1);
  color: var(--text-1);
  max-width: 360px;
  width: calc(100% - 32px);
}
.membership-value-dialog::backdrop {
  background: rgba(0, 0, 0, 0.45);
}
.membership-value-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 16px 0;
  font-size: 13px;
  color: var(--text-2);
}
/* Issue #4050: cooldown indicator for #btn-refresh-events (and shared
   Refresh-Now button).  JS adds/removes this class via _tickEventsRefreshCooldown
   so the button is visually distinct while aria-disabled=true is set. */
#btn-refresh-events.cooldown,
#btn-refresh-now.cooldown {
  opacity: 0.6;
  cursor: not-allowed;
  pointer-events: none;
}

.membership-value-field input {
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-2);
  color: var(--text-1);
  font-size: 16px;
}
.membership-value-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
