/* ============================================================
   app.css — shared shell for the signed-in pages.
   ------------------------------------------------------------
   MOBILE FIRST. The base styles ARE the phone layout; wider
   screens are progressive enhancement via min-width queries.
   That's the opposite of v1's stylesheet, which built for desktop
   and hid columns on small screens — the wrong direction for an
   app used one-handed next to a table.

   Consequences that shape everything below:
   - Navigation is a BOTTOM tab bar at EVERY width. On phones
     thumbs reach the bottom; on desktop it becomes a floating
     pill. There is no second copy of the nav in the header —
     two sets of the same links was just redundant.
   - Every interactive target is at least 44px, per Apple's HIG
     and the WCAG 2.5.5 target-size guidance.
   - Safe-area insets are respected so content clears the notch
     and the home indicator when installed to the home screen.
   - Standings render as CARDS on a phone and a TABLE from 600px
     up — the same data, not a hidden subset of it.

   ── Theming ────────────────────────────────────────────────
   Every colour is a token, and the tokens come in a light and a
   dark set. Resolution order:

     1. [data-theme="light"|"dark"] on <html>  — an explicit
        choice the person made in Settings, stored per device.
     2. prefers-color-scheme                   — the OS setting.
     3. light                                  — the bare default.

   theme.js (loaded in <head>, before paint) sets the attribute
   from localStorage so there's no flash of the wrong theme. The
   dark set is written out twice, once for the media query and
   once for the explicit attribute, because CSS has no way to
   reuse a declaration block — the alternative would be applying
   dark as a class from JS, which would flash.

   NOTHING below this token block may hardcode a colour. If a new
   shade is needed it becomes a token first, or light mode breaks
   in a way that's invisible while you're working in dark.

   Club colours (--primary/--secondary) arrive from
   /api/auth/me and override the tokens per club at runtime.
   ============================================================ */

