/* ============================================================
   Qual — Tactical Theme (dark default, light opt-in)
   Font: Inter | No all-caps | Sharp edges
   ============================================================ */

:root {
  --accent:       #f9832e;   /* orange — nudged from the original #f97316 to clear
                                 WCAG AAA (7:1) against all three dark surfaces,
                                 not just the darkest one (see contrast-report.md) */
  --accent-hover: #fa9a55;
  --accent-dim:   rgba(249, 131, 46, 0.12);
  --gold:         #eab308;
  --gold-dim:     rgba(234, 179, 8, 0.12);
  --green:        #22c55e;
  --red:          #ef4444;
  --blue:         #60a5fa;

  --bg:           #09090b;
  --bg2:          #111113;
  --bg3:          #18181b;
  --bg4:          #1f1f23;
  --border:       #27272a;
  --border-hi:    #3f3f46;

  --text:         #f4f4f5;
  --text-2:       #a1a1aa;
  --text-3:       #86868f;

  --radius:       0px;   /* sharp */
  --radius-sm:    0px;
  --shadow:       0 4px 24px rgba(0,0,0,0.5);

  --font: 'Inter', system-ui, -apple-system, sans-serif;
}

/* ── Light theme ──────────────────────────────────────────────
   Dark is the unchanged default above. Light applies when either
   the OS prefers it (and the visitor hasn't made an explicit choice
   yet) or the nav toggle has set data-theme="light" explicitly —
   the explicit attribute always wins over the media query. Built
   for outdoor/sunlight legibility, not a naive color inversion —
   see contrast-report.md for the reasoning and the numbers. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    /* Navy accent — complementary to the dark theme's orange (opposite side
       of the wheel), not a darkened version of it. All five brand colors
       here are held to WCAG AAA (7:1, not just the 4.5:1 AA floor) against
       the darkest of the three surfaces below, since contrast-report.md
       showed thin AA-only margins are exactly what fails fastest under
       simulated sunlight glare — see that file for the numbers.
       --blue is deliberately shifted toward cyan rather than sharing the
       accent's navy hue, so a semantic "info" badge doesn't read as an
       accidental second brand color. --accent-hover stays lighter/more
       vivid for the momentary hover state, not held to the same bar since
       it's not sustained reading text. */
    --accent:       #164f9b;
    --accent-hover: #246ccb;
    --accent-dim:   rgba(22, 79, 155, 0.12);
    --gold:         #654e03;
    --green:        #105d2d;
    --red:          #a40e0e;
    --blue:         #09586f;

    /* Cool, barely-tinted near-white — hue-biased toward the navy accent
       (same principle as the old warm-orange-biased neutrals, just
       repointed at the new accent family instead of a naive gray). */
    --bg:           #f8f8fa;
    --bg2:          #eff1f3;
    --bg3:          #ffffff;
    --bg4:          #e8eaed;
    --border:       #dce0e5;
    --border-hi:    #c0c6ce;

    --text:         #18181b;
    --text-2:       #505059;
    --text-3:       #67677f;

    --shadow:       0 4px 24px rgba(24,24,27,0.12);
  }
}

:root[data-theme="light"] {
  --accent:       #164f9b;
  --accent-hover: #246ccb;
  --accent-dim:   rgba(22, 79, 155, 0.12);
  --gold:         #654e03;
  --green:        #105d2d;
  --red:          #a40e0e;
  --blue:         #09586f;

  --bg:           #f8f8fa;
  --bg2:          #eff1f3;
  --bg3:          #ffffff;
  --bg4:          #e8eaed;
  --border:       #dce0e5;
  --border-hi:    #c0c6ce;

  --text:         #18181b;
  --text-2:       #505059;
  --text-3:       #67677f;

  --shadow:       0 4px 24px rgba(24,24,27,0.12);
}

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

html, body {
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* Defensive backstop against a tiny (1-2px) real-device-only horizontal
   overflow on mobile. Scoped to `html` only, NOT `body` — on `body` too,
   Chromium turns `body` into its own clipping/compositing layer, which
   painted `.lp-ambient-glow` (position:fixed, z-index:-1) behind `body`'s
   own background instead of behind its sibling content, invisibly. */
html {
  overflow-x: hidden;
}

a { color: inherit; text-decoration: none; }
button { cursor: pointer; font-family: var(--font); }
input, select, textarea { font-family: var(--font); }

.hidden { display: none !important; }

/* ── Boot splash ──────────────────────────────────────────────
   Static markup in index.html, visible by default (no JS needed to show
   it) — covers the gap between first paint and app.js finishing its
   initial navigate(). js/app.js adds .boot-splash-hidden once that
   settles (plus a minimum visible time so it doesn't flash on fast
   loads), then removes the node after the fade transition ends. */
#boot-splash {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  background: var(--bg);
  transition: opacity 0.4s ease;
}

#boot-splash.boot-splash-hidden {
  opacity: 0;
  pointer-events: none;
}

.boot-splash-logo {
  height: 36px;
  width: auto;
  animation: boot-pulse 1.6s ease-in-out infinite;
}

@keyframes boot-pulse {
  0%, 100% { opacity: 0.5; transform: scale(0.96); }
  50%      { opacity: 1;   transform: scale(1); }
}

.boot-splash-bar {
  width: 120px;
  height: 2px;
  background: var(--border);
  overflow: hidden;
  position: relative;
}

.boot-splash-bar::after {
  content: '';
  position: absolute;
  top: 0; left: 0;
  height: 100%;
  width: 40%;
  background: var(--accent);
  animation: boot-sweep 1.1s ease-in-out infinite;
}

@keyframes boot-sweep {
  0%   { left: -40%; }
  100% { left: 100%; }
}

/* ── Nav progress bar ──────────────────────────────────────────
   Thin top-of-viewport bar driven by navProgressStart()/navProgressDone()
   in js/app.js, wrapping every navigate() call — a lighter-weight signal
   than the per-page .loading-overlay spinner, meant for the moment
   between hash change and the next page's render settling. */
#nav-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 2px;
  width: 0%;
  background: var(--accent);
  z-index: 9998;
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .boot-splash-logo { animation: none; opacity: 1; transform: none; }
  .boot-splash-bar::after { animation: none; width: 100%; }
  #nav-progress { transition: none !important; }
}

/* ── Navigation ── */
.nav {
  display: flex;
  align-items: center;
  gap: 24px;
  padding: 0 24px;
  height: 56px;
  background: var(--bg2);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 100;
}

.nav-brand {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-shrink: 0;
}

.nav-logo {
  font-size: 18px;
  font-weight: 800;
  color: var(--text);
  letter-spacing: -0.5px;
}

.nav-logo .accent { color: var(--accent); }

.nav-logo-img {
  height: 36px;
  width: auto;
  display: block;
  object-fit: contain;
}

.nav-team {
  font-size: 12px;
  color: var(--text-2);
  font-weight: 500;
  padding: 2px 8px;
  border: 1px solid var(--border);
  background: var(--bg3);
}

.nav-links {
  display: flex;
  gap: 2px;
  margin-left: 8px;
}

.nav-link {
  padding: 6px 14px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-2);
  border: 1px solid transparent;
  transition: color 0.15s, border-color 0.15s;
}

.nav-link:hover { color: var(--text); }
.nav-link.active { color: var(--accent); border-color: var(--border); background: var(--bg3); }

.nav-right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 12px;
}

.nav-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
}

.nav-icon-btn.active { border-color: var(--border); background: var(--bg3); }

/* #btn-theme-toggle is a plain <button> (an action, not a route — see
   js/app.js), so it doesn't get .nav-link's color/border for free. */
button.nav-icon-btn {
  background: none;
  border: 1px solid transparent;
  color: var(--text-2);
  transition: color 0.15s, border-color 0.15s;
}
button.nav-icon-btn:hover { color: var(--text); }

/* Icon reflects the CURRENTLY ACTIVE theme (sun showing = light is active) —
   purely declarative, no JS icon-swapping needed. Same default/media-query/
   explicit-attribute precedence as the color tokens above. */
.theme-icon-sun { display: none; }
.theme-icon-moon { display: block; }

@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .theme-icon-sun { display: block; }
  :root:not([data-theme]) .theme-icon-moon { display: none; }
}

:root[data-theme="light"] .theme-icon-sun { display: block; }
:root[data-theme="light"] .theme-icon-moon { display: none; }
:root[data-theme="dark"] .theme-icon-sun { display: none; }
:root[data-theme="dark"] .theme-icon-moon { display: block; }

/* Generic version of the same pattern, for whole image assets that need a
   different file per theme rather than a currentColor-recolorable glyph
   (the logo wordmark, the Q mark, the unicorn library icon — none of these
   are single-color enough to just swap a CSS color on one image). "none"
   for the hidden state; "revert" rather than a hardcoded display value for
   the shown state, so each image keeps whatever display its own class
   already sets (block for the nav logo, inline for an icon, etc.) instead
   of this utility silently overriding it. */
.theme-only-dark { display: revert; }
.theme-only-light { display: none; }

@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .theme-only-dark { display: none; }
  :root:not([data-theme]) .theme-only-light { display: revert; }
}

:root[data-theme="light"] .theme-only-dark { display: none; }
:root[data-theme="light"] .theme-only-light { display: revert; }
:root[data-theme="dark"] .theme-only-dark { display: revert; }
:root[data-theme="dark"] .theme-only-light { display: none; }

.nav-member {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-2);
}

/* ── Main Container ── */
#page-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 32px 24px;
  /* Scopes the View Transitions cross-fade (js/app.js's navigate()) to just
     the page content — the persistent #nav/#bottom-nav chrome around it is
     excluded from the transition's before/after snapshots and never
     flickers during a route change. */
  view-transition-name: page-content;
}

::view-transition-old(page-content),
::view-transition-new(page-content) {
  animation-duration: 0.18s;
}

/* ── Buttons ── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 10px 20px;
  font-size: 14px;
  font-weight: 600;
  border: 1px solid transparent;
  transition: all 0.15s;
  white-space: nowrap;
}

.btn-primary {
  background: var(--accent);
  color: #000;
  border-color: var(--accent);
}
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

/* Light theme's --accent is darkened for text-contrast reasons (see
   contrast-report.md) — black text on that darker orange no longer clears
   AA, white does. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .btn-primary { color: #fff; }
}
:root[data-theme="light"] .btn-primary { color: #fff; }
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }

.btn-secondary {
  background: var(--bg3);
  color: var(--text);
  border-color: var(--border-hi);
}
.btn-secondary:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); }

.btn-ghost {
  background: transparent;
  color: var(--text-2);
  border-color: transparent;
}
.btn-ghost:hover { color: var(--text); border-color: var(--border); }

.btn-danger {
  background: transparent;
  color: var(--red);
  border-color: var(--border);
}
.btn-danger:hover { background: rgba(239, 68, 68, 0.1); border-color: var(--red); }

.btn-sm { padding: 5px 12px; font-size: 12px; }
.btn-lg { padding: 13px 28px; font-size: 15px; }
.btn-full { width: 100%; }

/* ── Forms ── */
.form-group { margin-bottom: 18px; }

.form-label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-2);
  margin-bottom: 6px;
}

.form-input {
  width: 100%;
  background: var(--bg3);
  border: 1px solid var(--border);
  color: var(--text);
  font-size: 14px;
  padding: 10px 14px;
  outline: none;
  transition: border-color 0.15s;
}

.form-input:focus { border-color: var(--accent); }
.form-input::placeholder { color: var(--text-3); }
.form-input:disabled { opacity: 0.5; cursor: not-allowed; }

select.form-input { cursor: pointer; }
select.form-input option { background: var(--bg3); }

.form-hint { font-size: 12px; color: var(--text-3); margin-top: 5px; }
.form-error { font-size: 12px; color: var(--red); margin-top: 5px; }

.form-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }

/* ── Toggle switch ──
   A slide-styled replacement for a bare <input type="checkbox">, used
   wherever a setting is a live on/off (e.g. the owner's Feature Flags page)
   rather than a form field submitted later. Sharp-edged (0 border-radius),
   matching this theme's "no rounded corners" rule instead of the usual
   pill-shaped switch. Structure: <label class="switch"><input type="checkbox">
   <span class="switch-track"><span class="switch-thumb"></span></span></label>
   — the checkbox stays real (and keyboard/screen-reader operable), just
   visually hidden; :checked drives the track/thumb via sibling selectors. */
.switch { position: relative; display: inline-flex; align-items: center; cursor: pointer; flex-shrink: 0; }

.switch input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.switch-track {
  width: 40px;
  height: 20px;
  background: var(--bg4);
  border: 1px solid var(--border-hi);
  position: relative;
  transition: background 0.15s ease, border-color 0.15s ease;
  flex-shrink: 0;
}

.switch-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  background: var(--text-3);
  transition: transform 0.15s ease, background 0.15s ease;
}

.switch input[type="checkbox"]:checked ~ .switch-track { background: var(--accent-dim); border-color: var(--accent); }
.switch input[type="checkbox"]:checked ~ .switch-track .switch-thumb { transform: translateX(20px); background: var(--accent); }
.switch input[type="checkbox"]:focus-visible ~ .switch-track { outline: 2px solid var(--accent); outline-offset: 2px; }
.switch input[type="checkbox"]:disabled ~ .switch-track { opacity: 0.5; cursor: not-allowed; }

/* Windows High Contrast Mode strips background-color from most elements —
   .switch-thumb otherwise conveys on/off purely by position + fill color
   with no border, so it would vanish into the (also-stripped) track
   background and the switch would read as permanently empty. Scoped to
   forced-colors so ordinary light/dark rendering is untouched. */
@media (forced-colors: active) {
  .switch-thumb { border: 1px solid CanvasText; }
}

/* ── Cards & Panels ── */
.card {
  background: var(--bg2);
  border: 1px solid var(--border);
}

.card-header {
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 12px;
}

.card-title {
  font-size: 15px;
  font-weight: 700;
}

.card-body { padding: 20px; }

.card-accent-left { border-left: 3px solid var(--accent); }
.card-accent-gold  { border-left: 3px solid var(--gold); }

/* Trend sparkline (My Scores personal-best cards) — display:block clears
   the few px of inline-baseline gap an <svg> otherwise leaves under itself. */
.sparkline { display: block; }

/* Personal-best card header (My Scores) — a 2-row grid with named areas
   rather than a flex row, so desktop and mobile can group the same four
   pieces (name+meta, sparkline, value+status, trend) differently without
   duplicating any markup. Desktop: name/spark/value share row one, trend
   spans its own row below. See the mobile override (768px block) for why
   mobile regroups these into name+value on row one and spark+trend
   together on row two, instead of either wrapping unpredictably or
   leaving the sparkline stranded on its own line disconnected from the
   trend number it illustrates. */
.pb-card-header {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "name value"
    "trend spark";
  align-items: center;
  column-gap: 12px;
}
.pb-card-name  { grid-area: name; min-width: 0; }
.pb-card-value { grid-area: value; text-align: right; }
/* trend and spark share row two — the sparkline is a picture of the trend
   number beside it, so they read as one unit (percentage first, left to
   right, then the shape backing it up) on every screen size, not just
   mobile; an earlier version kept spark up in row one with name/value on
   desktop (room enough there for three items across), but that divorced
   it from the number it's actually illustrating. Both get the same
   margin-top/padding-top/border-top so the divider line reads as one
   continuous rule across the full row — each only owns half its width,
   so without matching spacing on both sides the line would visibly stop
   partway across under trend and never reach under spark. */
.pb-card-trend {
  grid-area: trend;
  font-size: 12px;
  color: var(--text-2);
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}
.pb-card-spark {
  grid-area: spark;
  justify-self: end;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}

/* Full-history rows (My Scores, inside each drill's collapsible section) —
   a stacked list, not a table. A single member's own history has no
   "identity" column worth freezing the way Leaderboard's member column
   does when that table scrolls sideways, so rather than reach for a
   scroll treatment at all, each entry becomes one compact row: value +
   date always on one line, everything else (logged-by, notes, tier,
   verification badge) folded into a single wrapping meta line beneath it
   that simply doesn't render when none of those apply. Fluid width, no
   horizontal scroll possible — nothing here to hint at scrolling toward. */
.history-list { border: 1px solid var(--border); border-top: none; }
.history-row { padding: 10px 14px; border-top: 1px solid var(--border); }
.history-row-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}
.history-row-value { font-size: 14px; font-weight: 700; }
.history-row-best { font-size: 10px; font-weight: 400; color: var(--text-3); margin-left: 4px; }
.history-row-date { font-size: 12px; color: var(--text-3); white-space: nowrap; flex-shrink: 0; }
.history-row-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
  margin-top: 4px;
  font-size: 12px;
  color: var(--text-2);
}

/* Verified score icon (My Scores history rows, admin.js's Score History
   table) — a real button now, tappable on mobile, not just a hover-only
   icon. ::before is the actual hit target: 44px square (16px visible icon
   + 14px invisible padding on every side), the standard fix for "icon
   needs a comfortable touch target without visually dominating a dense
   list row" — position:absolute keeps it out of document flow, so it
   can't add stray whitespace or shift row height even though it extends
   well past the icon's own visible edges. */
.verified-icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 0;
  color: var(--green);
  font-size: 16px;
  line-height: 1;
  vertical-align: -3px;
  margin-right: 5px;
  cursor: pointer;
}
.verified-icon-btn::before { content: ''; position: absolute; inset: -14px; }
.verified-icon-btn:hover { filter: brightness(1.2); }

/* Verified score explanation modal — same backdrop/box treatment (and
   shared confirm-modal-fade-in/confirm-modal-pop-in keyframes) as the
   timer/help/qual modals, opened by openVerifiedModal() in js/utils.js. */
.verified-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.75);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 20px;
  opacity: 0;
  animation: confirm-modal-fade-in 0.15s ease forwards;
}
.verified-modal-box {
  position: relative;
  background: var(--bg2);
  border: 1px solid var(--border-hi);
  width: 100%;
  max-width: 360px;
  box-shadow: 0 24px 64px rgba(0,0,0,0.6);
  padding: 28px;
  text-align: center;
  transform: translateY(8px) scale(0.98);
  animation: confirm-modal-pop-in 0.15s ease forwards;
}
.verified-modal-close {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--text-2);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.verified-modal-close:hover { color: var(--text); }
.verified-modal-icon { font-size: 40px; color: var(--green); margin-bottom: 12px; }
.verified-modal-title { font-size: 16px; font-weight: 800; margin-bottom: 8px; }
.verified-modal-body { font-size: 13px; color: var(--text-2); line-height: 1.5; }

/* ── Page Header ── */
.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  margin-bottom: 28px;
  gap: 16px;
}

.page-title {
  font-size: 22px;
  font-weight: 800;
  color: var(--text);
  letter-spacing: -0.3px;
}

.page-subtitle {
  font-size: 13px;
  color: var(--text-2);
  margin-top: 4px;
}

/* Groups multiple header-right controls (e.g. Log Score's shot-timer icon
   next to the qual-status seal) into one flex item, so .page-header still
   has exactly two children for justify-content: space-between to split —
   title on one side, every small action icon on the other. */
.page-header-actions { display: flex; align-items: center; gap: 10px; }

/* ── Stats Bar ── */
.stats-bar {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
  margin-bottom: 28px;
}

.stat-cell {
  background: var(--bg2);
  padding: 16px 20px;
}

.stat-cell-label {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-3);
  margin-bottom: 6px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.stat-cell-value {
  font-size: 26px;
  font-weight: 800;
  color: var(--accent);
  line-height: 1;
}

.stat-cell-sub {
  font-size: 11px;
  color: var(--text-3);
  margin-top: 4px;
}

/* ── Leaderboard ── */
.lb-filters {
  display: flex;
  gap: 10px;
  margin-bottom: 16px;
  flex-wrap: wrap;
  align-items: center;
}

.lb-filters select { width: auto; flex: 1; min-width: 200px; }

.lb-table {
  width: 100%;
  border-collapse: collapse;
  border: 1px solid var(--border);
}

.lb-table th {
  background: var(--bg3);
  padding: 10px 16px;
  text-align: left;
  font-size: 11px;
  font-weight: 700;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  border-bottom: 1px solid var(--border);
}

.lb-table th:first-child { width: 52px; text-align: center; }

/* Drill best-times table (owner.js toggleDrills) has no rank-badge first
   column — it's the drill name/description, which needs real room instead
   of the 52px the rule above reserves for other lb-table usages. */
.lb-table-drills th:first-child { width: 34%; text-align: left; }
.lb-table-drills th:nth-child(2) { width: 26%; }
.lb-table-drills th:nth-child(3) { width: 14%; }
.lb-table-drills th:nth-child(4) { width: 14%; }
.lb-table-drills th:nth-child(5) { width: 12%; }

.lb-table td {
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
  vertical-align: middle;
}

.lb-table tr:last-child td { border-bottom: none; }

.lb-table tbody tr {
  background: var(--bg2);
  transition: background 0.1s;
}

.lb-table tbody tr:hover { background: var(--bg3); }

.lb-table tbody tr.rank-1 { background: var(--gold-dim); }
.lb-table tbody tr.rank-2 { background: rgba(148,163,184,0.05); }
.lb-table tbody tr.rank-3 { background: rgba(180,120,60,0.05); }

.rank-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  font-weight: 800;
  font-size: 14px;
  color: var(--text-3);
  border: 1px solid var(--border);
  margin: auto;
}