:root,
[data-theme='light'] {
  --bg: #f4f1e8;
  --bg-glow-a: rgba(15, 61, 99, .07);
  --bg-glow-b: rgba(196, 136, 28, .06);

  --surface: #ffffff;
  --surface-grad: linear-gradient(180deg, #ffffff 0%, #fafbfd 100%);
  --surface-2: #ebeef4;
  --surface-3: #e1e6ee;

  --border: rgba(16, 24, 40, .11);
  --hairline: rgba(16, 24, 40, .075);
  /* The 1px catch-light along the top edge of a raised surface. This is
     most of what reads as "physical" — more than the shadow does. */
  --edge-light: rgba(255, 255, 255, .9);
  --press: rgba(16, 24, 40, .05);

  --text: #14161a;
  --muted: #656e7d;

  /* Varsity navy. 11.3:1 on the cream background — comfortably clear of
     WCAG AA for the 11px tab labels and badge text that also use it, which
     is the constraint that ruled out a corn-gold primary: gold measured
     2.9:1 and would have failed everywhere small. Gold is the SECONDARY,
     where it only ever appears as a large fill. */
  --primary: #0f3d63;
  --primary-soft: #1d5a8c;
  --on-primary: #ffffff;
  --secondary: #c4881c;

  --tint: rgba(15, 61, 99, .09);
  --tint-strong: rgba(15, 61, 99, .16);

  --win: #12833a;
  --loss: #cf2f27;
  --win-bg: rgba(18, 131, 58, .12);
  --loss-bg: rgba(207, 47, 39, .1);
  --warn: #9a6400;
  --warn-bg: rgba(154, 100, 0, .13);

  /* Frosted layers: things that float OVER content, plus the dimmer
     behind a modal sheet. */
  --scrim: rgba(16, 24, 40, .34);
  --glass: rgba(255, 255, 255, .72);
  --glass-2: rgba(255, 255, 255, .85);
  --glass-border: rgba(16, 24, 40, .09);

  --shadow-sm: 0 1px 2px rgba(16, 24, 40, .05), 0 1px 3px rgba(16, 24, 40, .06);
  --shadow-md: 0 2px 4px rgba(16, 24, 40, .04), 0 8px 20px rgba(16, 24, 40, .07);
  --shadow-lg: 0 8px 16px rgba(16, 24, 40, .07), 0 24px 48px rgba(16, 24, 40, .13);
  --shadow-key: 0 6px 18px rgba(15, 61, 99, .34);
}

/* Dark set — see the note above about why this appears twice. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --bg: #0b1c2c;
    --bg-glow-a: rgba(227, 176, 75, .09);
    --bg-glow-b: rgba(29, 90, 140, .12);

    --surface: #161b22;
    --surface-grad: linear-gradient(180deg, #1b212b 0%, #14181f 100%);
    --surface-2: #21262d;
    --surface-3: #2b323c;

    --border: rgba(255, 255, 255, .1);
    --hairline: rgba(255, 255, 255, .065);
    --edge-light: rgba(255, 255, 255, .07);
    --press: rgba(255, 255, 255, .055);

    --text: #e6edf3;
    --muted: #8d97a5;

    --primary: #7fb2e5;
    --primary-soft: #a6cbf0;
    --on-primary: #06121f;
    --secondary: #e3b04b;

    --tint: rgba(127, 178, 229, .12);
    --tint-strong: rgba(127, 178, 229, .2);

    --win: #3fb950;
    --loss: #f85149;
    --win-bg: rgba(63, 185, 80, .16);
    --loss-bg: rgba(248, 81, 73, .14);
    --warn: #d29922;
    --warn-bg: rgba(210, 153, 34, .16);

    --scrim: rgba(0, 0, 0, .58);
    --glass: rgba(12, 15, 20, .7);
    --glass-2: rgba(18, 22, 29, .82);
    --glass-border: rgba(255, 255, 255, .09);

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .3), 0 1px 3px rgba(0, 0, 0, .25);
    --shadow-md: 0 2px 4px rgba(0, 0, 0, .3), 0 10px 24px rgba(0, 0, 0, .38);
    --shadow-lg: 0 8px 16px rgba(0, 0, 0, .35), 0 28px 56px rgba(0, 0, 0, .5);
    --shadow-key: 0 6px 20px rgba(15, 61, 99, .45);
  }
}

[data-theme='dark'] {
  --bg: #0b1c2c;
  --bg-glow-a: rgba(227, 176, 75, .09);
  --bg-glow-b: rgba(29, 90, 140, .12);

  --surface: #161b22;
  --surface-grad: linear-gradient(180deg, #1b212b 0%, #14181f 100%);
  --surface-2: #21262d;
  --surface-3: #2b323c;

  --border: rgba(255, 255, 255, .1);
  --hairline: rgba(255, 255, 255, .065);
  --edge-light: rgba(255, 255, 255, .07);
  --press: rgba(255, 255, 255, .055);

  --text: #e6edf3;
  --muted: #8d97a5;

  --primary: #7fb2e5;
  --primary-soft: #a6cbf0;
  --on-primary: #06121f;
  --secondary: #e3b04b;

  --tint: rgba(127, 178, 229, .12);
  --tint-strong: rgba(127, 178, 229, .2);

  --win: #3fb950;
  --loss: #f85149;
  --win-bg: rgba(63, 185, 80, .16);
  --loss-bg: rgba(248, 81, 73, .14);
  --warn: #d29922;
  --warn-bg: rgba(210, 153, 34, .16);

  --scrim: rgba(0, 0, 0, .58);
  --glass: rgba(12, 15, 20, .7);
  --glass-2: rgba(18, 22, 29, .82);
  --glass-border: rgba(255, 255, 255, .09);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, .3), 0 1px 3px rgba(0, 0, 0, .25);
  --shadow-md: 0 2px 4px rgba(0, 0, 0, .3), 0 10px 24px rgba(0, 0, 0, .38);
  --shadow-lg: 0 8px 16px rgba(0, 0, 0, .35), 0 28px 56px rgba(0, 0, 0, .5);
  --shadow-key: 0 6px 20px rgba(15, 61, 99, .45);
}

:root {
  --radius: 16px;
  --radius-sm: 11px;

  /* Text on a player's avatar colour. Deliberately NOT theme-dependent:
     avatar colours come from the database and are the same in both modes,
     so the ink on top has to be too. */
  --on-accent: #0b0f16;

  /* Buy Me a Coffee's yellow. Also deliberately theme-independent, and the
     only brand colour in the file: it is recognisable precisely because it is
     always this yellow, so a dark-mode variant would defeat the point of
     using it. Black on #FFDD00 is about 15:1, so the ink is fixed too.

     This exists because the CSP is `script-src 'self'` and
     `img-src 'self' data:` — BMAC's own button is offered as a remote script
     OR a remote image and both are blocked. Rebuilding it locally is not a
     workaround for the policy, it IS the policy working: no third-party code
     and no third-party request, which is what /privacy promises. */

  /* Notch / home indicator. Falls back to 0 where unsupported. */
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  --safe-right: env(safe-area-inset-right, 0px);

  --tabbar-height: 58px;
  --tap: 44px; /* minimum touch target */
  --ease: cubic-bezier(.32, .72, 0, 1);
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html {
  -webkit-text-size-adjust: 100%;
  /* Native form controls (selects, date pickers, scrollbars) pick their own
     light/dark rendering from this, so it has to track the tokens. */
  color-scheme: light;
}
@media (prefers-color-scheme: dark) {
  html:not([data-theme='light']) { color-scheme: dark; }
}
html[data-theme='dark'] { color-scheme: dark; }
html[data-theme='light'] { color-scheme: light; }

body {
  margin: 0;
  /*
   * ── WHY THE + 1px, WHICH LOOKS LIKE A TYPO ───────────────────────────
   *
   * On an iPhone in standalone mode this app is laid out in an 894px-tall
   * viewport on a 956px-tall screen. The bottom 62px is outside the page
   * entirely, and shows through as a black strip under the tab bar.
   *
   * Measured, not assumed: a throwaway probe in build .12 reported
   * `gap=0.0` between the tab bar's bottom edge and window.innerHeight in
   * every single sample, on every page, at every moment. The bar was always
   * exactly where it was told to be. What moved was innerHeight itself --
   * 894 at first paint, then 956.
   *
   * iOS only hands the page the full screen once the document is
   * SCROLLABLE. On the dashboard that happens immediately, because there is
   * more content than fits, and the strip vanishes before you notice it. On
   * Matchups the content is shorter than the screen, so the document never
   * scrolls, the viewport never expands, and the strip stays for good. The
   * probe caught the expansion happening in steps -- 894, 896, 902, 918,
   * 942, 956 -- as the page was dragged.
   *
   * ── WHY lvh AND NOT dvh ──────────────────────────────────────────────
   *
   * `dvh` is the height of the viewport AS IT IS — the short one, 894 — so a
   * page sized to `100dvh` exactly fills the short state and never triggers
   * the expansion at all. `100dvh + 1px` does trigger it, but only barely:
   * the page overflows by a single pixel, and iOS expands lazily, sliding the
   * viewport open over about two seconds. Pages with real content overflow by
   * hundreds of pixels and snap open in about half of one. The difference in
   * how it FEELS is entirely the difference in how much there is to scroll.
   *
   * `lvh` is the LARGE viewport — the full 956 — regardless of the current
   * state. So a short page is now 957 tall against an 894 viewport: 63px of
   * genuine overflow, the same order as a content-rich page, and it opens at
   * the same speed. After the expansion the page is 957 against 956, still
   * scrollable by the hair that keeps it from collapsing back.
   *
   * This is also the likeliest explanation for the tab bar "drifting" while
   * scrolling: a fixed element sits at the bottom of the viewport, and the
   * viewport was changing height underneath it mid-gesture.
   */
  min-height: calc(100dvh + 1px);   /* fallback: engines without lvh */
  min-height: calc(100lvh + 1px);
  background: var(--bg);
  color: var(--text);
  font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  /* Room for the floating tab bar: its height, the 12px it sits above the
     bottom edge, the home indicator, and a little air under the last card. */
  padding-bottom: calc(var(--tabbar-height) + var(--safe-bottom) + 32px);
  overscroll-behavior-y: contain;
}

/* Two very soft colour washes behind everything. Without them a flat --bg
   gives the frosted layers nothing to pick up, and the "glass" reads as
   plain grey. Fixed and non-interactive so it never affects scrolling. */
body::before {
  content: '';
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(60% 45% at 12% 0%, var(--bg-glow-a), transparent 70%),
    radial-gradient(55% 40% at 92% 8%, var(--bg-glow-b), transparent 72%);
}

/* ── Header ────────────────────────────────────────────────── */
.topbar {
  position: sticky; top: 0; z-index: 10;
  display: flex; align-items: center; gap: 10px;
  padding: 10px 16px;
  padding-top: calc(10px + var(--safe-top));
  padding-left: calc(16px + var(--safe-left));
  padding-right: calc(16px + var(--safe-right));
  background: var(--glass);
  backdrop-filter: saturate(180%) blur(20px);
  -webkit-backdrop-filter: saturate(180%) blur(20px);
  border-bottom: 1px solid var(--glass-border);
}
.topbar img {
  width: 30px; height: 30px; border-radius: 9px; flex: none;
  box-shadow: var(--shadow-sm);
}
.topbar .club-name {
  font-weight: 700; letter-spacing: -0.015em; font-size: 17px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.topbar .spacer { flex: 1; }
.topbar .icon-btn {
  display: grid; place-items: center;
  min-width: var(--tap); min-height: var(--tap);
  color: var(--muted); text-decoration: none; border-radius: var(--radius-sm);
  transition: background .18s var(--ease), color .18s var(--ease);
}
.topbar .icon-btn:hover { color: var(--text); background: var(--press); }
.topbar .icon-btn:active { background: var(--surface-2); }

/* ── Bottom tab bar ────────────────────────────────────────── */
/*
 * A floating pill at EVERY width, not just on desktop.
 *
 * It used to span the full width and sit flush against the bottom edge on
 * phones, becoming a centred pill only at 600px+. Two reasons it now floats
 * everywhere:
 *
 * 1. It looks better, and it is what the desktop layout already did.
 *
 * 2. It sidesteps the bottom edge of the screen, which on iOS is genuinely
 *    hard to sit on. The app is laid out in an 894px viewport on a 956px
 *    display until the page becomes scrollable, and iOS then ANIMATES the
 *    expansion over about a second (measured: 894, 895, 897, 900, 904 ... 956).
 *    A bar flush to the bottom is visibly short of the screen for that whole
 *    animation. A bar that was never touching the bottom has nothing to be
 *    short of — the gap beneath it is intentional, and the page background
 *    fills it at any viewport height.
 *
 * Full-width-minus-a-margin rather than the desktop pill's fit-to-content:
 * five labels at content width would leave "Matchups" cramped on a 393px
 * screen. The rounded ends still read as floating.
 */
.tabbar {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: calc(12px + var(--safe-bottom));
  z-index: 20;
  display: flex;
  height: var(--tabbar-height);
  padding: 0 4px;
  background: var(--glass-2);
  backdrop-filter: saturate(180%) blur(24px);
  -webkit-backdrop-filter: saturate(180%) blur(24px);
  border: 1px solid var(--glass-border);
  border-radius: 999px;
  box-shadow: var(--shadow-lg);
}
.tabbar a {
  flex: 1;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  min-height: var(--tap);
  color: var(--muted); text-decoration: none;
  font-size: 11px; font-weight: 500;
  transition: color .18s var(--ease), transform .18s var(--ease);
}
.tabbar a svg { width: 22px; height: 22px; }
.tabbar a[aria-current='page'] { color: var(--primary); }
.tabbar a:active { transform: scale(.94); }

/* The record button sits in the middle and is visually primary — it's
   the action people open the app to perform. */
.tabbar .record { color: var(--text); }
.tabbar .record .fab {
  /* No negative margin: the FAB overhung the old flush bar to sit proud of
     it, but a pill has a rounded top edge and the overhang reads as a
     misalignment rather than as emphasis. Size carries it instead. */
  width: 44px; height: 44px;
  border-radius: 50%; display: grid; place-items: center;
  background: linear-gradient(180deg, var(--primary-soft), var(--primary));
  color: var(--on-primary);
  box-shadow: var(--shadow-key), inset 0 1px 0 var(--edge-light);
  transition: transform .18s var(--ease), box-shadow .18s var(--ease);
}
.tabbar .record:active .fab { transform: scale(.92); }
.tabbar .record .fab svg { width: 24px; height: 24px; }

.wrap {
  max-width: 900px; margin: 0 auto;
  padding: 16px calc(14px + var(--safe-left)) 24px calc(14px + var(--safe-right));
}

h2 { font-size: 16px; margin: 26px 0 10px; letter-spacing: -0.015em; }

/*
 * The "no margin on the first heading" reset, SCOPED.
 *
 * It used to be a bare `h2:first-child`, but :first-child is relative to the
 * PARENT — so every heading that opened a <section> silently lost its top
 * margin and sat jammed against the block above it. That was 18 headings
 * across four pages, including Settings' "Club" and "Activity log" and the
 * Games page's confirmation queue; it just wasn't noticed until the dashboard
 * gained two more.
 *
 * Now it only applies where a heading genuinely sits at the top of its
 * container and needs no separation.
 */
.page > h2:first-child,
.wrap > h2:first-child,
.card > h2:first-child { margin-top: 0; }

/*
 * A heading that opens a section gets a little MORE room than a run-of-the-mill
 * one: a section is a larger visual unit than a paragraph-level heading, and
 * the extra few pixels are what stop consecutive sections reading as one long
 * list.
 *
 * .rel is excluded because release-notes entries set their own heading margin
 * and are already spaced 34px apart by .rel itself. Written as :not() rather
 * than relying on source order, because `section > h2:first-child` is more
 * specific than `.rel h2` and would win no matter which file loaded last.
 */
section:not(.rel) > h2:first-child { margin-top: 32px; }

/* ── Cards ─────────────────────────────────────────────────── */
/* Not frosted: cards are the content plane, not floating above it. They get
   depth from a gradient fill, a shadow, and a highlight on the top edge. */
.card {
  background: var(--surface-grad);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md), inset 0 1px 0 var(--edge-light);
  overflow: hidden;
}

/* ── Generic list row ──────────────────────────────────────────
   The one row primitive every list in the app uses. The project this
   template came from had three near-identical variants (standings, games,
   activity) that drifted apart; one primitive plus a modifier class is
   cheaper to keep honest. */
.row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  min-height: var(--tap);
  border-bottom: 1px solid var(--hairline);
}
.row:last-child { border-bottom: none; }
.row .muted { margin-left: auto; }
a.row-link { color: inherit; text-decoration: none; cursor: pointer; }
a.row-link:active { background: var(--press); }