.rank-badge.r1 { color: var(--gold); border-color: var(--gold); background: var(--gold-dim); }
.rank-badge.r2 { color: #94a3b8; border-color: #94a3b8; }
.rank-badge.r3 { color: #b47c3c; border-color: #b47c3c; }

.member-name-cell { font-weight: 700; white-space: nowrap; }
.member-role-cell { font-size: 11px; color: var(--text-3); margin-top: 2px; }

.time-value {
  font-size: 18px;
  font-weight: 800;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.time-value.gold { color: var(--gold); }
.time-value.under-par { color: var(--green); }

.avg-value { font-size: 13px; color: var(--text-2); }

.badge {
  display: inline-block;
  font-size: 11px;
  font-weight: 600;
  padding: 2px 8px;
  border: 1px solid;
}

.badge-hot    { color: var(--green); border-color: var(--green); background: rgba(34,197,94,0.1); }
.badge-new    { color: var(--blue);  border-color: var(--blue);  background: rgba(96,165,250,0.1); }
.badge-par    { color: var(--accent); border-color: var(--accent); background: var(--accent-dim); }
.badge-muted  { color: var(--text-3); border-color: var(--border); background: var(--bg3); }
.badge-soon   { color: var(--accent); border-color: var(--accent); background: var(--accent-dim); }

.badge-qualified { color: var(--green); border-color: var(--green); background: rgba(34,197,94,0.1); }
.badge-expired   { color: var(--red);   border-color: var(--red);   background: rgba(239,68,68,0.1); }
.badge-missing   { color: var(--text-3); border-color: var(--border); background: var(--bg3); }

/* Member's own qual status — My Scores / Log Score. Lives only inside
   .qual-modal-box (see below), never inline in a page. Centered column,
   matching .verified-modal-*'s layout exactly (seal/icon on top, title,
   body, centered) rather than its own left-aligned row with its own
   gold/red border+background — that earlier treatment was designed for an
   inline banner strip, and kept inside .qual-modal-box it read as a box
   nested inside a box (two borders, two padding scales) fighting the
   modal's own framing instead of just being its content. The status color
   now comes through via the seal's own ring color alone, same as
   Verified's icon carrying its color while the title stays plain text. */
.qual-banner {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

/* display:block alone (no margin) applies everywhere .qual-seal is used,
   including the header trigger button — that button needs to stay an
   exact square for its circular glow (border-radius:50%) to actually
   render as a circle, so the modal-only spacing below is scoped to
   .qual-banner specifically rather than living on the bare class, which
   previously stretched the header button to 40×52px (margin counts
   toward a flex item's box) and turned its "circular" glow into a lumpy
   ellipse. */
.qual-seal { display: block; }
.qual-banner .qual-seal { margin-bottom: 12px; }

.qual-banner-title { font-size: 16px; font-weight: 800; margin-bottom: 8px; }
.qual-banner-body { font-size: 13px; color: var(--text-2); line-height: 1.5; }
.qual-banner-expiry { font-size: 12px; color: var(--gold); font-weight: 600; margin-top: 8px; }

/* Seal-medallion trigger living in the page-header (Log Score / My Scores),
   top right — a small renderQualSealBadge() (js/utils.js), not a text
   pill, so the header trigger and the modal it opens share one visual
   identity instead of a pill handing off to an unrelated-looking seal.
   Circular (border-radius:50% + a circular glow) is a deliberate, narrow
   exception to this app's sharp-edges rule: the exception is the seal
   graphic itself, which has always been a circular medallion, not a new
   rounded-UI-chrome habit — the button is just matching content it didn't
   invent. A soft pulsing glow (gold/red to match the seal's own ring)
   draws the eye to it without needing the old chevron to hint "there's
   more here." */
.qual-seal-btn {
  display: inline-flex;
  border: none;
  background: none;
  padding: 0;
  border-radius: 50%;
  cursor: pointer;
  flex-shrink: 0;
  line-height: 0;
  transition: filter 0.15s;
}
.qual-seal-btn:hover { filter: brightness(1.15); }
.qual-seal-btn-good { animation: qual-seal-glow-good 2.4s ease-in-out infinite; }
.qual-seal-btn-bad  { animation: qual-seal-glow-bad 2.4s ease-in-out infinite; }

/* Glow RGB is hardcoded (rgba() can't take a custom property as its color
   channels) and kept the same across both themes deliberately, unlike the
   landing page's accent glows — a box-shadow halo isn't held to text
   contrast rules the way the seal's own ring/text color already is, and
   these particular values read clearly as "gold"/"red" against either a
   near-black or near-white surface, so a second light-theme tuning pass
   isn't earning its cost here the way it did for .lp-hero's wash. */
@keyframes qual-seal-glow-good {
  0%, 100% { box-shadow: 0 0 0 0 rgba(234,179,8,0.5), 0 0 10px rgba(234,179,8,0.3); }
  50%      { box-shadow: 0 0 0 6px rgba(234,179,8,0), 0 0 18px rgba(234,179,8,0.6); }
}
@keyframes qual-seal-glow-bad {
  0%, 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.5), 0 0 10px rgba(239,68,68,0.3); }
  50%      { box-shadow: 0 0 0 6px rgba(239,68,68,0), 0 0 18px rgba(239,68,68,0.6); }
}
@media (prefers-reduced-motion: reduce) {
  .qual-seal-btn-good, .qual-seal-btn-bad { animation: none; box-shadow: 0 0 12px rgba(234,179,8,0.35); }
  .qual-seal-btn-bad { box-shadow: 0 0 12px rgba(239,68,68,0.35); }
}

/* Qualification status modal — same backdrop/box treatment (and shared
   confirm-modal-fade-in/confirm-modal-pop-in keyframes) as the timer modal
   and help modal, opened by openQualModal() in js/utils.js. */
.qual-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.75);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 20px;
  opacity: 0;
  animation: confirm-modal-fade-in 0.15s ease forwards;
}

.qual-modal-box {
  position: relative;
  background: var(--bg2);
  border: 1px solid var(--border-hi);
  width: 100%;
  /* Matches .verified-modal-box's width, not the wider 440px other modals
     (timer/help) use for multi-line lists — this content is a short
     centered stack now, same shape as Verified's, so it gets the same
     tighter box rather than sitting lost in extra side-margin. */
  max-width: 360px;
  max-height: 85vh;
  overflow-y: auto;
  box-shadow: 0 24px 64px rgba(0,0,0,0.6);
  padding: 28px;
  text-align: center;
  transform: translateY(8px) scale(0.98);
  animation: confirm-modal-pop-in 0.15s ease forwards;
}

.qual-modal-close {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--text-2);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.qual-modal-close:hover { color: var(--text); }

/* .qual-banner sits inside .qual-modal-box with room to breathe on either
   side of the close button above it. */
.qual-modal-box .qual-banner { margin-top: 8px; }

.empty-state {
  padding: 48px;
  text-align: center;
  color: var(--text-2);
  border: 1px dashed var(--border);
  background: var(--bg2);
}

.empty-state-icon { font-size: 48px; margin-bottom: 12px; }
.empty-state-title { font-size: 16px; font-weight: 700; margin-bottom: 6px; }
.empty-state-body { font-size: 13px; color: var(--text-3); }

/* ── Drills ── */
.drill-list { display: flex; flex-direction: column; gap: 4px; }

.drill-card {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  padding: 16px 18px;
  cursor: pointer;
  transition: background 0.1s, border-left-color 0.1s;
  display: flex;
  align-items: flex-start;
  gap: 16px;
}

.drill-card:hover { background: var(--bg3); }

.drill-card-body { flex: 1; min-width: 0; }

.drill-card-name {
  font-size: 15px;
  font-weight: 700;
  margin-bottom: 4px;
}

.drill-card-desc {
  font-size: 13px;
  color: var(--text-2);
  line-height: 1.5;
  margin-bottom: 10px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.drill-chips { display: flex; gap: 6px; flex-wrap: wrap; }

.chip {
  font-size: 11px;
  font-weight: 600;
  padding: 3px 10px;
  border: 1px solid;
}

.chip-timed   { color: var(--gold); border-color: rgba(234,179,8,0.4); background: var(--gold-dim); }
.chip-score   { color: var(--blue); border-color: rgba(96,165,250,0.4); background: rgba(96,165,250,0.08); }
.chip-neutral { color: var(--text-2); border-color: var(--border); background: var(--bg3); }
.chip-qual    { color: var(--accent); border-color: var(--accent); background: var(--accent-dim); font-weight: 700; }

/* Drill library card icon — Phosphor <i> glyphs use currentColor at accent;
   the unicorn <img> is a pre-colored transparent PNG, so it just needs sizing. */
.lib-icon { display: flex; align-items: center; flex-shrink: 0; font-size: 18px; color: var(--accent); line-height: 1; }
.lib-icon img { width: 18px; height: 18px; object-fit: contain; }

.drill-card-actions { display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }

.btn-toggle-qual.qual-active {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-dim);
}

/* Delete drill — icon-only trash button rather than a text "Delete" label,
   matching the compact icon-button pattern already used elsewhere in the
   app (.verified-icon-btn, .drill-info-icon) instead of spelling the
   action out. Fixed square (matches .btn-sm's 38px on both desktop and
   mobile, since that rule is unchanged at either breakpoint) and
   align-self: flex-end so it doesn't stretch to match its wider "Set as
   Qual" sibling under .drill-card-actions' default flex cross-axis
   stretch. Still a documented .btn-sm touch-target exception (Mobile &
   Responsive Design Framework, CLAUDE.md) — deleting a drill is an
   infrequent, destructive Team Lead action, not a repeated-tap control. */
.btn-delete-drill {
  align-self: flex-end;
  width: 38px;
  height: 38px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  flex-shrink: 0;
}

/* ── Drills: full-screen Add Drill / Import Library modals ──
   Both used to render as an inline .card on the page itself, toggled via
   display:none/block — a multi-field form (or a grid of library cards)
   permanently taking up a slot in the page's DOM and competing for scroll
   space with the drill list below it. Converted to a real modal instead:
   full-screen on mobile, a large centered dialog on desktop. Same
   backdrop treatment (and shared confirm-modal-fade-in keyframe) as every
   other modal in the app, but its own box: a sticky header (title +
   close) and sticky footer (Cancel/Done + primary action) sandwich a
   scrollable body, so a form taller than the viewport still keeps its
   action buttons on screen. On mobile this also lands the primary action
   in the thumb-reachable bottom of the screen — see the Mobile &
   Responsive Design Framework's thumb-zone guidance in CLAUDE.md, which
   flagged these two triggers as sitting in the hardest one-handed reach
   zone (the page header); the trigger itself stays there (opening either
   flow is a single infrequent tap), but the actual repeated work — typing
   fields, tapping Import per library, hitting Save — now happens with its
   primary action pinned to the easy-reach zone instead of trailing off at
   the bottom of a long scroll. */
.drill-fs-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.75);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 32px 20px;
  opacity: 0;
  animation: confirm-modal-fade-in 0.15s ease forwards;
}

.drill-fs-modal {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--bg2);
  border: 1px solid var(--border-hi);
  width: 100%;
  max-width: 560px;
  max-height: 100%;
  box-shadow: 0 24px 64px rgba(0,0,0,0.6);
  transform: translateY(8px) scale(0.98);
  animation: confirm-modal-pop-in 0.15s ease forwards;
}

.drill-fs-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 18px 20px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

.drill-fs-modal-title { font-size: 16px; font-weight: 800; color: var(--text); }

.drill-fs-modal-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  background: none;
  border: none;
  color: var(--text-2);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  flex-shrink: 0;
}
.drill-fs-modal-close:hover { color: var(--text); }

.drill-fs-modal-body {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
}

.drill-fs-modal-footer {
  display: flex;
  gap: 8px;
  padding: 16px 20px;
  border-top: 1px solid var(--border);
  flex-shrink: 0;
}
.drill-fs-modal-footer .btn { flex: 1; }

/* The mobile slide-up (see the 768px media query below) is a much bigger,
   more perceptible motion than the 8px pop-in every other modal in this
   file uses uncontested — worth its own reduced-motion override where
   those smaller ones don't bother with one. */
@media (prefers-reduced-motion: reduce) {
  .drill-fs-modal-backdrop, .drill-fs-modal { animation: none; opacity: 1; transform: none; }
}

.add-drill-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 14px;
  border: 1px dashed var(--border);
  background: transparent;
  color: var(--text-3);
  font-size: 13px;
  font-weight: 500;
  width: 100%;
  transition: all 0.15s;
}

.add-drill-btn:hover {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-dim);
}

/* ── Log Score ── */
.log-form-wrap {
  max-width: 520px;
}

.time-display-large {
  background: var(--bg3);
  border: 2px solid var(--border);
  padding: 20px;
  text-align: center;
  margin-bottom: 20px;
  transition: border-color 0.2s;
}

.time-display-large.active { border-color: var(--accent); }

.time-display-number {
  font-size: 52px;
  font-weight: 800;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.time-display-unit {
  font-size: 14px;
  color: var(--text-3);
  margin-top: 6px;
}

.drill-info-box {
  background: var(--bg3);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  padding: 14px 16px;
  margin-bottom: 20px;
}

.drill-info-box .drill-info-name {
  font-size: 15px;
  font-weight: 700;
  margin-bottom: 4px;
}

.drill-info-box .drill-info-meta {
  font-size: 12px;
  color: var(--text-2);
  display: flex;
  gap: 16px;
}

/* Log Score's member/drill pickers — a select paired with an optional
   "next" button, each in its own labeled sub-field so the button's label
   ("Next up" / "Next drill") reads independently of the select's own label
   above it. align-items: flex-end bottom-aligns both fields regardless of
   their own content height. flex-wrap allows the "next" field to drop to
   its own line on very narrow screens rather than force horizontal scroll
   — unlike .log-score-row below, wrapping here is an acceptable fallback
   since a select+button pair isn't the pair this refactor is trying to
   keep glued together.
   The next-member/next-drill buttons are deliberately full-size `.btn`,
   not `.btn-sm` — the mobile pass (see ui-ux-trend-audit.md) reclassified
   them as primary-workflow controls (a Team Lead taps one per shooter,
   repeatedly, through a whole range session) rather than the
   secondary/infrequent case `.btn-sm`'s sub-44px height is meant for. */
.log-picker-row { display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; }
.log-picker-field { flex: 1; min-width: 140px; }
/* Defensive, not a fix for an observed bug: a flex item's default
   min-width:auto sizes it to its content's intrinsic minimum, and that
   can in principle exceed the 140px this field is allotted once a select's
   *longest option* is long enough (a Team Lead's own custom drill name,
   not the seed-library names tested against). min-width:0 here lets the
   box actually shrink to the row's real min-width instead of forcing a
   wider one — checked at 320px with the current drill library and found
   nothing overflowing without this, so this is cheap insurance, not a
   confirmed reflow fix. */
.log-picker-field .form-input { min-width: 0; }
.log-picker-field-next { flex: 0 0 auto; }
.log-picker-field-next .btn { max-width: 160px; overflow: hidden; text-overflow: ellipsis; }

/* Drill field's label row — the drill-info trigger sits immediately beside
   the "Drill" text itself now, not as a separate button competing for
   space in the picker row (that read as an awkward, oversized third
   control jammed next to the select). A tight gap keeps it read as part of
   the label, not a floating far-right control unrelated to it. */
.form-label-row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
.form-label-row .form-label { margin-bottom: 0; }

/* The drill-info icon itself: larger and more inviting to tap than the old
   .btn-sm icon button (18px glyph vs. 14px, no button chrome/border boxing
   it in), colored with the accent so it doesn't read as just decoration.
   Still visually compact, so it still needs the same ::before invisible
   hit-area trick as .verified-icon-btn elsewhere in this file to clear a
   real touch target without growing the glyph itself or disturbing the
   label row's height. */
.drill-info-icon {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
}
.drill-info-icon::before { content: ''; position: absolute; inset: -13px; }
.drill-info-icon:hover { filter: brightness(1.2); }

/* Time/Score/Points fields, side by side rather than stacked — the point
   of this row is to fit the whole Log Score form on one screen with no
   scrolling, so unlike .form-row-2 this deliberately has no mobile
   breakpoint override back to a single column: it must stay side by side
   on the phone-sized screens this page is actually used on at the range.
   Each field is a plain flex:1 item rather than a fixed grid, so whichever
   single field is visible (a pure-timed or pure-score drill) naturally
   fills the full row on its own with no JS needed to toggle a column count. */
.log-score-row { display: flex; gap: 12px; }
.log-score-row > .form-group { flex: 1; min-width: 0; }

/* ── Drill info modal (Log Score's condensed drill-info icon) ──
   Same backdrop/box treatment (and shared confirm-modal-fade-in/
   confirm-modal-pop-in keyframes) as the timer/help/qual/verified modals —
   own class names per this codebase's convention of one modal, one name,
   shared keyframes. Replaces the old always-visible drill-info-box on Log
   Score itself (still used as-is on Leaderboard/the demo pages — untouched). */
.drill-info-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.75);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 20px;
  opacity: 0;
  animation: confirm-modal-fade-in 0.15s ease forwards;
}

.drill-info-modal-box {
  position: relative;
  background: var(--bg2);
  border: 1px solid var(--border-hi);
  width: 100%;
  max-width: 420px;
  max-height: 85vh;
  overflow-y: auto;
  box-shadow: 0 24px 64px rgba(0,0,0,0.6);
  padding: 24px;
  transform: translateY(8px) scale(0.98);
  animation: confirm-modal-pop-in 0.15s ease forwards;
}

.drill-info-modal-close {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--text-2);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.drill-info-modal-close:hover { color: var(--text); }

.drill-info-modal-title { font-size: 16px; font-weight: 800; color: var(--text); padding-right: 24px; }
.drill-info-modal-desc { font-size: 13px; color: var(--text-2); line-height: 1.6; }

/* ── Admin Panel ── */
.admin-tabs { display: flex; gap: 2px; margin-bottom: 24px; }

.admin-tab {
  padding: 8px 18px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-2);
  background: var(--bg3);
  border: 1px solid var(--border);
  transition: all 0.15s;
  cursor: pointer;
}

.admin-tab.active { color: var(--accent); border-color: var(--accent); background: var(--accent-dim); }
.admin-tab:hover:not(.active) { color: var(--text); }

.member-row-wrap { margin-bottom: 4px; }

.member-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  background: var(--bg2);
  border: 1px solid var(--border);
}

.member-row-info { flex: 1; min-width: 0; }
.member-row-name { font-weight: 600; font-size: 14px; }
.member-row-meta { font-size: 12px; color: var(--text-3); margin-top: 2px; }

.member-row-actions { display: flex; gap: 8px; flex-shrink: 0; }

.member-pin-form,
.member-username-form {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  background: var(--bg3);
  border: 1px solid var(--border);
  border-top: none;
}

.pin-reset-input, .username-edit-input { max-width: 200px; }

/* ── Login / Setup ── */
.auth-wrap {
  min-height: calc(100vh - 57px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px 24px;
}

.auth-box {
  width: 100%;
  max-width: 420px;
}

.auth-logo {
  font-size: 28px;
  font-weight: 800;
  letter-spacing: -0.5px;
  margin-bottom: 6px;
}

.auth-logo .accent { color: var(--accent); }

.auth-logo-link {
  display: block;
}

.auth-logo-img {
  height: 56px;
  width: auto;
  display: block;
  margin-bottom: 12px;
  object-fit: contain;
}

.auth-tagline {
  font-size: 13px;
  color: var(--text-2);
  margin-bottom: 32px;
}

.auth-divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: 20px 0;
}

.auth-step-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 14px;
}

.member-pick-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 16px;
  max-height: 260px;
  overflow-y: auto;
}

.member-pick-item {
  padding: 12px 16px;
  background: var(--bg3);
  border: 1px solid var(--border);
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.1s;
  text-align: left;
  width: 100%;
  color: var(--text);
}

.member-pick-item:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); }
.member-pick-item.selected { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); }

.setup-link {
  font-size: 12px;
  color: var(--text-3);
  margin-top: 20px;
  text-align: center;
}

.setup-link a {
  color: var(--accent);
  text-decoration: underline;
  cursor: pointer;
}

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

.toast {
  padding: 12px 18px;
  font-size: 13px;
  font-weight: 600;
  border: 1px solid;
  background: var(--bg2);
  animation: slideIn 0.2s ease;
  max-width: 320px;
  display: flex;
  align-items: center;
  gap: 10px;
}

.toast-success { border-color: var(--green); color: var(--green); }
.toast-error   { border-color: var(--red);   color: var(--red); }
.toast-info    { border-color: var(--accent); color: var(--accent); }
.toast-fun     { border-color: var(--gold);  color: var(--gold); }

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

.toast-icon-wiggle { display: inline-block; animation: fishWiggle 0.35s ease-in-out 3; }

@keyframes fishWiggle {
  0%, 100% { transform: rotate(-14deg); }
  50%      { transform: rotate(14deg); }
}

.toast-persistent { max-width: 440px; }

.toast-close {
  background: none;
  border: none;
  color: inherit;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
  opacity: 0.6;
}

.toast-close:hover { opacity: 1; }

/* ── PWA install bottom sheet ──
   A true modal now (full-viewport backdrop + a sheet anchored to the
   bottom of the viewport), not the fixed corner card this used to be —
   the backdrop dims .bottom-nav along with the rest of the app and blocks
   interaction with it, rather than the sheet floating above it unshielded.
   Appended directly to <body> (see js/pwa.js), so it's outside #app/
   #page-container and is never constrained by their stacking context.
   z-index 1000 matches the rest of the app's modal layer (.confirm-modal-
   backdrop, .help-modal-backdrop, etc.) — high enough to sit above
   .bottom-nav's 200 and .nav's 100, but still under the toast layer
   (9999) so a toast can still surface over an open sheet.
   Sharp corners, matching the app's existing sharp-edges design language
   — no exception here.
   No reverse/exit animation on dismiss — matches every other modal in
   the app (confirm/help/donate/timer/qual/drill-fs), none of which
   animate out either; it's just removed. */
.install-sheet-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: 1000;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  opacity: 0;
  animation: confirm-modal-fade-in 0.2s ease forwards;
}

.install-sheet {
  position: relative;
  width: 100%;
  max-width: 440px;
  background: var(--bg2);
  border: 1px solid var(--border-hi);
  border-bottom: none;
  box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.45);
  padding: 20px 20px calc(20px + env(safe-area-inset-bottom));
  transform: translateY(100%);
  animation: install-sheet-slide-up 0.25s ease forwards;
}

@keyframes install-sheet-slide-up {
  to { transform: translateY(0); }
}

.install-sheet-body {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}

.install-sheet-icon {
  flex-shrink: 0;
  border-radius: 8px;
}

.install-sheet-title {
  font-size: 16px;
  font-weight: 800;
  color: var(--text);
  margin-bottom: 6px;
}

.install-sheet-desc {
  font-size: 13px;
  color: var(--text-2);
  line-height: 1.5;
}

.install-sheet-actions {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  margin-top: 18px;
}

/* Install App reads as the obvious, unambiguous primary action (full-width,
   .btn-primary); Maybe Later stays a plain-text-weight secondary action
   below it rather than a bordered/filled button competing for the same
   attention — mirrors .confirm-modal-actions' primary/secondary pairing,
   just stacked instead of side-by-side since there's only one sheet-width
   button, not two. */
.install-sheet-actions .btn-primary { width: 100%; }
.install-sheet-later {
  background: none;
  border: none;
  color: var(--text-2);
  font-size: 13px;
  font-weight: 600;
  padding: 8px 12px;
  min-height: 44px;
  cursor: pointer;
}
.install-sheet-later:hover { color: var(--text); }

@media (min-width: 769px) {
  /* On desktop the sheet still anchors to the bottom of the viewport
     (consistent single treatment across breakpoints, no separate centered-
     dialog variant to maintain) but gets a little breathing room off the
     bottom edge and its own margin/shadow instead of sitting flush, since
     nothing forces it to the physical screen edge the way a phone chrome
     does. */
  .install-sheet-backdrop { padding-bottom: 24px; }
  .install-sheet {
    border-bottom: 1px solid var(--border-hi);
    box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
  }
}

@media (prefers-reduced-motion: reduce) {
  .install-sheet-backdrop, .install-sheet { animation: none; opacity: 1; transform: none; }
}

/* ── Divider / Section label ── */
.section-label {
  font-size: 11px;
  font-weight: 700;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 12px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
}