.dot {
  width: 32px; height: 32px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  font-size: 12px; font-weight: 700; color: var(--on-accent);
  box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, .3);
}

/* Table layout only once there is room for it. Phones get .row cards; the
   table is the SAME data, not a hidden subset of it. Never solve a narrow
   screen by hiding columns — that is v1's mistake, and it is why this
   stylesheet is mobile-first. */
table { display: none; }

/* ── Controls ──────────────────────────────────────────────── */
.filters { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
select, input, textarea, button, .btn {
  font: inherit; color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm); padding: 10px 12px;
  min-height: var(--tap);
  box-shadow: var(--shadow-sm), inset 0 1px 0 var(--edge-light);
  transition: background .16s var(--ease), border-color .16s var(--ease),
              box-shadow .16s var(--ease), transform .12s var(--ease);
}
select, button, .btn { cursor: pointer; }
/* .btn is used on anchors as well as buttons. An inline <a> ignores
   min-height and lets its vertical padding bleed over the elements
   around it, so it has to be laid out as a flex box explicitly. */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  box-sizing: border-box; text-decoration: none; text-align: center;
  font-weight: 500;
}
button:hover:not(:disabled), .btn:hover { border-color: var(--muted); }
/* A press that visibly gives way is most of what makes a flat surface feel
   like a button. Shadow collapses at the same time as the scale. */