/* ── Spinner ── */
.spinner {
  display: inline-block;
  width: 18px;
  height: 18px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

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

/* ── Skeleton loading placeholders ──
   Content-shaped placeholders shown while a data-backed page's first fetch
   is in flight — replaces the old spinner-only .loading-overlay on the
   three pages dense enough for it to matter (Leaderboard, Drills, My
   Scores; see each page's renderXxxSkeleton()). Static chrome that's
   already known without the fetch (page titles, a member's own name) still
   renders for real immediately — only the genuinely data-dependent parts
   (stat values, table rows, card bodies) get a placeholder. `.skel` is the
   shimmering shape itself; skeleton "cards"/"rows" reuse the real
   .card/.stat-cell/etc. containers (static background, no shimmer) so only
   the shapes standing in for text/values move. */
.skel {
  display: block;
  background: linear-gradient(90deg, var(--bg3) 25%, var(--bg4) 37%, var(--bg3) 63%);
  background-size: 400% 100%;
  animation: skel-shimmer 1.4s ease infinite;
}

@keyframes skel-shimmer {
  0%   { background-position: 100% 0; }
  100% { background-position: 0 0; }
}

@media (prefers-reduced-motion: reduce) {
  .skel { animation: none; background: var(--bg3); }
}

@keyframes gunfire-shake {
  0%,100% { transform: translate(0, 0) rotate(0deg); }
  10%      { transform: translate(-4px, -3px) rotate(-0.5deg); }
  20%      { transform: translate(5px, 2px) rotate(0.5deg); }
  30%      { transform: translate(-3px, 4px) rotate(-0.3deg); }
  40%      { transform: translate(4px, -2px) rotate(0.4deg); }
  50%      { transform: translate(-5px, 1px) rotate(-0.5deg); }
  60%      { transform: translate(3px, 3px) rotate(0.3deg); }
  70%      { transform: translate(-2px, -4px) rotate(0.2deg); }
  80%      { transform: translate(4px, 2px) rotate(-0.3deg); }
  90%      { transform: translate(-3px, -1px) rotate(0.1deg); }
}

.gunfire-shake {
  animation: gunfire-shake 0.6s ease-in-out;
}

/* Tannerite-style boom: bright flash + billowing smoke cloud, spawned at
   confetti origin points in js/pages/leaderboard.js (fireConfetti). */
.boom-fx {
  position: fixed;
  pointer-events: none;
  z-index: 500;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  will-change: transform, opacity;
}

.boom-flash {
  background: radial-gradient(circle, rgba(255,230,150,0.95) 0%, rgba(255,140,40,0.75) 35%, rgba(120,60,20,0) 70%);
  animation: boom-flash-anim 0.4s ease-out forwards;
}

@keyframes boom-flash-anim {
  0%   { opacity: 1; transform: translate(-50%, -50%) scale(0.2); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(2.4); }
}

.boom-smoke {
  background: radial-gradient(circle, rgba(190,190,190,0.85) 0%, rgba(120,120,120,0.5) 45%, rgba(90,90,90,0) 75%);
  filter: blur(2px);
  animation: boom-smoke-anim 1.8s ease-out forwards;
}

@keyframes boom-smoke-anim {
  0%   { opacity: 0.9; transform: translate(-50%, -50%) scale(0.3) translateY(0); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(2.8) translateY(-90px); }
}

/* Explains the confetti/boom explosion — non-blocking, auto-dismisses.
   Typography mirrors the landing page's "eyebrow label + bold tight
   headline" treatment (see .lp-section-label / .lp-hero-headline) instead
   of reading like a plain alert box. */
.celebration-banner {
  position: fixed;
  top: 80px;
  left: 50%;
  z-index: 900;
  background: linear-gradient(165deg, var(--bg3) 0%, var(--bg2) 100%);
  border: 1px solid var(--border-hi);
  border-top: 3px solid var(--gold);
  box-shadow: 0 24px 64px rgba(0,0,0,0.55), 0 0 60px var(--gold-dim);
  padding: 30px 36px;
  max-width: 440px;
  width: calc(100% - 40px);
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transform: translateX(-50%) scale(0.85);
  animation: celebration-banner-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

.celebration-banner.dismissing {
  animation: celebration-banner-out 0.4s ease forwards;
}

@keyframes celebration-banner-in {
  to { opacity: 1; transform: translateX(-50%) scale(1); }
}

@keyframes celebration-banner-out {
  to { opacity: 0; transform: translateX(-50%) translateY(-20px); }
}

.celebration-banner-eyebrow {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--gold);
  margin-bottom: 12px;
}

.celebration-banner-title {
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.15;
  color: var(--text);
  margin-bottom: 12px;
}

.celebration-banner-body {
  font-size: 14px;
  color: var(--text-2);
  line-height: 1.6;
}

.celebration-banner-body strong {
  color: var(--text);
  font-weight: 700;
}

.loading-overlay {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 64px;
  color: var(--text-3);
  gap: 12px;
  font-size: 14px;
}

/* ── Bottom Navigation ── */
.bottom-nav {
  display: none; /* shown via media query */
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: var(--bg2);
  border-top: 1px solid var(--border);
  z-index: 200;
  padding-bottom: env(safe-area-inset-bottom); /* notch/home bar */
}

.bottom-nav-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  padding: 10px 0;
  color: var(--text-3);
  font-size: 11px;
  font-weight: 600;
  flex: 1;
  border: none;
  background: none;
  transition: color 0.15s;
  -webkit-tap-highlight-color: transparent;
}

.bottom-nav-item.active { color: var(--accent); }
.bottom-nav-item:hover  { color: var(--text); }

.bottom-nav-primary {
  color: var(--accent);
  position: relative;
}

.bottom-nav-primary svg {
  background: var(--accent);
  color: #000;
  padding: 5px;
  border-radius: 50%;
  width: 44px;
  height: 44px;
  margin-top: -8px;
}

/* Same background-only-fill risk as .switch-thumb above — under forced-colors
   this circular badge would blend into the nav bar and Log would lose its
   distinct primary-action treatment. */
@media (forced-colors: active) {
  .bottom-nav-primary svg { border: 1px solid CanvasText; }
}

/* Same reasoning as .btn-primary above. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .bottom-nav-primary svg { color: #fff; }
}
:root[data-theme="light"] .bottom-nav-primary svg { color: #fff; }

.bottom-nav-primary.active svg,
.bottom-nav-primary:hover svg {
  background: var(--accent-hover);
}

/* ══════════════════════════════════════════════════════════════
   Landing Page
   ══════════════════════════════════════════════════════════════ */

/* The page-container becomes a full scrollable page */
#page-container:has(.lp-hero) {
  padding: 0;
  max-width: none;
}

/* Ambient glow — ported from timgriffeth.com's mouse-reactive-bg. One fixed
   radial gradient anchored near the bottom of the viewport; only its
   BRIGHTNESS (not its position) reacts to the mouse, via the --glow custom
   property (0.06 dark → 0.5 bright) set every frame from a rAF loop in
   js/pages/home.js. Higher mouse = brighter. z-index:-1 so it sits behind
   all content but above the page's own background color. */
.lp-ambient-glow {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  --glow: 0.16;
  background:
    radial-gradient(120% 110% at 50% 95%, rgba(249,131,46, var(--glow)) 0%, rgba(250,154,85, calc(var(--glow) * 0.8)) 35%, transparent 60%),
    linear-gradient(to bottom, rgba(9,9,11,0.85), var(--bg));
  transition: background 80ms linear;
}

/* Both layers above are literal RGB, not var()-linked: the spotlight is the
   dark theme's orange accent, and the vignette fades to a literal --bg
   (dark). Neither is theme-aware by default. Light mode needs its own
   spotlight color (navy accent, not the dark theme's orange — the whole
   point of choosing navy was for it to look deliberate, not like an
   orange decoration left behind by mistake) and its own lighter vignette
   fade so it blends into the light background instead of painting a dark
   band across the hero. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .lp-ambient-glow {
    background:
      radial-gradient(120% 110% at 50% 95%, rgba(22,79,155, var(--glow)) 0%, rgba(36,108,203, calc(var(--glow) * 0.8)) 35%, transparent 60%),
      linear-gradient(to bottom, rgba(248,248,250,0.9), var(--bg));
  }
}

:root[data-theme="light"] .lp-ambient-glow {
  background:
    radial-gradient(120% 110% at 50% 95%, rgba(22,79,155, var(--glow)) 0%, rgba(36,108,203, calc(var(--glow) * 0.8)) 35%, transparent 60%),
    linear-gradient(to bottom, rgba(248,248,250,0.9), var(--bg));
}

/* Persistent conversion nav — transparent over the hero at rest (so it
   reads as part of the hero, not a separate chrome layer) and solidifies
   to a bordered --bg2 bar once the page scrolls past the hero's own top
   padding (.lp-nav-scrolled, toggled from a scroll listener in
   js/pages/home.js). Fixed rather than sticky since it must stay
   overlaid on the hero at scroll position 0 — .lp-hero's top padding is
   sized comfortably larger than this nav's height so hero content is
   never covered even before it solidifies. Anchor links + the two CTAs
   reuse .lp-btn-signin/.lp-btn-getstarted's existing delegated click
   handlers at the bottom of renderHome(), so no extra JS wiring needed
   for the buttons themselves. */
.lp-nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  height: 64px;
  display: flex;
  align-items: center;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background 0.2s, border-color 0.2s;
}

.lp-nav-scrolled {
  background: var(--bg2);
  border-bottom-color: var(--border);
}

.lp-nav-inner {
  width: 100%;
  max-width: 1120px;
  margin: 0 auto;
  padding: 0 24px;
  display: flex;
  align-items: center;
  gap: 24px;
}

.lp-nav-brand {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

.lp-nav-logo {
  height: 30px;
  width: auto;
  display: block;
}

.lp-nav-links {
  display: flex;
  gap: 4px;
}

.lp-nav-link {
  padding: 6px 12px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-2);
  border: 1px solid transparent;
  text-decoration: none;
  transition: color 0.15s;
}

.lp-nav-link:hover { color: var(--text); }

/* margin-left:auto (rather than margin-right:auto on .lp-nav-links) so
   Sign In / Get started stay pushed to the right even on mobile, where
   .lp-nav-links is display:none and can't provide the push itself. */
.lp-nav-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
  margin-left: auto;
}

.lp-nav-signin {
  background: none;
  border: 1px solid transparent;
  color: var(--text-2);
  font-size: 13px;
  font-weight: 600;
  padding: 8px 14px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}

.lp-nav-signin:hover {
  color: var(--text);
  border-color: var(--border);
  background: var(--bg3);
}

/* Floating "next section" arrow (js/pages/home.js) — fixed bottom-center,
   works identically on mobile and desktop. Sits below .lp-nav's z-index
   (50) since the two never overlap on screen in practice. Hidden via
   opacity/pointer-events (not display:none) so the fade transition can
   play; toggled from a scroll listener in home.js. */
.lp-scroll-next {
  position: fixed;
  left: 50%;
  bottom: 28px;
  transform: translateX(-50%);
  z-index: 40;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  background: var(--bg2);
  border: 1px solid var(--border);
  color: var(--text-2);
  font-size: 18px;
  cursor: pointer;
  opacity: 1;
  pointer-events: auto;
  animation: lp-scroll-next-bounce 2.2s ease-in-out infinite,
             lp-scroll-next-glow 2.4s ease-in-out infinite;
  transition: opacity 0.2s, color 0.15s, border-color 0.15s, background 0.15s;
}

.lp-scroll-next:hover {
  color: var(--text);
  border-color: var(--accent);
  background: var(--bg3);
}

.lp-scroll-next-hidden {
  opacity: 0;
  pointer-events: none;
}

@keyframes lp-scroll-next-bounce {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50% { transform: translateX(-50%) translateY(6px); }
}

/* Soft pulsing "orb" ring, same expanding-box-shadow technique as
   #lp-signin's lp-btn-glow/lp-btn-glow-light — see that comment for why
   this needs its own named light-mode keyframe rather than a var(--accent)
   box-shadow (rgba() can't take a custom property as its color channels). */
@keyframes lp-scroll-next-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(249,131,46, 0.35), 0 0 16px 4px rgba(249,131,46,0.18); }
  50% { box-shadow: 0 0 0 8px rgba(249,131,46, 0), 0 0 16px 4px rgba(249,131,46,0.32); }
}
@keyframes lp-scroll-next-glow-light {
  0%, 100% { box-shadow: 0 0 0 0 rgba(22,79,155, 0.35), 0 0 16px 4px rgba(22,79,155,0.18); }
  50% { box-shadow: 0 0 0 8px rgba(22,79,155, 0), 0 0 16px 4px rgba(22,79,155,0.32); }
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .lp-scroll-next { animation-name: lp-scroll-next-bounce, lp-scroll-next-glow-light; }
}
:root[data-theme="light"] .lp-scroll-next { animation-name: lp-scroll-next-bounce, lp-scroll-next-glow-light; }

@media (prefers-reduced-motion: reduce) {
  /* No pulsing, but keep a static glow rather than none at all — same
     dark-default/light-override precedence as the animated version above. */
  .lp-scroll-next { animation: none; box-shadow: 0 0 14px 3px rgba(249,131,46,0.22); }
  :root[data-theme="light"] .lp-scroll-next { box-shadow: 0 0 14px 3px rgba(22,79,155,0.22); }
}
@media (prefers-reduced-motion: reduce) and (prefers-color-scheme: light) {
  :root:not([data-theme]) .lp-scroll-next { box-shadow: 0 0 14px 3px rgba(22,79,155,0.22); }
}

@media (max-width: 768px) {
  .lp-scroll-next { bottom: 20px; width: 40px; height: 40px; font-size: 16px; }
}

/* ── Hero ── */
.lp-hero {
  min-height: 90vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 80px 24px 60px;
  position: relative;
  background: radial-gradient(ellipse 75% 70% at 50% 42%, rgba(249,131,46,0.10) 0%, rgba(249,131,46,0.04) 45%, transparent 75%),
              var(--bg);
  border-bottom: 1px solid var(--border);
}

/* Same "shouldn't look like a leftover orange decoration" reasoning as
   .lp-ambient-glow above — and the same intensity problem too. The original
   swap just recolored orange->navy at the same 0.10/0.04 alpha, but that
   alpha was tuned for how visible warm orange reads against near-black;
   against near-white it's nearly invisible (same relative-contrast
   perception issue, see the .lp-ambient-glow comment above for the actual
   numbers). Boosted here too, and using the brighter --accent-hover blue
   rather than the deeper --accent navy, same reasoning as the ambient glow. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .lp-hero {
    background: radial-gradient(ellipse 75% 70% at 50% 42%, rgba(36,108,203,0.22) 0%, rgba(36,108,203,0.09) 45%, transparent 75%),
                var(--bg);
  }
}
:root[data-theme="light"] .lp-hero {
  background: radial-gradient(ellipse 75% 70% at 50% 42%, rgba(36,108,203,0.22) 0%, rgba(36,108,203,0.09) 45%, transparent 75%),
              var(--bg);
}

.lp-hero-inner {
  max-width: 680px;
  width: 100%;
}

.lp-hero-headline {
  font-size: clamp(2.2rem, 6vw, 3.8rem);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -0.03em;
  color: var(--text);
  /* Breathing room above the headline, now that it's the first thing in
     .lp-hero-inner (the logo that used to sit above it, and provide this
     spacing for free, was removed once the sticky nav made it redundant
     — see .lp-nav). */
  margin: 40px 0 20px;
}

.lp-hero-gradient {
  background: linear-gradient(90deg, var(--accent) 0%, var(--gold) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.lp-hero-sub {
  font-size: 17px;
  line-height: 1.6;
  color: var(--text-2);
  max-width: 520px;
  margin: 0 auto 36px;
}

.lp-hero-actions {
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
  margin-bottom: 56px;
}

.lp-hero-actions .btn {
  min-width: 140px;
  padding: 14px 28px;
  font-size: 15px;
  font-weight: 700;
}

/* Pulsing glow ring on the hero's primary CTA — draws the eye without a heavy effect */
#lp-signin {
  animation: lp-btn-glow 2.4s ease-in-out infinite;
}

@keyframes lp-btn-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(249,131,46, 0.45), 0 4px 14px rgba(249,131,46,0.25); }
  50% { box-shadow: 0 0 0 10px rgba(249,131,46, 0), 0 4px 14px rgba(249,131,46,0.35); }
}

/* @keyframes can't be conditionally scoped by a parent selector, so light
   mode gets its own named keyframe (navy, not the dark theme's orange) and
   #lp-signin switches which one it plays. */
@keyframes lp-btn-glow-light {
  0%, 100% { box-shadow: 0 0 0 0 rgba(22,79,155, 0.45), 0 4px 14px rgba(22,79,155,0.25); }
  50% { box-shadow: 0 0 0 10px rgba(22,79,155, 0), 0 4px 14px rgba(22,79,155,0.35); }
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) #lp-signin { animation-name: lp-btn-glow-light; }
}
:root[data-theme="light"] #lp-signin { animation-name: lp-btn-glow-light; }

/* Staggered entrance — hero content fades/slides up on mount, one beat at a time */
@keyframes lp-fade-up {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}

.lp-hero-headline,
.lp-hero-sub,
.lp-hero-actions,
.lp-hero-stat {
  animation: lp-fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) backwards;
}

.lp-hero-headline { animation-delay: 0ms; }
.lp-hero-sub      { animation-delay: 90ms; }
.lp-hero-actions  { animation-delay: 180ms; }
.lp-hero-stat:nth-child(1) { animation-delay: 290ms; }
.lp-hero-stat:nth-child(2) { animation-delay: 350ms; }
.lp-hero-stat:nth-child(3) { animation-delay: 410ms; }
.lp-hero-stat:nth-child(4) { animation-delay: 470ms; }

@media (prefers-reduced-motion: reduce) {
  #lp-signin,
  .lp-hero-headline,
  .lp-hero-sub,
  .lp-hero-actions,
  .lp-hero-stat {
    animation: none;
  }
}

/* Faux leaderboard preview grid */
.lp-hero-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  max-width: 440px;
  margin: 0 auto;
}

.lp-hero-stat {
  background: var(--bg3);
  border: 1px solid var(--border);
  padding: 16px 20px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  text-align: left;
}

.lp-stat-gold { border-color: var(--gold); background: var(--gold-dim); }
.lp-stat-green { border-left: 3px solid var(--green); }

.lp-stat-num {
  font-size: 24px;
  font-weight: 800;
  color: var(--text);
  line-height: 1;
}

.lp-stat-unit { font-size: 14px; color: var(--text-2); font-weight: 400; margin-left: 2px; }

.lp-stat-label {
  font-size: 11px;
  color: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

/* ── Sections ── */
.lp-section {
  padding: 80px 24px;
  border-bottom: 1px solid var(--border);
}

.lp-section-dark {
  background: var(--bg2);
}

.lp-section-inner {
  max-width: 1000px;
  margin: 0 auto;
}

.lp-section-label {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--accent);
  margin-bottom: 12px;
}

.lp-section-heading {
  font-size: clamp(1.6rem, 4vw, 2.4rem);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--text);
  margin: 0 0 40px;
}

/* Scroll-reveal utility, shared by feature cards, drill cards, and steps
   below — a plain opacity/translateY transition rather than a keyframe
   (unlike the hero's lp-fade-up) since these fire on-demand from an
   IntersectionObserver in js/pages/home.js the first time each element
   scrolls into view, not all at once on mount. --i (set inline per
   element in the markup, 0-based index within its section) staggers the
   reveal one beat at a time, same idea as the hero's per-child
   animation-delay above. Reduced motion shows everything immediately and
   skips the transition entirely, regardless of whether the observer has
   added .lp-revealed yet — the class toggle is harmless either way. */
.lp-reveal {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
  transition-delay: calc(var(--i, 0) * 70ms);
}

.lp-reveal.lp-revealed {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .lp-reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ── Feature cards ── */
.lp-features {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}

/* A lone leftover card (count not a multiple of 3) starts a fresh row by
   itself — nth-child(3n+1) catches "first in its row", :last-child narrows
   that to only when it's ALSO the final card, i.e. genuinely alone. Center
   it in the middle column instead of leaving it flush-left. Self-adjusting:
   with a multiple-of-3 count this never matches anything. */
.lp-feature-card:nth-child(3n+1):last-child {
  grid-column: 2;
}

.lp-feature-card {
  background: var(--bg3);
  border: 1px solid var(--border);
  padding: 28px 24px;
  transition: border-color 0.2s, transform 0.25s, box-shadow 0.25s;
}

.lp-feature-card:hover {
  border-color: var(--border-hi);
  transform: translateY(-4px);
  box-shadow: 0 12px 28px rgba(0,0,0,0.18);
}

/* Icon sits inline with the title (same head-row pattern as .lp-drill-head/
   .lp-step-head below), not stacked above it. */
.lp-feature-head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}

.lp-feature-icon {
  flex-shrink: 0;
  font-size: 32px;
  color: var(--accent);
  line-height: 1;
  transition: transform 0.25s;
}

/* Icon nudges independently of the card's own lift — a small "this icon
   represents something that moves/responds" cue rather than pure
   decoration, per the "purposeful micro-animation" landing-page trend. */
.lp-feature-card:hover .lp-feature-icon {
  transform: translateY(-3px) scale(1.08);
}

.lp-feature-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
}

.lp-feature-body {
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-2);
}

/* ── Drill libraries ── */
.lp-drills {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}

/* Same self-adjusting "lone leftover card" centering as .lp-feature-card
   above — see the comment there. */
.lp-drill-card:nth-child(3n+1):last-child {
  grid-column: 2;
}

.lp-drill-card {
  border: 1px solid var(--border);
  border-top: 3px solid var(--accent);
  padding: 24px;
  background: var(--bg3);
  transition: transform 0.25s, box-shadow 0.25s, border-top-color 0.25s;
}

.lp-drill-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 12px 28px rgba(0,0,0,0.18);
  border-top-color: var(--accent-hover);
}

.lp-drill-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}

.lp-drill-icon {
  display: flex;
  flex-shrink: 0;
  font-size: 26px;
  color: var(--accent);
  line-height: 1;
}

.lp-drill-icon img { width: 26px; height: 26px; object-fit: contain; }

.lp-drill-name {
  font-size: 16px;
  font-weight: 800;
  color: var(--text);
}

.lp-drill-name a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color 0.2s;
}

.lp-drill-name a:hover { border-color: var(--text-3); }

.lp-drill-meta {
  font-size: 12px;
  color: var(--accent);
  font-weight: 600;
  margin-bottom: 12px;
}

.lp-drill-desc {
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-2);
}

/* ── Steps ── */
/* 2×2 card grid — same visual language as .lp-feature-card (icon, hover
   lift) and .lp-drill-card (small accent label above the title), rather
   than being the one plain vertical list on a page otherwise built from
   card grids. Sequence still reads clearly via the "Step N" label, same
   role .lp-drill-meta plays for drill cards. */
.lp-steps {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
}

.lp-step-card {
  background: var(--bg3);
  border: 1px solid var(--border);
  padding: 28px 24px;
  transition: border-color 0.2s, transform 0.25s, box-shadow 0.25s;
}

.lp-step-card:hover {
  border-color: var(--border-hi);
  transform: translateY(-4px);
  box-shadow: 0 12px 28px rgba(0,0,0,0.18);
}

/* Icon sits inline with the Step N label + title stack, same pattern as
   .lp-feature-head/.lp-drill-head. */
.lp-step-head {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}

.lp-step-icon {
  flex-shrink: 0;
  font-size: 32px;
  color: var(--accent);
  line-height: 1;
  transition: transform 0.25s;
}

.lp-step-card:hover .lp-step-icon { transform: translateY(-3px) scale(1.08); }

.lp-step-num-label {
  font-size: 12px;
  color: var(--accent);
  font-weight: 600;
  margin-bottom: 2px;
}

.lp-step-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
}

.lp-step-body {
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-2);
}

/* ── Donate ── */
.lp-donate-wrap {
  text-align: center;
}

.lp-donate-wrap .lp-section-heading { margin-bottom: 16px; }

.lp-donate-sub {
  font-size: 15px;
  line-height: 1.6;
  color: var(--text-2);
  max-width: 500px;
  margin: 0 auto 36px;
}

.lp-donate-amounts {
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
  margin-bottom: 20px;
}

.lp-donate-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 72px;
  padding: 14px 20px;
  background: var(--bg3);
  border: 2px solid var(--border);
  color: var(--text);
  font-size: 16px;
  font-weight: 700;
  text-decoration: none;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}

.lp-donate-btn:hover {
  border-color: var(--accent);
  background: var(--accent-dim);
  color: var(--accent);
}

.lp-donate-btn-other {
  border-style: dashed;
  color: var(--text-2);
}

.lp-donate-cards {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-top: 20px;
  margin-bottom: 10px;
}

.pay-card-svg {
  height: 28px;
  width: 44px;
  border-radius: 4px;
  opacity: 0.9;
}

.lp-donate-note {
  font-size: 12px;
  color: var(--text-3);
}

/* ── CTA ── */
.lp-cta {
  background: radial-gradient(ellipse 60% 80% at 50% 50%, rgba(249,131,46,0.06) 0%, transparent 70%);
  border-bottom: none;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .lp-cta {
    background: radial-gradient(ellipse 60% 80% at 50% 50%, rgba(22,79,155,0.06) 0%, transparent 70%);
  }
}