button:active:not(:disabled), .btn:active {
  transform: scale(.975);
  box-shadow: inset 0 1px 3px var(--press);
}
button:disabled, .btn:disabled {
  opacity: .5; cursor: default; box-shadow: none;
}
select:focus-visible, input:focus-visible, textarea:focus-visible,
button:focus-visible, .btn:focus-visible {
  outline: 2px solid var(--primary); outline-offset: 2px;
}
input:focus, textarea:focus, select:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--tint);
}

.btn-primary {
  background: linear-gradient(180deg, var(--primary-soft), var(--primary));
  border-color: transparent;
  color: var(--on-primary); font-weight: 600;
  box-shadow: var(--shadow-key), inset 0 1px 0 var(--edge-light);
}
.btn-primary:hover { border-color: transparent; filter: brightness(1.05); }
.btn-primary:disabled { filter: none; }

.empty, .status { padding: 28px 16px; text-align: center; color: var(--muted); }
.error, .success {
  border-radius: var(--radius);
  padding: 12px 14px; margin-bottom: 14px; font-size: 14px;
  box-shadow: var(--shadow-sm);
}
.error { background: var(--loss-bg); border: 1px solid var(--loss); color: var(--loss); }
.success { background: var(--win-bg); border: 1px solid var(--win); color: var(--win); }

[hidden] { display: none !important; }

/* ── Tablet and up ─────────────────────────────────────────── */
@media (min-width: 600px) {
  /* The pill floats at every width now. All that changes here is that it
     shrinks to its content instead of spanning the window: stretched across
     a wide screen it would look stranded. */
  .tabbar {
    left: 50%; right: auto; transform: translateX(-50%);
    bottom: calc(14px + var(--safe-bottom));
    width: auto;
    padding: 0 6px;
  }
  .tabbar a { padding: 0 18px; border-radius: 999px; }
  .tabbar a:active { transform: scale(.96); }
  .tabbar a:hover { color: var(--text); }
  .tabbar .record .fab { width: 42px; height: 42px; }

  .wrap { padding: 24px 20px 48px; }
  h2 { font-size: 18px; margin-top: 30px; }

  /* Standings become a real table, showing MORE than the phone view
     rather than the phone view being a truncation of this one. */
  .standing { display: none; }
  table { display: table; width: 100%; border-collapse: collapse; font-size: 15px; }
  th, td { padding: 12px; text-align: left; }
  th {
    font-size: 12px; text-transform: uppercase; letter-spacing: .04em;
    color: var(--muted); font-weight: 600; border-bottom: 1px solid var(--border);
  }
  td { border-bottom: 1px solid var(--hairline); }
  tbody tr { transition: background .15s var(--ease); }
  tbody tr:last-child td { border-bottom: none; }
  tbody tr:hover { background: var(--press); cursor: pointer; }
  .num { text-align: right; font-variant-numeric: tabular-nums; }
  .rank-col { width: 40px; color: var(--muted); font-variant-numeric: tabular-nums; }
  .player { display: flex; align-items: center; gap: 10px; }
  .peak { color: var(--muted); font-size: 12px; }
  .elo { font-weight: 600; font-variant-numeric: tabular-nums; }
}

@media (min-width: 900px) {
  .wrap { padding: 32px 24px 64px; }
}

/* Respect the OS setting rather than animating regardless. */
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
  button:active:not(:disabled), .btn:active, .tabbar a:active { transform: none; }
}

/* ── Segmented control ─────────────────────────────────────── */
/* Shared: the format picker on Record and Matchups, and the appearance
   picker in Settings all used a local copy of this. One definition, with
   the selected segment lifted out of the trough rather than just tinted. */
.segmented {
  display: flex; gap: 4px; padding: 4px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 13px;
  box-shadow: inset 0 1px 3px var(--press);
}
.segmented button {
  flex: 1; min-height: 38px; padding: 8px 4px;
  background: transparent; border: 1px solid transparent; border-radius: 10px;
  box-shadow: none;
  color: var(--muted); font-size: 13px; font-weight: 500;
}
.segmented button:hover:not([aria-selected='true']) { color: var(--text); border-color: transparent; }
.segmented button[aria-selected='true'] {
  background: var(--surface-grad);
  border-color: var(--border);
  color: var(--text); font-weight: 600;
  box-shadow: var(--shadow-sm), inset 0 1px 0 var(--edge-light);
}
.segmented button:active { transform: scale(.97); box-shadow: none; }

/* A dot holding a photo rather than initials. The img fills the circle and is
   centre-cropped, so a non-square source still renders cleanly. */
.dot.has-photo { background: var(--surface-2); overflow: hidden; padding: 0; }
.dot.has-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ── Pull to refresh ──────────────────────────────────────────────────────
   Only ever visible in an installed PWA; shell.js doesn't attach the gesture
   in a browser tab, where the native one already exists.

   Fixed rather than sticky, and above the content but below the tab bar, so
   a pull can't push the layout around — the indicator overlays, the page
   itself never moves. Cheaper too: transform and opacity only, both
   compositor-only properties, so dragging never triggers layout. */
.ptr {
  position: fixed;
  top: 0;
  left: 50%;
  z-index: 40;                      /* tab bar is 50 */
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  margin-left: -17px;
  opacity: 0;
  pointer-events: none;             /* never eats a tap */
  background: var(--glass);
  backdrop-filter: blur(12px);
  border: 1px solid var(--edge-light);
  border-radius: 50%;
  box-shadow: var(--shadow-md);
  transform: translateY(0);
}

.ptr-spinner {
  width: 16px;
  height: 16px;
  border: 2px solid var(--border);
  border-top-color: var(--primary);
  border-radius: 50%;
}

/* Past the commit threshold: the ring completes and colours, so you can feel
   where the gesture takes effect without watching a number. */
.ptr-ready .ptr-spinner {
  border-color: var(--primary);
  border-top-color: var(--primary);
}

.ptr-spinning .ptr-spinner {
  animation: ptr-spin .7s linear infinite;
}

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

@media (prefers-reduced-motion: reduce) {
  /* Don't remove the animation entirely — it's the only signal that a refresh
     is in flight. Slow it well below the flicker range instead. */
  .ptr-spinning .ptr-spinner { animation-duration: 2s; }
}

/* Divider and sub-heading inside a card, for the Restore block that sits under
   Export. A second <h2> there would read as a separate section in the page's
   outline when it's really one idea: back up, and put it back. */
.hr {
  height: 1px;
  margin: 16px 0 14px;
  background: var(--border);
  border: 0;
}

.sub {
  margin: 0 0 6px;
  font-size: 14px;
  font-weight: 650;
}

/* ── Update available banner ──────────────────────────────────────────────
   Sits above the tab bar rather than at the top: on a phone the top of the
   screen is under the notch and the least reachable part of the display, and
   this has a button that needs pressing. */
.update-bar {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: calc(var(--tabbar-height) + var(--safe-bottom) + 24px);
  z-index: 60;                  /* above the tab bar: it's an interruption */
  display: flex;
  gap: 10px;
  align-items: center;
  padding: 10px 12px;
  font-size: 13px;
  color: var(--text);
  background: var(--glass);
  backdrop-filter: blur(14px);
  border: 1px solid var(--edge-light);
  border-radius: 12px;
  box-shadow: var(--shadow-lg);
  animation: update-in .18s ease-out;
}

.update-bar span { flex: 1; min-width: 0; }

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

@media (prefers-reduced-motion: reduce) {
  .update-bar { animation: none; }
}

/* The ONE definition of .btn.small. Previously declared separately in
   games.css and settings.css at 38px, and not loaded at all by players.html —
   which used the class regardless, so those buttons silently rendered
   full-size. Keeping the 38px value the two page stylesheets had agreed on,
   since that's what most of the app was already showing.

   38px is also above the 32px I'd briefly set here for the update banner:
   these are real touch targets in the middle of a form, not chrome. */
.btn.small {
  min-height: 38px;
  padding: 8px 14px;
  width: auto;
  font-size: 14px;
}

/* A secondary button: transparent, muted, doesn't compete for attention.
   Moved here from settings.css because players.html uses it too and never
   loaded that file — so "Remove photo" on the profile had no quiet styling at
   all, which is what made "Change photo…" beside it look highlighted. The
   button that stood out was the one MISSING this class, not the ones with it. */
.btn.quiet {
  background: transparent;
  border-color: var(--border);
  color: var(--muted);
}
.btn.quiet:hover { color: var(--text); }
.btn.quiet:active { color: var(--text); background: var(--surface-2); }

/* The one place muted text is correct, so it stays meaningful. */
.btn:disabled,
.btn[aria-disabled='true'] {
  color: var(--muted);
  cursor: not-allowed;
  opacity: .6;
}

/* The update banner's button IS chrome and wants to be compact, so it asks for
   that explicitly instead of redefining what "small" means for the whole app. */
.update-bar .btn.small {
  min-height: 32px;
  padding: 4px 12px;
  font-size: 13px;
}

.update-x {
  width: 32px;                  /* 32px touch target for an 18px glyph */
  height: 32px;
  padding: 0;
  font-size: 18px;
  line-height: 1;
  color: var(--muted);
  background: none;
  border: 0;
  cursor: pointer;
}

@media (min-width: 600px) {
  .update-bar { left: auto; right: 16px; max-width: 380px; }
}

/* ── Bottom sheet ─────────────────────────────────────────────────────────
   Shared by the game detail sheet and the record-a-game player picker. Was
   duplicated verbatim in games.css and table.css, which is how the achievement
   detail ended up unstyled: it used these classes on two pages that load
   neither file. Anything shell.js injects has to be styled here. */
.sheet {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: flex-end;
}

.sheet-backdrop {
  position: absolute;
  inset: 0;
  background: var(--scrim);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
}

.sheet-body {
  position: relative;
  width: 100%;
  max-height: 80dvh;
  overflow-y: auto;
  background: var(--glass-2);
  backdrop-filter: saturate(180%) blur(26px);
  -webkit-backdrop-filter: saturate(180%) blur(26px);
  border-top-left-radius: 20px;
  border-top-right-radius: 20px;
  border-top: 1px solid var(--glass-border);
  box-shadow: var(--shadow-lg);
  padding: 14px 14px calc(14px + var(--safe-bottom));
}

.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}

/* Above phone width a sheet stops being a sheet and becomes a centred dialog:
   a full-width panel stuck to the bottom of a wide window looks like a mistake. */
@media (min-width: 600px) {
  .sheet { align-items: center; justify-content: center; }
  .sheet-body {
    max-width: 460px;
    border-radius: 18px;
    border: 1px solid var(--border);
  }
}

/* ── Stat tiles ───────────────────────────────────────────────────────────
   Used by the player profile AND the dashboard's "you" panel. Lived in
   player.css and worked on the dashboard only because dashboard.html happens
   to load that file; shared markup shouldn't rely on that coincidence. */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.stat {
  padding: 12px 10px;
  text-align: center;
  background: var(--surface-grad);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm), inset 0 1px 0 var(--edge-light);
}

/* The large avatar variant, used by the player profile, the dashboard's "you"
   panel and the Settings account card. It lived in player.css, which Settings
   doesn't load — so the class was inert there and the size only came out right
   because the JS also passes it inline. Defined here, the markup alone is
   enough. */
.dot.big {
  width: 56px;
  height: 56px;
  font-size: 20px;
  flex: none;
}

/* A destructive action. Muted until hovered so it doesn't shout from the page,
   but unmistakably red once you're on it — the danger is in the outcome, not
   in the button sitting there. */
.btn.danger { color: var(--loss); border-color: var(--border); background: transparent; }
.btn.danger:hover { background: var(--loss-bg); border-color: var(--loss); }

/* ── Utilities the pages rely on ──────────────────────────────────────────
   Kept at the bottom deliberately: these are the last things loaded, so a
   page-specific stylesheet can still override them without an !important.
   ------------------------------------------------------------------------ */

/* `.btn.primary` and `.btn-primary` are the same thing. Both spellings
   exist because both got written, in different files, months apart — which
   is exactly the drift a design system is supposed to prevent. Aliased
   rather than picked, because renaming one silently breaks whichever pages
   used the other. Prefer `.btn.primary` in new code. */