/* ── Print (qualification PDF export) ─────────────────────────
   Exported via the browser's native print-to-PDF rather than a PDF
   library, to avoid adding a dependency to a build-step-free app.
   #print-report is populated just before window.print() is called
   (see printQualificationReport() in js/utils.js) and is otherwise
   always empty/hidden. Colors are hardcoded black-on-white instead
   of theme tokens — --text/--bg invert under dark theme and would
   be illegible on printed paper regardless of which theme is active
   when the export is triggered. */
#print-report { display: none; }

@media print {
  body * { visibility: hidden; }
  #print-report, #print-report * { visibility: visible; }
  #print-report {
    display: block;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    color: #111;
    background: #fff;
    font-family: var(--font);
  }

  .print-header {
    margin-bottom: 20px;
    border-bottom: 2px solid #111;
    padding-bottom: 12px;
  }

  .print-title { font-size: 20px; font-weight: 800; }
  .print-meta { font-size: 12px; color: #444; margin-top: 4px; }

  .print-table { width: 100%; border-collapse: collapse; font-size: 12px; }
  .print-table th,
  .print-table td { border: 1px solid #999; padding: 6px 8px; text-align: left; vertical-align: top; }
  .print-table th { background: #eee; }
}
:root[data-theme="light"] .lp-cta {
  background: radial-gradient(ellipse 60% 80% at 50% 50%, rgba(22,79,155,0.06) 0%, transparent 70%);
}

.lp-cta-btn {
  padding: 16px 40px;
  font-size: 16px;
  font-weight: 700;
}

/* ── Footer ── */
.lp-footer {
  background: var(--bg2);
  border-top: 1px solid var(--border);
  padding: 24px;
}

.lp-footer-inner {
  max-width: 1000px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 16px;
}

.lp-footer-logo {
  height: 24px;
  width: auto;
}

.lp-footer-copy {
  font-size: 13px;
  color: var(--text-3);
  flex: 1;
}

.lp-footer-link {
  font-size: 13px;
  color: var(--text-2);
  text-decoration: none;
}

.lp-footer-link:hover { color: var(--accent); }

.lp-footer-powered {
  max-width: 1000px;
  margin: 14px auto 0;
  padding-top: 14px;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

.lp-footer-powered-text {
  font-size: 12px;
  color: var(--text-3);
}

.lp-footer-powered-link {
  display: inline-flex;
  align-items: center;
  opacity: 0.85;
  transition: opacity 0.15s;
}

.lp-footer-powered-link:hover { opacity: 1; }

.lp-footer-powered-logo {
  height: 36px;
  width: auto;
  display: block;
}

/* ── Donate Modal ── */
.donate-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.75);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 20px;
}

.donate-modal-box {
  background: var(--bg2);
  border: 1px solid var(--border);
  border-radius: 12px;
  width: 100%;
  max-width: 780px;
  max-height: 92vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  box-shadow: 0 24px 64px rgba(0,0,0,0.6);
}

.donate-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  flex-shrink: 0;
}

.donate-modal-close {
  background: none;
  border: none;
  color: var(--text-3);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
  transition: color 0.15s;
}

.donate-modal-close:hover { color: var(--text); }

.donate-modal-body {
  flex: 1;
  overflow-y: auto;
  min-height: 200px;
  -webkit-overflow-scrolling: touch;
}

.donate-modal-iframe {
  width: 100%;
  min-height: 620px;
  border: none;
  display: block;
  overflow: hidden;
}

/* ── Confirm modal (replaces native confirm()/alert()) ── */
.confirm-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.75);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 20px;
  opacity: 0;
  animation: confirm-modal-fade-in 0.15s ease forwards;
}

@keyframes confirm-modal-fade-in { to { opacity: 1; } }

.confirm-modal-box {
  background: var(--bg2);
  border: 1px solid var(--border-hi);
  width: 100%;
  max-width: 420px;
  box-shadow: 0 24px 64px rgba(0,0,0,0.6);
  padding: 24px;
  transform: translateY(8px) scale(0.98);
  animation: confirm-modal-pop-in 0.15s ease forwards;
}

@keyframes confirm-modal-pop-in { to { transform: translateY(0) scale(1); } }

.confirm-modal-title {
  font-size: 16px;
  font-weight: 800;
  color: var(--text);
  margin-bottom: 10px;
}

.confirm-modal-message {
  font-size: 14px;
  color: var(--text-2);
  line-height: 1.6;
  white-space: pre-line;
  margin-bottom: 24px;
}

.confirm-modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}

/* ── Help modal ── */
.help-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.75);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 20px;
  opacity: 0;
  animation: confirm-modal-fade-in 0.15s ease forwards;
}

.help-modal-box {
  position: relative;
  background: var(--bg2);
  border: 1px solid var(--border-hi);
  width: 100%;
  max-width: 480px;
  max-height: 85vh;
  overflow-y: auto;
  box-shadow: 0 24px 64px rgba(0,0,0,0.6);
  padding: 28px;
  transform: translateY(8px) scale(0.98);
  animation: confirm-modal-pop-in 0.15s ease forwards;
}

.help-modal-close {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--text-2);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.help-modal-close:hover { color: var(--text); }

.help-modal-header { margin-bottom: 20px; padding-right: 24px; }

.help-modal-title {
  font-size: 19px;
  font-weight: 800;
  color: var(--text);
  margin-bottom: 6px;
}

.help-modal-subtitle {
  font-size: 13px;
  color: var(--text-2);
}

.help-modal-body {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin-bottom: 24px;
}

.help-modal-section {
  display: flex;
  gap: 12px;
}

.help-modal-section-num {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--bg3);
  border: 1px solid var(--border);
  color: var(--accent);
  font-size: 11px;
  font-weight: 800;
  display: flex;
  align-items: center;
  justify-content: center;
}

.help-modal-section-title {
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 3px;
}

.help-modal-section-body {
  font-size: 13px;
  color: var(--text-2);
  line-height: 1.55;
}

.help-modal-section-body strong { color: var(--text); }

.help-modal-actions {
  display: flex;
  justify-content: flex-end;
}

/* ── Timer modal (BLE shot timer picker, Log Score page) ──
   Same backdrop/box treatment as the help modal above, own class names
   since it's an unrelated feature. */
.timer-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.75);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 20px;
  opacity: 0;
  animation: confirm-modal-fade-in 0.15s ease forwards;
}

.timer-modal-box {
  position: relative;
  background: var(--bg2);
  border: 1px solid var(--border-hi);
  width: 100%;
  max-width: 440px;
  max-height: 85vh;
  overflow-y: auto;
  box-shadow: 0 24px 64px rgba(0,0,0,0.6);
  padding: 28px;
  transform: translateY(8px) scale(0.98);
  animation: confirm-modal-pop-in 0.15s ease forwards;
}

.timer-modal-close {
  position: absolute;
  top: 16px;
  right: 16px;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--text-2);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.timer-modal-close:hover { color: var(--text); }

.timer-modal-header { margin-bottom: 16px; padding-right: 24px; }

.timer-modal-title {
  font-size: 18px;
  font-weight: 800;
  color: var(--text);
  margin-bottom: 6px;
}

.timer-modal-subtitle {
  font-size: 13px;
  color: var(--text-2);
  line-height: 1.5;
}

.timer-modal-body {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.timer-device-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: var(--bg3);
  border: 1px solid var(--border);
  padding: 12px 14px;
  cursor: pointer;
  text-align: left;
  width: 100%;
  transition: border-color 0.15s;
  font-family: inherit;
}
.timer-device-option:hover:not(:disabled) { border-color: var(--accent); }
.timer-device-option:disabled { cursor: not-allowed; opacity: 0.55; }

.timer-device-name { font-size: 14px; font-weight: 700; color: var(--text); }
.timer-device-desc { font-size: 12px; color: var(--text-3); margin-top: 2px; }

/* ── Timer connection status chip (Log Score page) ── */
.timer-status-chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--accent-dim);
  border: 1px solid var(--accent);
  padding: 6px 10px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text);
}

.timer-status-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--green);
  flex-shrink: 0;
  animation: timer-status-pulse 1.6s ease-in-out infinite;
}

@keyframes timer-status-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.35; }
}

/* Same background-only-fill risk as .switch-thumb above — the connected/
   disconnected state would otherwise be unreadable under forced-colors. */
@media (forced-colors: active) {
  .timer-status-dot { border: 1px solid CanvasText; }
}

/* Shot-timer header icon (Log Score) — replaces what used to be a permanent
   "Connect Shot Timer" button + label + hint sitting in the main form,
   costing a fixed block of vertical space on a page whose whole point is
   fitting on one screen with no scrolling (see ui-ux-trend-audit.md). Blue
   by default (Bluetooth's own conventional color, and distinct from the
   accent/gold/green already in heavy use elsewhere on this page) whether
   connected or not, since it's a Bluetooth control either way — the small
   dot is what actually carries connected-vs-not, matching the same
   division of labor .timer-status-dot already uses in the status chip. */
.timer-icon-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 0;
  color: var(--blue);
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
}
.timer-icon-btn::before { content: ''; position: absolute; inset: -11px; }
.timer-icon-btn:hover { filter: brightness(1.2); }
.timer-icon-btn.connecting { animation: timer-status-pulse 1.2s ease-in-out infinite; }

.timer-icon-dot {
  position: absolute;
  top: -1px;
  right: -1px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--green);
  border: 1px solid var(--bg2);
  animation: timer-status-pulse 1.6s ease-in-out infinite;
}

/* Same background-only-fill risk as .switch-thumb/.timer-status-dot above. */
@media (forced-colors: active) {
  .timer-icon-dot { border: 1px solid CanvasText; }
}

/* ── Shot capture confirmation (BLE, Log Score page) ──
   Center-screen rather than the bottom toast rail — see
   js/ble/captureConfirm.js for why. Non-blocking (pointer-events: none, no
   dismiss control) since a real drill can fire this several times a minute;
   it has to get out of the way on its own rather than demand a tap. */
.capture-confirm {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 2000;
}

.capture-confirm-scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  opacity: 0;
  transition: opacity 0.15s ease;
}
.capture-confirm.show .capture-confirm-scrim { opacity: 1; }
.capture-confirm.hide .capture-confirm-scrim {
  opacity: 0;
  /* Trails the card slightly on the way out so the dimming doesn't vanish
     in the same instant the card does. */
  transition: opacity 0.32s ease;
}

.capture-confirm-card {
  position: relative;
  background: var(--bg3);
  border: 1px solid var(--accent);
  padding: 22px 30px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  opacity: 0;
  transform: scale(0.82);
  transition: opacity 0.16s ease, transform 0.16s cubic-bezier(0.2, 0.8, 0.3, 1.3);
}
.capture-confirm.show .capture-confirm-card {
  opacity: 1;
  transform: scale(1);
}
.capture-confirm.hide .capture-confirm-card {
  opacity: 0;
  transform: scale(0.94);
  transition: opacity 0.22s ease, transform 0.22s ease;
}

/* Corner brackets snap inward on show — reads as a target lock rather than
   a generic checkmark pop, tying the moment back to what's being captured. */
.capture-confirm-bracket {
  position: absolute;
  width: 14px;
  height: 14px;
  border: 2px solid var(--accent);
  opacity: 0;
  transition: opacity 0.18s ease, transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.capture-confirm-bracket.tl { top: -7px; left: -7px; border-right: none; border-bottom: none; transform: translate(-6px, -6px); }
.capture-confirm-bracket.tr { top: -7px; right: -7px; border-left: none; border-bottom: none; transform: translate(6px, -6px); }
.capture-confirm-bracket.bl { bottom: -7px; left: -7px; border-right: none; border-top: none; transform: translate(-6px, 6px); }
.capture-confirm-bracket.br { bottom: -7px; right: -7px; border-left: none; border-top: none; transform: translate(6px, 6px); }
.capture-confirm.show .capture-confirm-bracket { opacity: 1; transform: translate(0, 0); }

.capture-confirm-icon {
  width: 30px;
  height: 30px;
  color: var(--accent);
  margin-bottom: 2px;
}

.capture-confirm-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--text-2);
}