.btn.primary {
  background: linear-gradient(180deg, var(--primary-soft), var(--primary));
  border-color: transparent;
  color: var(--on-primary);
  font-weight: 600;
  box-shadow: var(--shadow-key);
}
.btn.primary:hover { filter: brightness(1.05); }
.btn.primary:disabled { filter: none; }

/* Full-width, for stacked calls to action on the sign-in and create pages,
   where a row of side-by-side buttons would be too narrow to hit. */
.btn.block { display: flex; width: 100%; justify-content: center; margin: 8px 0; }

/* Landing pages: no tab bar, so content is centred and the bottom padding
   the tab bar normally reserves is not needed. */
.wrap.narrow { max-width: 460px; }
body.landing .wrap { padding-bottom: 24px; }
body.landing { text-align: center; }
body.landing form { text-align: left; }

.mark { width: 72px; height: 72px; margin: 32px auto 8px; display: block; }
.tagline { color: var(--muted); margin: 0 0 24px; }
.hint { color: var(--muted); font-size: 12px; margin: 4px 0 16px; }
.slug-suffix { color: var(--muted); white-space: nowrap; }
.site-foot { margin: 32px 0 16px; color: var(--muted); font-size: 13px; }
.site-foot a { color: var(--muted); }

/* The "you" panel at the top of the dashboard. */
.me-head { display: flex; align-items: center; gap: 14px; margin: 20px 0 12px; }
.me-who { min-width: 0; text-align: left; }
.me-who h1 { margin: 0; font-size: 20px; }

/* A card that is purely a list of links (the More screen). */
.links { display: flex; flex-direction: column; }
.links a {
  padding: 14px;
  min-height: var(--tap);
  display: flex;
  align-items: center;
  border-bottom: 1px solid var(--hairline);
  color: inherit;
  text-decoration: none;
}
.links a:last-child { border-bottom: none; }
.links a:active { background: var(--press); }

/* ============================================================
   CORN CLUB — match and standings components.
   ------------------------------------------------------------
   Mobile first, like everything above: the base rules are the
   phone layout and the table only appears from 600px. Both the
   card list and the table are rendered by leaderboard.js with
   the SAME rows — the table is not a superset.
   ============================================================ */

.page-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: .75rem; margin-bottom: 1rem;
}
.page-head h1 { margin: 0; }
.section-head { font-size: .8rem; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); margin: 1.25rem 0 .5rem; }

/* ── Match cards ──────────────────────────────────────────────────────── */
.match-list { display: grid; gap: .6rem; }

.match-card {
  display: grid; gap: .35rem;
  padding: .85rem 1rem;
  min-height: 44px;
  border: 1px solid var(--border); border-radius: 14px;
  background: var(--surface-grad);
  box-shadow: var(--shadow-sm), inset 0 1px 0 var(--edge-light);
  color: inherit; text-decoration: none;
}
.match-card:active { background: var(--press); }

.match-teams { display: flex; align-items: baseline; gap: .5rem; flex-wrap: wrap; }
.match-teams .team { font-weight: 600; }
/* The winner is bolder AND coloured. Colour alone would be the only signal
   for a result, which fails for anyone who cannot distinguish it. */
.match-teams .team.won { color: var(--win); font-weight: 700; }
.match-teams .vs { color: var(--muted); font-size: .8rem; }

.match-score { display: flex; align-items: baseline; gap: .3rem; font-variant-numeric: tabular-nums; }
.match-score b { font-size: 1.25rem; }
.match-score .dash { color: var(--muted); }

.match-meta { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

.pill {
  display: inline-block; padding: .1rem .5rem; border-radius: 999px;
  font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
}
.pill.live { background: var(--tint-strong); color: var(--primary); }

/* ── Standings ────────────────────────────────────────────────────────── */
.standings-cards { display: grid; gap: .5rem; }

.standing-card {
  display: grid; grid-template-columns: 2rem 1fr auto; align-items: center; gap: .75rem;
  padding: .75rem 1rem; min-height: 44px;
  border: 1px solid var(--border); border-radius: 14px;
  background: var(--surface-grad);
  box-shadow: var(--shadow-sm), inset 0 1px 0 var(--edge-light);
}
.standing-rank { font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums; }
.standing-main { display: grid; }
.standing-name { font-weight: 600; text-decoration: none; color: inherit; }
.standing-record { color: var(--muted); font-size: .8rem; font-variant-numeric: tabular-nums; }
.standing-stats { display: flex; gap: .9rem; }
.standing-stat { display: grid; justify-items: end; font-variant-numeric: tabular-nums; }
.standing-stat b { font-size: .95rem; }
.standing-stat small { color: var(--muted); font-size: .65rem; text-transform: uppercase; }

.table-wrap { display: none; }
@media (min-width: 600px) {
  .standings-cards { display: none; }
  .table-wrap { display: block; }
}

.season-select { min-height: 44px; }

/* ── New match sheet ──────────────────────────────────────────────────── */
.sheet {
  border: none; border-radius: 18px 18px 0 0;
  padding: 1.25rem;
  width: 100%; max-width: 34rem;
  margin: auto auto 0;
  background: var(--surface); color: var(--text);
  /* Clears the home indicator on an installed iOS app. */
  padding-bottom: calc(1.25rem + env(safe-area-inset-bottom));
  box-shadow: var(--shadow-lg);
}
.sheet::backdrop { background: var(--scrim); }
@media (min-width: 600px) {
  .sheet { border-radius: 18px; margin: auto; }
}

.side-picker { display: grid; gap: 1rem; margin: 1rem 0; }
@media (min-width: 480px) { .side-picker { grid-template-columns: 1fr 1fr; } }
.side-picker h3 { font-size: .8rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); margin: 0 0 .4rem; }

.player-picker { display: flex; flex-wrap: wrap; gap: .4rem; }
.player-chip {
  display: inline-flex; align-items: center; gap: .4rem;
  min-height: 44px; padding: .35rem .7rem;
  border: 1px solid var(--border); border-radius: 999px;
  background: var(--surface-2); cursor: pointer;
}
.player-chip:has(input:checked) {
  background: var(--tint-strong); border-color: var(--primary); font-weight: 600;
}

.segmented { border: none; padding: 0; margin: 0 0 .75rem; display: flex; gap: .5rem; }
.segmented legend { font-size: .8rem; color: var(--muted); }
.segmented label { display: inline-flex; align-items: center; gap: .35rem; min-height: 44px; }

.check { display: flex; align-items: center; gap: .5rem; min-height: 44px; }