.capture-confirm-time {
  font-variant-numeric: tabular-nums;
  font-size: 32px;
  font-weight: 800;
  color: var(--text);
  line-height: 1;
}
.capture-confirm-time .unit {
  font-size: 16px;
  color: var(--text-3);
  font-weight: 700;
}

@media (prefers-reduced-motion: reduce) {
  .capture-confirm-card,
  .capture-confirm-bracket {
    transition: opacity 0.12s linear !important;
    transform: none !important;
  }
}

/* ── Interactive demo (js/pages/demo/) ── */
/* .demo-banner and #demo-nav render inside #page-container alongside the
   demo's own page content (unlike the real app, where #nav lives outside
   #page-container entirely and so needs no such trick) — the negative
   margins pull them back out to #page-container's own edges so they read
   as full-width chrome rather than being trapped inside its 32px/24px
   padding. #demo-bottom-nav needs no such treatment since .bottom-nav is
   already position:fixed (viewport-relative, ignores its parent's box
   entirely) — and the existing mobile #page-container padding-bottom
   (below, in the 768px block) already clears it for demo content too,
   for free, since it's the same #page-container. */
.demo-banner,
#demo-nav {
  margin-left: -24px;
  margin-right: -24px;
}

/* On the real app, #nav lives outside #page-container, so the container's
   own 32px top padding is entirely the gap between nav and page content.
   Here, #demo-nav lives inside #page-container (see comment above) right
   after .demo-banner, whose negative margin-top already cancels that 32px
   padding out to make the banner sit flush with the true page edge — by
   the time #demo-nav's content follows, that padding has been "spent" and
   nothing is left to separate the nav from the page title below it. This
   restores it on the one element (<main>) common to every demo page. */
.demo-page-content {
  margin-top: 32px;
}

.demo-banner {
  margin-top: -32px;
  margin-bottom: 0;
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  padding: 10px 24px;
  background: var(--accent-dim);
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}

.demo-banner-text {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--text);
  font-weight: 600;
}

.demo-banner-text i { color: var(--accent); }

.demo-role-toggle {
  display: inline-flex;
  border: 1px solid var(--border);
  margin-left: auto;
}

.demo-role-btn {
  padding: 5px 12px;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-2);
  background: none;
  border: none;
  cursor: pointer;
  transition: color 0.15s, background 0.15s;
}

.demo-role-btn.active {
  color: var(--accent);
  background: var(--bg3);
}

.demo-exit-link {
  background: none;
  border: 1px solid var(--border);
  color: var(--text-2);
  font-size: 12px;
  font-weight: 600;
  padding: 5px 12px;
  cursor: pointer;
  transition: color 0.15s, border-color 0.15s;
}

.demo-exit-link:hover { color: var(--text); border-color: var(--border-hi); }

/* ── Responsive ── */
@media (max-width: 768px) {
  .nav-logo-img { height: 28px; }
  .auth-logo-img { height: 44px; }

  /* Landing page mobile */
  .lp-nav { height: 56px; }
  .lp-nav-inner { padding: 0 16px; gap: 12px; }
  .lp-nav-logo { height: 24px; }
  .lp-nav-links { display: none; }
  .lp-nav-signin { font-size: 12px; padding: 7px 10px; }
  .lp-hero { padding: 60px 20px 48px; min-height: auto; }
  .lp-hero-actions .btn { min-width: 120px; padding: 12px 20px; }
  .lp-hero-grid { grid-template-columns: 1fr 1fr; max-width: 100%; }
  .lp-section { padding: 56px 20px; }
  .lp-features { grid-template-columns: 1fr; gap: 12px; }
  .lp-feature-card:nth-child(3n+1):last-child { grid-column: auto; }
  .lp-drills { grid-template-columns: 1fr; gap: 12px; }
  .lp-drill-card:nth-child(3n+1):last-child { grid-column: auto; }
  .lp-steps { grid-template-columns: 1fr; gap: 12px; }
  .lp-donate-amounts { gap: 8px; }
  .lp-donate-btn { min-width: 60px; padding: 12px 16px; font-size: 15px; }
  .lp-footer-inner { flex-wrap: wrap; gap: 8px; }

  /* Switch to bottom nav */
  .nav-links { display: none; }
  .nav-right .nav-member { display: none; }
  .bottom-nav { display: flex; }

  /* Account for bottom nav height */
  #page-container {
    padding: 20px 16px 88px;
  }

  /* Matches #page-container's mobile padding above — see the .demo-banner
     comment near its declaration for why this bleed exists at all. */
  .demo-banner,
  #demo-nav {
    margin-left: -16px;
    margin-right: -16px;
  }
  .demo-banner { margin-top: -20px; }
  .demo-page-content { margin-top: 20px; }

  /* Nav: logo + icon buttons (theme toggle, donate, feedback, help) + sign
     out. Tighter gap than desktop's 12px — six items in this row on the
     narrowest real phones (~320-360px) leaves little headroom otherwise. */
  .nav { padding: 0 16px; gap: 10px; height: 50px; }
  .nav-right { gap: 4px; }
  .nav-team { display: none; }

  /* Page headers */
  .page-header { flex-direction: column; gap: 12px; }
  .page-header .btn { width: 100%; justify-content: center; }
  /* Column-stacking exists for headers with a wide action button (Drills'
     "+ Add Drill", etc.) that needs its own full-width row below the
     title. The qual-status seal (and, on Log Score, the shot-timer icon
     beside it) is the opposite case — a small, fixed-size icon that reads
     correctly sitting inline beside the title, same as it does on desktop,
     and looks like a mistake dropped onto its own line below a title it's
     clearly meant to sit next to. :has() lets this stay a row specifically
     when one of those icons is present without a modifier class or
     touching every page-header call site to opt in. */
  .page-header:has(.qual-seal-btn), .page-header:has(.timer-icon-btn) { flex-direction: row; align-items: center; }

  /* Stats bar: single column, each cell a horizontal label/value row
     instead of a stacked block. 2 columns left an odd cell stranded next
     to an empty box whenever a page had 3 (or 5) stats — single column
     always produces exactly N rows for N cells, no orphans possible
     regardless of count. Grid (not flex) so an optional .stat-cell-sub
     (e.g. Leaderboard's "Team best" member-name line) drops onto its own
     row under the value with no markup changes — a cell that never has a
     sub just doesn't use that grid row. */
  .stats-bar { grid-template-columns: 1fr; }
  .stat-cell {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: baseline;
    gap: 2px 12px;
    padding: 12px 16px;
  }
  .stat-cell-label { grid-column: 1; grid-row: 1; margin-bottom: 0; }
  .stat-cell-value { grid-column: 2; grid-row: 1; text-align: right; font-size: 20px; }
  .stat-cell-sub { grid-column: 2; grid-row: 2; text-align: right; margin-top: 0; }

  /* Personal-best card (My Scores) needs no mobile override anymore — the
     name+value / trend+spark grid (css/style.css's .pb-card-header, near
     the top of this file) is now the same structure on every screen size,
     not just mobile, so there's nothing left to remap here. */

  /* Leaderboard: horizontal scroll on mobile */
  .lb-table-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border: 1px solid var(--border);
  }
  .lb-table { border: none; min-width: 480px; }

  /* Leaderboard: sticky identity column + scroll edge-fade cues (Mobile &
     Responsive Design framework, CLAUDE.md item 8 — "a gesture always needs
     a visible affordance"; also closes the Part 7 item from
     ui-ux-trend-audit.md flagging this exact table). The Member column
     (2nd child — rank stays scrollable) pins to the left edge of
     .lb-table-wrap as the row scrolls, so you never lose track of whose row
     you're looking at. Its background is set per row-state to match
     .lb-table's own tbody/hover/rank-1..3 rules above, since a sticky cell
     paints its own background rather than inheriting the row's. Scoped to
     .lb-scroll-container (opt-in per caller, see js/utils.js's
     wireScrollFade) rather than every .lb-table-wrap, since admin.js/
     owner.js reuse that class for tables with a differently-shaped first
     column. */
  .lb-scroll-container .lb-table-wrap .lb-table td:nth-child(2),
  .lb-scroll-container .lb-table-wrap .lb-table th:nth-child(2) {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--bg2);
    border-right: 1px solid var(--border);
  }
  .lb-scroll-container .lb-table-wrap .lb-table thead th:nth-child(2) { background: var(--bg3); z-index: 2; }
  .lb-scroll-container .lb-table-wrap .lb-table tbody tr:hover td:nth-child(2) { background: var(--bg3); }
  .lb-scroll-container .lb-table-wrap .lb-table tbody tr.rank-1 td:nth-child(2) { background: var(--gold-dim); }
  .lb-scroll-container .lb-table-wrap .lb-table tbody tr.rank-2 td:nth-child(2) { background: rgba(148,163,184,0.05); }
  .lb-scroll-container .lb-table-wrap .lb-table tbody tr.rank-3 td:nth-child(2) { background: rgba(180,120,60,0.05); }

  /* The fade cues themselves — drawn on .lb-scroll-container (the
     non-scrolling positioning parent) so they stay pinned to the viewport
     edge instead of scrolling away with the table content inside
     .lb-table-wrap. js/utils.js's wireScrollFade() toggles has-scroll-left/
     has-scroll-right based on actual scroll position, so a cue only shows
     while there's genuinely more content in that direction — e.g. the right
     fade disappears once you've scrolled all the way to the last column. */
  .lb-scroll-container { position: relative; }
  .lb-scroll-container::before,
  .lb-scroll-container::after {
    content: '';
    position: absolute;
    top: 1px;
    bottom: 1px;
    width: 28px;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.15s;
    z-index: 3;
  }
  .lb-scroll-container::before { left: 1px; background: linear-gradient(to left, transparent, var(--bg2) 80%); }
  .lb-scroll-container::after { right: 1px; background: linear-gradient(to right, transparent, var(--bg2) 80%); }
  .lb-scroll-container.has-scroll-left::before { opacity: 1; }
  .lb-scroll-container.has-scroll-right::after { opacity: 1; }

  /* Forms: single column, larger touch targets */
  .form-row-2 { grid-template-columns: 1fr; }

  .form-input {
    font-size: 16px; /* prevents iOS auto-zoom */
    padding: 13px 14px;
    min-height: 50px;
  }

  .btn { min-height: 50px; font-size: 15px; }
  .btn-sm { min-height: 38px; font-size: 13px; }

  /* Log score — big time display */
  .time-display-number { font-size: 64px; }

  /* Drill cards */
  .drill-card { flex-direction: column; }
  .drill-card-actions { flex-direction: row; }
  .btn-delete-drill { align-self: center; }

  /* Add Drill / Import Library modals go edge-to-edge full screen on
     mobile instead of a centered dialog with visible backdrop margin —
     the "full-screen modal" pattern this was built for in the first
     place. Swaps the desktop pop-in for a slide-up-from-bottom, a more
     native-feeling motion for a panel that now owns the whole viewport. */
  .drill-fs-modal-backdrop { padding: 0; }
  .drill-fs-modal {
    max-width: none;
    max-height: none;
    height: 100%;
    height: 100dvh;
    border: none;
    transform: translateY(100%);
    animation: drill-fs-slide-up 0.2s ease forwards;
  }
  @keyframes drill-fs-slide-up { to { transform: translateY(0); } }

  /* Admin tabs: scrollable */
  .admin-tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; padding-bottom: 2px; }
  .admin-tab { white-space: nowrap; }

  /* Auth box: full width, no top padding */
  .auth-wrap { align-items: flex-start; padding-top: 24px; }
  .auth-box { max-width: 100%; }

  /* Member pick list: bigger tap targets */
  .member-pick-item { padding: 16px; font-size: 16px; }

  /* Toast: full width on mobile */
  .toast-container { left: 16px; right: 16px; bottom: 80px; }
  .toast { max-width: 100%; }
}