.sheet-actions { display: flex; justify-content: flex-end; gap: .5rem; padding: 0; margin: 1rem 0 0; }

/* ============================================================
   OAUTH PROVIDER BUTTONS — trademarked assets, not our design.
   ------------------------------------------------------------
   ⚠️ EVERY COLOUR BELOW IS HARDCODED ON PURPOSE. This is the one
   block in this stylesheet exempt from the "no colour outside the
   token block" rule, and it has to be: Google and Apple both
   publish exact values and prohibit altering them. Tokenising
   these would let a club's chosen accent colour tint someone
   else's trademark, which is both a brand violation and, for
   Apple, grounds for App Store rejection.

   The specs being followed:

   Google (Sign in with Google branding guidelines)
     - Light: #FFFFFF face, #747775 border, #1F1F1F label
     - Dark:  #131314 face, #8E918F border, #E3E3E3 label
     - The 4-colour "G" is never recoloured, never cropped, and
       never used without clear space around it.
     - Minimum height 40px; label is Roboto Medium where available.

   Apple (Human Interface Guidelines — Sign in with Apple)
     - Black, white, or white-with-outline. We use black on light
       backgrounds and white on dark, which is Apple's own guidance
       for maximum contrast.
     - Corner radius may be 0 to 50% of height. 8px here to sit
       with the rest of the UI.
     - The logo must be Apple's glyph, must not be replaced with
       an emoji or a lookalike, and the button must not be smaller
       or less prominent than any other sign-in option on screen.

   Both buttons are therefore the SAME height and width. Making
   Google the visually primary one — which is what the previous
   `.btn primary` did — is specifically what Apple prohibits.
   ============================================================ */

.oauth-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  width: 100%;
  /* 44px satisfies both Google's 40px floor and our own tap-target
     rule, so there is no conflict to resolve. */
  min-height: 44px;
  padding: 0 16px;
  margin-bottom: 10px;
  border-radius: 8px;
  border: 1px solid transparent;
  box-sizing: border-box;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: .01em;
  text-decoration: none;
  transition: filter .15s ease;
}
.oauth-btn:active { transform: scale(.985); }

.oauth-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
}
/* Fixed, so a long localised label can never squeeze the logo. */
.oauth-icon svg { display: block; }

.oauth-label {
  /* Both brands specify their own type. Fall back to the platform UI
     font rather than to a serif, which is what the page default is. */
  font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

/* ── Light theme ─────────────────────────────────────────────────────── */
.oauth-google {
  background: #ffffff;
  border-color: #747775;
  color: #1f1f1f;
}
.oauth-google:hover { border-color: #747775; filter: brightness(.97); }

.oauth-apple {
  background: #000000;
  border-color: #000000;
  color: #ffffff;
}
.oauth-apple:hover { border-color: #000000; filter: brightness(1.15); }
/* Optical correction: the Apple glyph reads small next to the G at the
   same box size, and sits slightly low because of the leaf. */
.oauth-apple .oauth-icon { margin-top: -2px; }

/* ── Dark theme ──────────────────────────────────────────────────────── */
/* Written out twice for the same reason the token block is: a media query
   and an explicit [data-theme] attribute, with no way in CSS to share the
   declarations. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .oauth-google {
    background: #131314;
    border-color: #8e918f;
    color: #e3e3e3;
  }
  :root:not([data-theme='light']) .oauth-google:hover { border-color: #8e918f; filter: brightness(1.25); }

  :root:not([data-theme='light']) .oauth-apple {
    background: #ffffff;
    border-color: #ffffff;
    color: #000000;
  }
  :root:not([data-theme='light']) .oauth-apple:hover { border-color: #ffffff; filter: brightness(.93); }
}

[data-theme='dark'] .oauth-google {
  background: #131314;
  border-color: #8e918f;
  color: #e3e3e3;
}
[data-theme='dark'] .oauth-google:hover { border-color: #8e918f; filter: brightness(1.25); }

[data-theme='dark'] .oauth-apple {
  background: #ffffff;
  border-color: #ffffff;
  color: #000000;
}
[data-theme='dark'] .oauth-apple:hover { border-color: #ffffff; filter: brightness(.93); }

/* Long names and emails wrapped rather than overflowing, and rows with
   several controls put the controls on their own line instead of crushing
   the name down to one letter. Both ported from Paddle Club (2026.08.05.1
   and .2), where the same starter markup produced the same two bugs. */
.row { flex-wrap: wrap; }
.row > span, .roster-name, .standing-name, .match-teams .team {
  min-width: 0;
  overflow-wrap: anywhere;
}
.row .controls { flex-basis: 100%; }

/* ── Account menu ─────────────────────────────────────────────────────── */
.topbar { position: relative; }

.account-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 8px;
  z-index: 40;
  min-width: 11rem;
  display: flex;
  flex-direction: column;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  box-shadow: var(--shadow-lg);
}
/* `hidden` loses to `display: flex` on specificity, so it has to be
   restated. This is the standard trap with a flex container that toggles. */
.account-menu[hidden] { display: none; }

.account-menu-item {
  display: flex;
  align-items: center;
  min-height: 44px;
  padding: 0 12px;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--text);
  font-size: 15px;
  text-align: left;
  cursor: pointer;
}
.account-menu-item:hover { background: var(--press); border-color: transparent; }
.account-menu-item.danger { color: var(--loss); }

/* The sign-up line on the landing page. Given real weight rather than left
   as muted small print: on an invite-only app the commonest first-time
   question is "am I supposed to have an account already?" */
.signup-hint {
  margin: 1rem 0 .35rem;
  padding-top: 1rem;
  border-top: 1px solid var(--hairline);
  font-size: 15px;
}
.signup-hint strong { margin-right: .35rem; }

/* A disabled provider button still has to look like the provider's button —
   dimming the whole thing would alter the logo's colours, which neither
   brand permits. Opacity on the label and a not-allowed cursor carry the
   state instead. */
.oauth-btn:disabled { cursor: not-allowed; }
.oauth-btn:disabled .oauth-label { opacity: .55; }

/* ============================================================
   RECORD SCREEN — scoring a live match.
   ------------------------------------------------------------
   Used standing up, outdoors, one-handed. The scoreboard is the
   largest element because "what's the score" is asked far more
   often than a round is entered, and every control clears 44px
   because the person tapping is not looking carefully.
   ============================================================ */
.record-wrap { padding-bottom: 6rem; }

.scoreboard {
  position: sticky; top: 0; z-index: 20;
  display: grid; grid-template-columns: 1fr auto 1fr; gap: .5rem; align-items: center;
  margin: 0 -16px 1rem; padding: 1rem 16px;
  background: var(--glass-2);
  /* Frosted rather than opaque so the round list scrolling underneath still
     reads as one page rather than two stacked panes. */
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--hairline);
}
.score-side { text-align: center; min-width: 0; }
.score-names {
  font-size: .8rem; color: var(--muted);
  overflow-wrap: anywhere; line-height: 1.25;
}
.score-value {
  font-size: 2.75rem; font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.score-side.winner .score-value { color: var(--win); }
.score-side.winner .score-names { color: var(--win); font-weight: 600; }
.score-mid { text-align: center; }
.score-round { font-size: .75rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
.score-target { font-size: .75rem; color: var(--muted); }

.bag-entry { display: grid; gap: .75rem; }
@media (min-width: 620px) { .bag-entry { grid-template-columns: 1fr 1fr; } }

.bag-side-head {
  font-size: .8rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); margin: 0 0 .4rem;
}
.bag-card {
  padding: .75rem;
  border: 1px solid var(--border); border-radius: 14px;
  background: var(--surface-grad);
  box-shadow: var(--shadow-sm), inset 0 1px 0 var(--edge-light);
  margin-bottom: .6rem;
}
.bag-card-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: .5rem; margin-bottom: .5rem;
}

.stepper {
  display: grid;
  grid-template-columns: 4.5rem 44px 2.5rem 44px 1fr;
  align-items: center; gap: .4rem;
  min-height: 44px;
}
.stepper-label { font-size: .9rem; }
.stepper-btn {
  width: 44px; height: 44px;
  border: 1px solid var(--border); border-radius: 10px;
  background: var(--surface-2);
  font-size: 1.25rem; line-height: 1;
  /* The one place in the app where a tap repeats quickly; removing the
     highlight delay matters more here than anywhere else. */
  touch-action: manipulation;
}
.stepper-btn:active { background: var(--press); }
.stepper-value {
  text-align: center; font-size: 1.1rem; font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.stepper-hint { font-size: .7rem; color: var(--muted); }

.round-preview {
  margin: .75rem 0; padding: .6rem .9rem;
  border-radius: 10px; background: var(--tint);
  font-weight: 600; text-align: center;
}

.record-actions { display: flex; gap: .5rem; margin: .75rem 0; }
.record-actions .btn { flex: 1; min-height: 48px; }
.record-footer { display: flex; gap: .5rem; margin-top: 1.5rem; }
.record-footer .btn { flex: 1; }

.round-list { display: grid; gap: .35rem; }
.round-row {
  display: grid; grid-template-columns: 2rem 1fr auto;
  align-items: center; gap: .6rem;
  padding: .5rem .75rem;
  border: 1px solid var(--hairline); border-radius: 10px;
}
.round-no { color: var(--muted); font-variant-numeric: tabular-nums; }
.round-total { font-variant-numeric: tabular-nums; font-weight: 600; }

.result-panel {
  margin-bottom: 1rem; padding: 1rem;
  border-radius: 14px; background: var(--win-bg);
  text-align: center;
}
.result-panel h2 { margin: 0 0 .25rem; }

/* ============================================================
   PLAYER PROFILE
   ============================================================ */
.player-head { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.25rem; }
.player-head h1 { margin: 0; }
.player-head p { margin: .2rem 0 0; }

.stat-grid {
  display: grid; gap: .6rem;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  margin-bottom: 1.5rem;
}
.stat-card {
  padding: .8rem; border-radius: 12px;
  background: var(--surface-2);
}
.stat-value { font-size: 1.5rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.stat-label { font-size: .8rem; color: var(--muted); }
.stat-hint { font-size: .7rem; color: var(--muted); margin-top: .15rem; }
.stat-card.good .stat-value { color: var(--win); }
.stat-card.bad  .stat-value { color: var(--loss); }

.chart-card { padding: .75rem; }
.rating-chart { width: 100%; height: auto; display: block; }

.partner-row { display: grid; grid-template-columns: 1fr auto auto; gap: .75rem; align-items: center; }
.partner-name { text-decoration: none; color: inherit; font-weight: 500; }
.partner-lift {
  font-variant-numeric: tabular-nums; font-weight: 700; min-width: 3rem; text-align: right;
}
.partner-lift.good { color: var(--win); }
.partner-lift.bad  { color: var(--loss); }
/* A thin sample is dimmed, not hidden: it is real, it just isn't evidence. */
.partner-row.thin { opacity: .6; }

.h2h-record { font-variant-numeric: tabular-nums; font-weight: 600; }
.h2h-record.good { color: var(--win); }
.h2h-record.bad  { color: var(--loss); }

.badge-grid { display: flex; flex-wrap: wrap; gap: .4rem; }
.badge {
  padding: .3rem .7rem; border-radius: 999px;
  background: var(--tint-strong); color: var(--primary);
  font-size: .8rem; font-weight: 600;
}

/* Settings additions */
.photo-row { align-items: center; gap: .9rem; }
.invite-actions { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .75rem; }
.btn.danger { color: var(--loss); border-color: var(--loss); }
.btn.danger:hover { background: var(--loss-bg); border-color: var(--loss); }
.success {
  padding: .6rem .9rem; border-radius: 10px;
  background: var(--win-bg); color: var(--win); font-weight: 500;
}

/* Transient confirmation. Sits above the tab bar rather than over it, and
   clears the home indicator on an installed app. */
.flash {
  position: fixed;
  left: 50%; transform: translate(-50%, 1rem);
  bottom: calc(5.5rem + env(safe-area-inset-bottom));
  z-index: 60;
  max-width: calc(100vw - 2rem);
  padding: .6rem 1.1rem;
  border-radius: 999px;
  background: var(--primary); color: var(--on-primary);
  font-weight: 600; font-size: .95rem;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  transition: opacity .2s ease, transform .2s ease;
  pointer-events: none;
}
.flash.in { opacity: 1; transform: translate(-50%, 0); }

/* Roster rows are links now, so they need the affordances of one. */
.member-row { text-decoration: none; color: inherit; min-height: 44px; }
.member-row:active { background: var(--press); }
.member-name { font-weight: 500; }

.match-details { margin-top: 1.5rem; }
.match-details summary {
  min-height: 44px; display: flex; align-items: center;
  cursor: pointer; font-weight: 500;
}
.match-details .card { margin-top: .5rem; }
.match-details input[type="text"],
.match-details input[type="date"],
.match-details input[type="number"] { width: 100%; }
