/* ==========================================================================
   South Side Cycles — design tokens
   Palette pulled from the club logo: deep navy, sun-bleached yellow,
   coral outline. Light theme: cream page, navy ink, yellow/coral accents.
   ========================================================================== */

:root {
  --navy:        #5271FF;
  --navy-soft:   #7189FF;
  --navy-mute:   #6E79B8;
  --text:        #1E1E1E;
  --cream:       #FBF6E9;
  --card:        #FFFFFF;
  --yellow:      #E1F558;
  --yellow-deep: #C7D93E;
  --coral:       #FF6F81;
  --coral-deep:  #FF4F68;
  --green:       #6FB98F;
  --line:        #EAE1C8;

  /* Strava's own brand orange. Not part of the club palette and not
     admin-overridable: it belongs to Strava and is only ever used to
     mark links and controls that lead there. --strava-deep is the
     darker shade for the connect button's drop shadow. */
  --strava:      #FC4C02;
  --strava-deep: #D63F02;

  --font-display: 'Baloo 2', 'Arial Rounded MT Bold', sans-serif;
  --font-body:    'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono:    'DM Mono', 'SFMono-Regular', monospace;

  --radius-lg: 20px;
  --radius-md: 14px;
  --radius-sm: 8px;
  --shadow-card: 0 2px 0 var(--line), 0 10px 24px -16px rgba(30, 40, 102, 0.35);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--cream);
  color: var(--text);
  font-family: var(--font-body);
  -webkit-font-smoothing: antialiased;
}

.site-footer {
  text-align: center;
  padding: 24px 18px 32px;
}

/* Source image is 365x37, so height drives the width: 18px renders it
   about 178px wide, down from ~237px at the old 24px. Small enough not
   to dominate the footer links above it, still comfortably legible.
   Margin is on top now that it sits last rather than first. */
.strava-badge {
  display: block;
  height: 18px;
  width: auto;
  margin: 16px auto 0;
}

.site-footer p {
  margin: 0;
  font-size: 12.5px;
  color: var(--navy-mute);
}

/* Link back to the club's main site when this install runs as a
   sub-site. Given its own line above the copyright and a touch more
   weight than the rest of the footer, since it's a real destination
   rather than the fine print around it. */
.site-footer-main-link {
  margin: 0 0 8px;
  font-size: 13.5px;
  font-weight: 600;
}

.site-footer-main-link a { color: var(--navy); }
.site-footer-main-link a:hover { color: var(--coral-deep); }

.site-footer a {
  color: var(--navy-mute);
  text-decoration: underline;
}

.site-footer a:hover { color: var(--text); }

a { color: inherit; }

img { max-width: 100%; display: block; }

/* --------------------------------------------------------------------
   Header / nav
   -------------------------------------------------------------------- */

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 18px 32px;
  background: var(--navy);
  position: relative;
}

.brand {
  display: flex;
  align-items: center;
  gap: 12px;
  text-decoration: none;
  /* Never let the header compress the brand: without this it shrinks as a
     flex item and the global `img { max-width: 100% }` then squashes the
     logo (or, with a max-height, shrinks it to something tiny). Instead the
     logo stays full-size and the nav collapses to the hamburger below
     1100px (see that media query) to make room. */
  flex-shrink: 0;
}

.brand-logo {
  height: 38px;
  width: auto;
  /* Belt-and-braces: if an unusually narrow viewport ever does cap the
     width, scale within the box keeping aspect ratio rather than distort. */
  object-fit: contain;
  display: block;
}

.main-nav {
  display: flex;
  align-items: center;
  gap: 2px;
}

.main-nav a {
  text-decoration: none;
  color: #C9C9EA;
  font-weight: 600;
  font-size: 14px;
  padding: 7px 11px;
  border-radius: 999px;
  /* Never let a label break mid-phrase (e.g. "Past Rides" onto two lines):
     flex items default to min-width:auto and will otherwise shrink to their
     longest word under any horizontal pressure. With nowrap the row stays
     one line and the mobile breakpoint handles genuinely-narrow widths. */
  white-space: nowrap;
  transition: background 0.15s ease, color 0.15s ease;
}

.main-nav a:hover { color: #fff; background: rgba(255,255,255,0.08); }

.main-nav a.active { color: var(--text); background: var(--yellow); }

/* Pill-style tab switcher. Deliberately never wraps its links onto a
   second row: a rounded pill with stacked rows inside it reads as
   broken rather than as a control. Links stay on one line and the
   toggle scrolls internally if the viewport is genuinely too narrow.
   flex-shrink: 0 stops a sibling flex item (the note sitting beside it
   on riders.php) from squeezing it; the whole toggle drops to its own
   line instead of being compressed. */
.period-toggle {
  display: inline-flex;
  flex-wrap: nowrap;
  flex-shrink: 0;
  max-width: 100%;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--cream);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 3px;
  gap: 2px;
}

.period-toggle a {
  padding: 7px 16px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
  flex-shrink: 0;
  white-space: nowrap;
}

/* Denser variant for the dashboard and teams tab strips, which have
   more items to fit. The no-wrap/scroll behaviour now comes from
   .period-toggle itself, so only the sizing differs here. */
.leaderboard-tabs a {
  padding: 6px 10px;
  font-size: 11.5px;
}

/* Row holding a right-aligned tab strip inside a card. The side inset
   tightens on narrow screens (see the 520px breakpoint) so a five-tab
   strip gets the handful of extra pixels it needs to fit, rather than
   the strip itself having to shrink or move. */
.tab-strip-row {
  display: flex;
  justify-content: flex-end;
  margin: 16px 22px;
}

.period-toggle a:hover { color: var(--text); }

.period-toggle a.active {
  background: var(--navy);
  color: var(--cream);
}

/* The sign-in button, in Strava's own orange rather than the club coral:
   this control leads to Strava and reads as theirs, the same reasoning
   that keeps --strava out of the admin-overridable palette. */
.btn-strava {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--strava);
  color: #fff;
  font-weight: 700;
  font-size: 14px;
  text-decoration: none;
  padding: 10px 18px;
  border-radius: 999px;
  box-shadow: 0 3px 0 var(--strava-deep);
  transition: transform 0.1s ease;
}

.btn-strava:hover { transform: translateY(-1px); }
.btn-strava:active { transform: translateY(1px); box-shadow: 0 1px 0 var(--strava-deep); }

/* Official "Connect with Strava" button image, used in place of the
   styled button above when an assets/strava-button.* asset is present
   (see strava_connect_button()). Height-constrained so whatever exact
   asset is dropped in sits at a sensible size. */
.btn-strava-official {
  display: inline-block;
  border: none;
  background: none;
  padding: 0;
  line-height: 0;
}

.btn-strava-official img {
  height: 40px;
  width: auto;
  display: block;
}

.btn-strava-official:hover { transform: translateY(-1px); }

/* Slightly smaller in the compact header row, where 40px would stand
   taller than the icons/avatar beside it and stretch the bar. */
.header-actions .btn-strava-official img { height: 34px; }

.rider-avatar-static {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  flex-shrink: 0;
  font-size: 11px;
}

.rider-avatar-wrap {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
}

.rider-avatar-admin-flag {
  position: absolute;
  bottom: -3px;
  right: -4px;
  width: 16px;
  height: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  line-height: 1;
  background: var(--yellow);
  border: 2px solid var(--navy);
  border-radius: 50%;
}

.rider-menu {
  position: relative;
}

.rider-menu-toggle {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 34px;
  height: 34px;
  background: rgba(255,255,255,0.08);
  border: none;
  border-radius: 8px;
  cursor: pointer;
  padding: 0;
}

.rider-menu-toggle:hover { background: rgba(255,255,255,0.16); }

.rider-menu-toggle span {
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 1px;
  background: var(--cream);
}

.rider-menu-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 180px;
  background: var(--card);
  border-radius: var(--radius-md);
  box-shadow: 0 10px 30px -8px rgba(27,27,78,0.45);
  padding: 8px;
  z-index: 50;
}

.rider-menu-dropdown.open { display: block; }

.rider-menu-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 13.5px;
  color: var(--text);
  padding: 8px 10px 10px;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 4px;
}

.role-badge {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text);
  background: var(--yellow);
  padding: 2px 8px;
  border-radius: 999px;
  white-space: nowrap;
  flex-shrink: 0;
}

/* Wraps a row of role badges as whole units - a badge that doesn't fit
   the current line moves entirely to the next one, rather than its
   text breaking mid-word inside the pill (the .role-badge white-space:
   nowrap above is what stops that). Used on riders.php and
   personal-bests.php wherever a rider can have several custom badges. */
.role-badge-row {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

/* Milestone badges on rider.php — deliberately distinct from the
   dark navy .kpi-card blocks (rider.php's own averages), since these
   celebrate milestones rather than report plain stats. */
.milestone-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px;
}

.milestone-badge {
  background: var(--cream);
  border: 1px solid var(--yellow-deep);
  border-radius: var(--radius-sm);
  padding: 14px 16px;
  display: flex;
  align-items: center;
  gap: 12px;
}

.milestone-badge-icon {
  font-size: 26px;
  flex-shrink: 0;
  line-height: 1;
}

.milestone-badge-label {
  font-size: 11px;
  font-weight: 700;
  color: var(--navy-mute);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-bottom: 2px;
}

.milestone-badge-value {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 15px;
  color: var(--text);
}

/* The milestone feed — index.php, milestones.php — styled like a social
   feed: each milestone is its own bordered "post" card, not just a thin
   list row, with a clear name+timestamp header and the achievement
   itself as the prominent line, not an afterthought crammed after a
   colon. */
.milestone-post {
  display: flex;
  gap: 14px;
  padding: 16px;
  background: var(--cream);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  margin-bottom: 12px;
}

.milestone-post:last-child { margin-bottom: 0; }

.milestone-post-avatar img {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  flex-shrink: 0;
}

.milestone-post-body { flex: 1; min-width: 0; }

.milestone-post-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 5px;
}

.milestone-post-name {
  font-weight: 700;
  font-size: 14.5px;
  color: var(--text);
  text-decoration: none;
}

.milestone-post-name:hover { color: var(--coral-deep); text-decoration: underline; }

.milestone-post-time {
  font-size: 12px;
  color: var(--navy-mute);
  white-space: nowrap;
  flex-shrink: 0;
}

.milestone-post-content {
  font-size: 13px;
  font-weight: 400;
  color: var(--text);
  line-height: 1.4;
}

.milestone-post-emoji {
  font-size: 13px;
  margin-right: 5px;
  vertical-align: middle;
}

.milestone-post-meta {
  margin-top: 5px;
  font-size: 12.5px;
  color: var(--navy-mute);
}

.role-badge.role-rider {
  background: var(--line);
  color: var(--navy-mute);
}

/* Gold/amber, deliberately evoking a cycling leader's jersey — kept
   visually distinct from both role-rider's muted gray and
   role-custom's light blue below. */
.role-badge.role-rideleader {
  background: #FFF3D6;
  color: #926C00;
}

/* Custom role tags (rider.php) — purely cosmetic, no permissions
   attached, so deliberately a different color from role-admin/role-rider
   rather than looking like a third "real" access level. */
.role-badge.role-custom {
  background: #E8ECFB;
  color: var(--navy);
}

.rider-menu-dropdown a {
  display: block;
  padding: 9px 10px;
  border-radius: 8px;
  color: var(--text);
  font-size: 13.5px;
  font-weight: 600;
  text-decoration: none;
}

.rider-menu-dropdown a:hover { background: var(--cream); }

.rider-menu-dropdown a.active { background: var(--yellow); color: var(--text); }

/* The account/utility links (Profile, My Rides, Settings, Contact, Sign out)
   carry an outline icon before the label; the nav and Admin links stay
   text-only. Icon inherits the link's currentColor, so it follows hover,
   .active, and the club theme.

   Selector is qualified as `.rider-menu-dropdown a.rider-menu-link-icon` on
   purpose: the base `.rider-menu-dropdown a` rule (specificity 0-1-1) sets
   `display: block`, which would otherwise beat a lone `.rider-menu-link-icon`
   (0-1-0) and keep these inline — leaving the icon baseline-aligned (sitting
   high) and ignoring `gap`. Matching the same ancestor+element wins the cascade. */
.rider-menu-dropdown a.rider-menu-link-icon {
  display: flex;
  align-items: center;
  gap: 10px;
}
.rider-menu-icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

/* Sign out becomes a disabled span (not a link) while a real admin is in any
   "view as" preview: the terminal action there is Exit preview, not signing
   out the real admin session, which mid-preview is a confusing thing to do.
   Mirrors the link's padding/type + icon layout, just greyed and inert. */
.rider-menu-dropdown span.rider-menu-link-disabled {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text);
  opacity: 0.4;
  cursor: not-allowed;
}

/* A labeled subsection within the account dropdown (e.g. "Admin" with
   just Site Admin in it, "Account" with Profile/Settings/Sign out) —
   its own divider and small uppercase label, same visual language as
   .rider-menu-name's divider above it, so each group reads as distinct
   rather than everything sitting flat in one undivided list. */
.rider-menu-subsection {
  border-top: 1px solid var(--line);
  margin-top: 4px;
  padding-top: 4px;
}

.rider-menu-section-label {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--navy-mute);
  padding: 6px 10px 2px;
}

.rider-menu-nav-section {
  display: none;
}

/* Admin-only notification bell, same toggle/dropdown pattern as
   .rider-menu right next to it. */
/* Right-hand header cluster: the public weather link plus the account
   controls (bell/avatar/menu, or the Connect button), kept together as
   one flex item so the header's space-between keeps nav centred. */
.header-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Weather link, styled to match the admin bell toggle beside it: a
   cream currentColor icon on a faint tile. Colour is set explicitly
   because the SVG inherits currentColor and would otherwise pick up the
   dark body text and vanish against the navy header. */
.header-weather-link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  background: rgba(255,255,255,0.08);
  border-radius: 8px;
  color: var(--cream);
  text-decoration: none;
  flex-shrink: 0;
}

.header-weather-link:hover { background: rgba(255,255,255,0.16); }

/* Current-page state, matching how .main-nav a.active reads: the coral
   accent, so it's clear you're on the weather page. */
.header-weather-link.active {
  background: var(--coral);
  color: #fff;
}

.header-weather-icon {
  width: 19px;
  height: 19px;
  display: block;
}

.notification-bell {
  position: relative;
}

/* Colour has to be set explicitly now the bell is a currentColor SVG
   rather than a colour emoji glyph: without it the icon would inherit
   the dark body text colour and vanish against the navy header. Matches
   the cream used by the hamburger toggles either side of it. */
.notification-bell-toggle {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  background: rgba(255,255,255,0.08);
  border: none;
  border-radius: 8px;
  cursor: pointer;
  padding: 0;
  color: var(--cream);
}

.notification-bell-toggle:hover { background: rgba(255,255,255,0.16); }

.notification-bell-icon {
  width: 18px;
  height: 18px;
  display: block;
}

.notification-bell-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  color: #fff;
  background: var(--coral-deep);
  border: 2px solid var(--navy);
  border-radius: 999px;
  line-height: 1;
}

.notification-bell-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  width: min(300px, calc(100vw - 32px));
  max-height: 360px;
  overflow-y: auto;
  background: var(--card);
  border-radius: var(--radius-md);
  box-shadow: 0 10px 30px -8px rgba(27,27,78,0.45);
  padding: 8px;
  z-index: 50;
}

.notification-bell-dropdown.open { display: block; }

.notification-bell-header {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 13.5px;
  color: var(--text);
  padding: 8px 10px 10px;
}

.notification-bell-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px;
  border-radius: 8px;
  text-decoration: none;
  color: var(--text);
  font-size: 13px;
  line-height: 1.4;
}

.notification-bell-item:hover { background: var(--cream); }

.notification-bell-icon {
  flex-shrink: 0;
  font-size: 15px;
}

.notification-bell-empty {
  padding: 16px 10px;
  font-size: 13px;
  color: var(--navy-mute);
  text-align: center;
}

/* --------------------------------------------------------------------
   Page shell
   -------------------------------------------------------------------- */

.page {
  max-width: 1120px;
  margin: 0 auto;
  padding: 40px 32px 80px;
}

/* Set while an in-place navigation is fetching (see the script at the
   foot of riders.php). Dims just enough to show something is happening
   and blocks further clicks so a fast double-tap can't queue two swaps.
   The class goes with the element it's on, since the whole <main> is
   replaced on success, so there's nothing to clean up. */
.page.is-swapping {
  opacity: 0.55;
  pointer-events: none;
  transition: opacity 0.12s ease;
}

.page-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 28px;
  flex-wrap: wrap;
  background: var(--card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  padding: 26px 28px;
}

.page-heading h1 {
  font-family: var(--font-display);
  font-size: 30px;
  margin: 0;
}

.page-heading p {
  margin: 4px 0 0;
  color: var(--navy-mute);
  font-size: 15px;
}

/* Lightweight variant of .page-heading for a mid-page section divider
   (e.g. "Past Challenges", "Archived Teams") rather than the page's own
   top-level title — same layout, no card treatment, since it's meant to
   sit as a plain divider between two groups of cards rather than read
   as a boxed section of its own. */
.section-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 28px;
  flex-wrap: wrap;
}

.page-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* --------------------------------------------------------------------
   KPI strip — "cyclocomputer" readouts
   -------------------------------------------------------------------- */

.kpi-strip {
  display: grid;
  grid-template-columns: repeat(6, minmax(0, 1fr));
  gap: 12px;
  margin-bottom: 36px;
}

.kpi-card {
  background: var(--navy);
  border-radius: var(--radius-lg);
  padding: 16px 14px;
  color: var(--cream);
  position: relative;
  overflow: hidden;
}

.kpi-card::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, rgba(245,214,103,0.10), transparent 55%);
  pointer-events: none;
}

/* Sized to keep the longest label on one line now that six KPIs share
   the row. Uppercase plus letter-spacing widens these more than the
   font size alone suggests, so the tracking is eased off slightly too. */
.kpi-label {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--yellow);
  margin-bottom: 8px;
  line-height: 1.3;
}

.kpi-value {
  font-family: var(--font-mono);
  font-size: clamp(19px, 2.6vw, 30px);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  white-space: nowrap;
}

.kpi-unit {
  font-size: 14px;
  color: #C9C9EA;
  margin-left: 4px;
}

/* .kpi-label/.kpi-value default to colors meant for the dark .kpi-card
   background (yellow label, light value) — this page reuses them on a
   plain white card, so both need overriding for readable contrast. */
.sync-rider-count .kpi-label { color: var(--navy-mute); }
.sync-rider-count .kpi-value { color: var(--text); }

/* "Fun facts" block on rider.php: each distance/elevation landmark
   comparison plus its next-milestone progress bar, reusing the
   .progress-* bar styles below. */
.fun-facts {
  margin-top: 18px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.fun-fact-headline {
  font-size: 13.5px;
  color: var(--navy-mute);
  margin-bottom: 2px;
}

/* The "(3,220 of 6,650 km)" readout on the next-milestone line —
   de-bolded so the landmark name stays the label and the numbers read
   as secondary detail. */
.fun-fact-next-nums {
  font-weight: 400;
}

.progress-row-label {
  display: flex;
  justify-content: space-between;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--navy-mute);
  margin-bottom: 6px;
}

.progress-track {
  background: var(--cream);
  border: 1px solid var(--line);
  border-radius: 999px;
  height: 10px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  background: var(--navy);
  border-radius: 999px;
  transition: width 0.2s ease;
}

.progress-fill-alt {
  background: var(--coral);
}

.progress-fill-warn {
  background: var(--yellow-deep);
}

/* A finished challenge bar on the dashboard. Green rather than the
   default navy so "done" is obvious at a glance without reading the
   number, matching the green already used for success alerts. */
.progress-fill-done {
  background: var(--green);
}

/* --------------------------------------------------------------------
   Elevation-line divider — signature element
   -------------------------------------------------------------------- */

/* Topographic contour divider on the dashboard, echoing the club
   jersey's contour-map print. Coral picks up the jersey's own line
   colour against the cream page, and because it's the themed token
   rather than a fixed hex, a club that re-themes the site gets a
   matching divider without touching this.

   Taller than the single line it replaced, since six stacked contours
   need the room to read as separate lines rather than a smudge. */
/* Elevation profile with a rider on it, dividing the KPI strip from the
   dashboard cards. Coral rather than the club yellow it used to be, to
   pick up the jersey's line colour; still a themed token, so a club that
   re-themes the site gets a matching divider. */
.elevation-divider {
  position: relative;
  height: 46px;
  margin: 8px 0 32px;
  color: var(--coral);
}

.elevation-divider-line {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 28px;
}

/* Parked on the summit at x=300 in the line's 1100-unit viewBox.
   Horizontal is a plain percentage (300/1100). Vertical works out
   because only the width stretches: that summit sits at y=6 of 40
   viewBox units, which across a fixed 28px-tall line is always
   6/40 x 28 = 4.2px down, so 23.8px up from the bottom. The bike's own
   viewBox ends exactly at the bottom of its wheels, so that offset puts
   the tyres on the line rather than near it. */
.elevation-divider-bike {
  position: absolute;
  left: 27.3%;
  bottom: 23.8px;
  width: 30px;
  height: 19.2px;
  transform: translateX(-50%);
}

/* --------------------------------------------------------------------
   Dashboard grid: leaderboard + recent rides
   -------------------------------------------------------------------- */

.dashboard-grid {
  display: grid;
  grid-template-columns: 1.35fr 1fr;
  gap: 20px;
  align-items: stretch;
}

/* Each of .dashboard-grid's two columns, flowing independently (a tall
   card in one column never row-locks a shorter card in the other, the
   way explicit CSS Grid row placement would) — on desktop just a plain
   flex column. Below 860px this switches to display:contents (see that
   breakpoint), which un-boxes it entirely so its own card children
   become direct flex items of .dashboard-grid instead, letting each one
   carry its own `order` for a mobile-only sequence independent of which
   desktop column it lives in. */
.dashboard-col {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.card {
  background: var(--card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

/* A card's "see all of this" link, sitting on its own row after the
   card's content rather than beside the heading. In the header it
   shares a flex row with the title and wraps onto a second line on a
   phone; down here it stays on one line at any width.

   The top border deliberately mirrors .card-header's bottom one: on its
   own the link floated in whitespace and read as lost, where the header
   version at least had a defined band around it. Bookending the card
   gives it back that structure without inflating a navigation link into
   a button. */
.card-footer-link {
  display: flex;
  justify-content: flex-end;
  padding: 14px 22px;
  border-top: 1px solid var(--line);
}

.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 22px 14px;
  border-bottom: 1px solid var(--line);
}

.card-header h2 {
  font-family: var(--font-display);
  font-size: 18px;
  margin: 0;
}

/* Collapsible groupings on admin/admin.php — native <details>/<summary>,
   so expand/collapse works with zero JS and stays keyboard/screen-reader
   accessible for free. Each section's own cards sit inside unchanged;
   this only wraps them, it doesn't restyle them. */
.admin-section {
  margin-bottom: 20px;
}

.admin-section-title {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  list-style: none;
  padding: 14px 20px;
  background: var(--navy);
  border: 1px solid var(--navy);
  border-radius: var(--radius-lg);
  font-family: var(--font-display);
  font-size: 16px;
  font-weight: 700;
  color: #fff;
  user-select: none;
}

.admin-section-title::-webkit-details-marker { display: none; }

.admin-section-title .admin-section-arrow {
  font-size: 11px;
  color: #fff;
  transition: transform 0.15s ease;
  flex-shrink: 0;
}

.admin-section[open] > .admin-section-title .admin-section-arrow {
  transform: rotate(90deg);
}

.admin-section-body {
  padding-top: 20px;
}

/* Lightweight collapsible for nested use inside a card (e.g. stats.php's
   condition-comparison tables) — same <details>/<summary> mechanism as
   .admin-section above, but sized and coloured to sit quietly inside a
   card's own padding rather than as a bold top-level section header. */
.compare-group {
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  margin-bottom: 10px;
  overflow: hidden;
}

.compare-group summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 14px;
  background: var(--cream);
}

.compare-group summary::-webkit-details-marker { display: none; }

.compare-group-arrow {
  font-size: 10px;
  color: var(--navy-mute);
  transition: transform 0.15s ease;
  flex-shrink: 0;
}

.compare-group[open] > summary .compare-group-arrow {
  transform: rotate(90deg);
}

.compare-group-body {
  padding: 0 14px 14px;
}

/* Flags a tool's explainer text as carrying real risk of data loss or
   harm (deletes something permanently, can't be undone, etc.) — reuses
   the same red already used for .btn-danger/.alert-error rather than a
   new color, so "red" means the same thing everywhere on the site.
   !important because this is always combined with .form-hint (whose own
   color: var(--navy-mute) is declared later in this file and would
   otherwise win the cascade tie and silently grey the warning back out). */
.tool-warning {
  color: var(--coral-deep) !important;
  font-weight: 700;
}

/* admin.php's tool cards reuse .compare-group for its compact sizing,
   but should read as white cards (matching .card everywhere else on the
   site), not the cream stats.php uses for its nested condition-comparison
   tables — scoped to .admin-section-body so stats.php's own usage is
   untouched. */
.admin-section-body .compare-group summary {
  background: var(--card);
}

.admin-section-body .compare-group-body {
  background: var(--card);
  padding-top: 14px;
}

.subtle-link {
  font-size: 13px;
  font-weight: 600;
  color: var(--coral-deep);
  text-decoration: none;
}

/* Leaderboard table */

.leaderboard { width: 100%; border-collapse: collapse; }

.leaderboard th {
  text-align: left;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--navy-mute);
  font-weight: 700;
  padding: 10px 22px;
  border-bottom: 1px solid var(--line);
}

/* Sortable column headers (riders.php, personal-bests.php) — the link
   inherits the th's own text styling rather than looking like a normal
   inline link, so a sortable header reads the same as a plain one
   until you notice the arrow or hover it. */
.leaderboard th a.sortable-col {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: inherit;
  text-decoration: none;
  font: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
}

.leaderboard th a.sortable-col:hover { color: var(--text); }

.leaderboard th a.sortable-col.active { color: var(--text); }

.leaderboard th a.sortable-col .sort-arrow { font-size: 9px; }

.leaderboard td {
  padding: 12px 22px;
  border-bottom: 1px solid var(--line);
  font-size: 14px;
}

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

/* ---- My Performance (my-performance.php) --------------------------------
   The per-metric trend page: a year headline, the shared bar-chart, and a
   month-by-month table below it. Numeric columns right-align, and the peak
   month is tinted the same way the chart labels it. */
.myperf-summary {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 20px 0 4px;
}
.myperf-summary-label {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--navy-mute);
  font-weight: 700;
}
.myperf-summary-value {
  font-family: var(--font-mono);
  font-size: 28px;
  font-weight: 500;
  color: var(--navy);
}
.myperf-summary-club {
  font-size: 13px;
  color: var(--navy-mute);
  margin-top: 2px;
}
.myperf-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  margin-top: 16px;
  font-size: 13px;
  color: var(--navy-mute);
}
.myperf-legend-item { display: inline-flex; align-items: center; gap: 7px; }
.myperf-swatch { display: inline-block; }
.myperf-swatch-you { width: 12px; height: 12px; border-radius: 2px; background: var(--navy); }
.myperf-swatch-club { width: 16px; height: 3px; border-radius: 2px; background: var(--coral); }
.myperf-table th.num,
.myperf-table td.num { text-align: right; }
.myperf-table td.num { font-family: var(--font-mono); }
.myperf-th-unit { display: block; font-size: 10px; font-weight: 500; text-transform: none; letter-spacing: 0; color: var(--navy-mute); }
.myperf-club-cell { color: var(--navy-mute); }
.myperf-peak-row td { background: color-mix(in srgb, var(--yellow) 22%, transparent); }

/* ---- Expandable participant rows (ride.php) -----------------------------
   The participant table carries nine columns; on a phone the low-priority
   ones (.col-extra) are hidden, and this reveals them in a single
   tap-to-expand detail row rather than a horizontal scroll. The heroes
   (Distance, Avg speed) are guaranteed visible: the rider NAME never
   truncates — the avatar shrinks, then drops, before the name gives an inch.
   Desktop is untouched; all the mobile behaviour lives in the media query. */

/* The unit suffix on a stat, e.g. the "km/h" in "28.5 km/h" — smaller and
   muted so the number reads first. In an expandable table it drops to its own
   line under the number, on every screen, so a stat reads number-over-unit
   identically everywhere rather than autowrapping by column width (a narrow
   cell would otherwise break "km" while a wide one kept "bpm" inline). */
.stat-unit { font-size: 0.72em; font-weight: 400; color: var(--navy-mute); }
.leaderboard-expandable .stat-unit { display: block; }

/* Avatar in a participant row, previously an inline style repeated per row. */
.rider-cell-avatar {
  width: 32px; height: 32px; border-radius: 50%;
  flex-shrink: 0; display: block;
}

/* All three are desktop-inert: the columns show inline, so there's nothing
   to expand and no control to show. */
.rider-detail { display: none; }
.col-toggle { display: none; }
.row-expand-toggle { display: none; }

.rider-detail-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 26px;
  padding: 4px 2px 6px;
}
.rd-item { display: flex; flex-direction: column; gap: 2px; }
.rd-label {
  font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.05em; color: var(--navy-mute);
}
.rd-value { font-size: 14px; }
.rd-item-actions { flex-basis: 100%; }
.rider-actions { display: flex; align-items: center; gap: 6px; }

/* my-rides.php: one card per ride, focused on club-ride matching (not stats —
   those live on rider.php). Each card is identity up top, a divider, then the
   match status and its self-service action. Spacious on purpose. */
.myride-list { display: flex; flex-direction: column; gap: 14px; }
.myride-card { padding: 18px 22px; }
.myride-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 6px 16px;
  flex-wrap: wrap;
}
.myride-title { font-size: 15px; }
.myride-meta {
  font-size: 12.5px;
  color: var(--navy-mute);
  font-family: var(--font-mono);
  white-space: nowrap;
}
.myride-divider { border-top: 1px solid var(--line); margin: 13px 0; }
.myride-status {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 10px 16px;
  flex-wrap: wrap;
  font-size: 14px;
}
.myride-status-text { line-height: 1.5; }
.myride-status-muted { color: var(--navy-mute); }

/* The self-service actions are clear buttons now there's room in the card.
   Semantic colour signals direction (green = add me back, red = take me off,
   neutral = cancel), deliberately fixed rather than the themeable brand
   colour so a re-theme can't break the go/stop meaning. Text is a deep
   green/red so it stays readable on the light tint. */
.myride-action { margin: 0; }
.myride-btn {
  font: inherit;
  font-weight: 600;
  font-size: 13.5px;
  padding: 8px 16px;
  border-radius: 8px;
  border: 1px solid transparent;
  cursor: pointer;
  white-space: nowrap;
}
.myride-btn-go { background: #E8F5EE; color: #2E7D52; border-color: #6FB98F; }
.myride-btn-go:hover { background: #D8EEE1; }
.myride-btn-stop { background: #FDECEE; color: #B02A37; border-color: #E39BA3; }
.myride-btn-stop:hover { background: #F8DBDF; }
.myride-btn-neutral { background: transparent; color: var(--text); border-color: var(--line); }
.myride-btn-neutral:hover { background: var(--cream); }

/* Inline two-step confirm for "I wasn't there": "Are you sure?" plus a
   Yes/Cancel pair. :not([hidden]) so the CSS display value doesn't override
   the `hidden` attribute (a plain author `display` beats `hidden`, which
   would otherwise leave it always visible). */
.myride-confirm:not([hidden]) { display: inline-flex; align-items: center; gap: 8px; }
.myride-confirm-q { font-size: 13.5px; font-weight: 600; white-space: nowrap; }
.myride-confirm-actions { display: inline-flex; gap: 8px; }

/* On a phone, stack the action under the status text and let it fill the
   width for an easy tap target; the question sits on its own line with the
   Yes/Cancel pair splitting the width below it. */
@media (max-width: 640px) {
  .myride-status { flex-direction: column; align-items: stretch; }
  .myride-action > .myride-btn { width: 100%; }
  .myride-confirm:not([hidden]) { display: flex; flex-direction: column; align-items: stretch; }
  .myride-confirm-actions { display: flex; }
  .myride-confirm-actions .myride-btn { flex: 1; }
}

/* my-rides "waiting on an admin" reassurance card: a club-yellow left edge to
   read as a pending/heads-up notice rather than an error. */
.notice-card { border-left: 4px solid var(--yellow); }
.notice-list { margin: 0; padding-left: 18px; font-size: 14px; }
.notice-list li { margin: 3px 0; }


@media (max-width: 860px) and (orientation: portrait) {
  /* Appears exactly when .col-extra columns are hidden (same query), so the
     chevron is only ever shown when there's something to expand. Kept as tight
     as possible — it's a whole column competing with the heroes for width. */
  .col-toggle { display: table-cell; width: 1%; text-align: right; padding-left: 0 !important; padding-right: 2px !important; }
  .leaderboard-expandable .row-expand-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; padding: 0;
    background: none; border: none; cursor: pointer;
    color: var(--navy-mute);
  }
  .row-expand-chevron { transition: transform 0.15s ease; font-size: 15px; line-height: 1; }
  .rider-row[aria-expanded="true"] .row-expand-chevron { transform: rotate(180deg); }

  .rider-detail:not([hidden]) { display: table-row; }
  .rider-detail > td { padding-top: 4px; }

  /* Tighten the hero row so three columns plus the toggle fit a phone without
     the last one clipping — the 22px cell padding was the real space thief.
     6px keeps the table within the content width down to ~360px screens. */
  .leaderboard-expandable th,
  .leaderboard-expandable td { padding: 9px 6px; }

  /* Name wins, avatar yields: wrap the name to a second line rather than
     ever truncating it, and shrink the avatar to buy the room. */
  .leaderboard-expandable .rider-name { white-space: normal; }
  .leaderboard-expandable .rider-cell-avatar { width: 26px; height: 26px; }

  .rider-row { cursor: pointer; }
}

/* Really no room: drop the avatar entirely rather than squeeze the name. */
@media (max-width: 380px) and (orientation: portrait) {
  .leaderboard-expandable .rider-cell-avatar { display: none; }
}

/* Head-to-head table (compare.php). Three columns: a label and two
   riders. The two value columns are centred and equal-width so the
   numbers line up as a genuine side-by-side rather than drifting apart
   as the label column grows. */
.compare-table th:not(:first-child),
.compare-table td:not(:first-child) {
  text-align: center;
  width: 34%;
}

.compare-table td:first-child {
  color: var(--navy-mute);
  font-size: 13px;
}

/* Section headers inside the table (Totals / Averages / Personal bests),
   reusing the same treatment as stats.php's pace-group rows. */
.compare-group-row td {
  background: var(--cream);
  font-family: 'Baloo 2', sans-serif;
  font-weight: 600;
  font-size: 13px;
  color: var(--navy);
}

/* The winning side of a row. Deliberately subtle: this is a club stats
   page, not a scoreboard, and only rows where "more" is genuinely an
   achievement ever get this class (see $comparisonGroups' 'better'). */
.compare-winner {
  font-weight: 700;
  color: var(--navy);
}

.compare-winner::after {
  content: ' ●';
  color: var(--coral);
  font-size: 10px;
  vertical-align: middle;
}

.pace-group-row td {
  background: var(--cream);
  color: var(--text);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 12.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 9px 22px;
}

.rider-cell {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* Team roster (teams.php) - a stack of full-width rows instead of a
   plain table, since a spreadsheet layout undersells what's meant to
   feel like a squad, not a report. */
.team-fill-bar-track {
  background: var(--line);
  border-radius: 999px;
  height: 8px;
  overflow: hidden;
  margin-bottom: 22px;
}

.team-fill-bar-fill {
  background: linear-gradient(90deg, var(--navy), var(--navy-soft));
  height: 100%;
  border-radius: 999px;
  transition: width 0.3s ease;
}

.team-roster-grid {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.team-member-card {
  position: relative;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 12px 18px;
  display: flex;
  align-items: center;
  gap: 14px;
  text-align: left;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.team-member-card:hover {
  transform: translateX(3px);
  box-shadow: 0 8px 20px rgba(30, 30, 30, 0.08);
}

.team-member-card--captain {
  background: linear-gradient(100deg, #FFF7DC 0%, var(--card) 55%);
  border-color: var(--yellow-deep);
}

.team-member-card--empty {
  border-style: dashed;
  background: transparent;
  opacity: 0.55;
}

.team-member-card--empty:hover {
  transform: none;
  box-shadow: none;
}

.team-member-number {
  font-size: 10.5px;
  font-weight: 700;
  color: var(--navy-mute);
  flex-shrink: 0;
  width: 22px;
}

.team-member-crown {
  font-size: 15px;
  flex-shrink: 0;
}

.team-member-card img {
  width: 44px;
  height: 44px;
  font-size: 15px;
  display: flex;
  flex-shrink: 0;
  border: 3px solid var(--card);
  box-shadow: 0 0 0 2px var(--line);
}

.team-member-card--captain img {
  box-shadow: 0 0 0 2px var(--yellow-deep);
}

.team-member-card img { display: block; object-fit: cover; }

.team-member-info {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 2px 12px;
}

.team-member-name {
  font-weight: 700;
  font-size: 14px;
  color: var(--text);
  line-height: 1.3;
}

.team-member-name a { color: inherit; text-decoration: none; }
.team-member-name a:hover { text-decoration: underline; }

.team-member-role {
  font-size: 10.5px;
  font-weight: 700;
  color: var(--yellow-deep);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-left: auto;
}

.team-member-empty-icon {
  font-size: 18px;
  color: var(--line);
  flex-shrink: 0;
}

.team-member-empty-label {
  font-size: 12px;
  color: var(--navy-mute);
}

.profile-avatar {
  width: 96px;
  height: 96px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  border: 1px solid var(--line);
}

.rider-name { font-weight: 600; }

/* Secondary lines stacked under a table cell's main link: the ride date
   and the rider's own Strava activity title on rider.php, the activity
   title on ride.php. Deliberately part of the cell rather than their own
   .col-extra columns, so this context survives on a phone where those
   columns are hidden. Replaced a hover tooltip that showed the activity
   title, which was invisible on touch devices and undiscoverable
   anywhere else. */
.cell-sub {
  color: var(--navy-mute);
  font-size: 12px;
  margin-top: 2px;
}

/* The ride date on rider.php, sitting inline after the ride title so it
   reads as part of the same line. Being inline it drops to the next
   line on its own when the title is long enough to need it, which is
   the wrapping behaviour we want rather than a fixed second row.
   nowrap keeps the date itself from splitting across that break. */
.cell-date {
  color: var(--navy-mute);
  font-size: 12px;
  margin-left: 7px;
  white-space: nowrap;
}

/* Modifier for where the date belongs on its own line beneath the title
   rather than trailing it (my-rides.php, whose first column carries both the
   activity name and its full date+time). Dropping the inline left margin is
   the point — kept as a modifier so the shared .cell-date used inline on
   rider.php/rides.php is untouched. */
.cell-date-own-line {
  display: block;
  margin-left: 0;
  margin-top: 2px;
}

/* The Strava activity line: a small logo followed by the activity
   title, with the whole line acting as the link out to Strava. The logo
   and Strava's own orange are what mark it as an external link, so
   there's no separate arrow icon. Laid out as a flex row so the
   truncation applies to the title alone and never clips the logo. */
.cell-strava-link {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 260px;
  text-decoration: none;
  color: var(--strava);
}

/* Holds the Strava orange on hover rather than shifting to the club's
   coral, which would undercut the point of colouring it at all. */
.cell-strava-link:hover {
  color: var(--strava);
  text-decoration: underline;
}

/* Strava activity titles are free text and regularly long enough to
   stretch the column on their own. The full text stays available via
   the link's title attribute. */
.cell-strava-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The supplied logo is a hard square, so round it just enough to sit
   comfortably next to the site's other rounded shapes. Kept small: at
   12px a larger radius starts eating into the mark itself. */
.cell-strava-icon {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  border-radius: 3px;
}

/* --------------------------------------------------------------------
   Strava activity privacy markers (see strava_activity_link_html())
   -------------------------------------------------------------------- */

/* Icon artwork is LINKED from assets/svg/ and applied as a CSS mask, never
   as an <img>. Those files ship with a hardcoded fill="#000000"; a mask uses
   only their alpha channel, so that black is discarded entirely and the icon
   is painted by `background-color: currentColor` instead — i.e. it inherits
   whatever club-palette colour the surrounding row already has, and follows
   an admin re-theme for free. An <img> could not be recoloured at all, and
   inlining the files would mean rewriting their fill on every render.

   The shared base lives here; each icon only adds its own mask-image below.
   -webkit- prefixes are kept for Safari, which still needs them. */
.visibility-icon,
.lock-icon {
  display: inline-block;
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  vertical-align: -1px;
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-size: contain;
          mask-size: contain;
}

/* Muted by default: these are qualifiers on a value, not competing with the
   value itself or the Strava mark beside them. */
.visibility-icon { opacity: 0.55; }

.visibility-icon-everyone {
  -webkit-mask-image: url("/assets/svg/globe.svg");
          mask-image: url("/assets/svg/globe.svg");
}
.visibility-icon-followers_only {
  -webkit-mask-image: url("/assets/svg/friends.svg");
          mask-image: url("/assets/svg/friends.svg");
}
.visibility-icon-only_me {
  -webkit-mask-image: url("/assets/svg/private.svg");
          mask-image: url("/assets/svg/private.svg");
}
.lock-icon {
  -webkit-mask-image: url("/assets/svg/lock.svg");
          mask-image: url("/assets/svg/lock.svg");
}

/* Without mask support the elements above would render as solid coloured
   blocks, which is worse than no icon at all — so the paint is only applied
   where masking actually works. Every current browser qualifies; this is
   purely a floor for very old ones. */
@supports not ((-webkit-mask-image: url("/assets/svg/lock.svg")) or (mask-image: url("/assets/svg/lock.svg"))) {
  .visibility-icon,
  .lock-icon { background-color: transparent; }
}

/* The icon leads the label, so it needs its own right-hand spacing in the
   admin tables' plain .table-link, which isn't a flex container. The
   inline-flex contexts (.cell-strava-link, .table-link-private) already
   space it with their own `gap`, so adding a margin there would double up. */
.table-link .visibility-icon { margin-right: 4px; }

/* Private is the one worth actually noticing — it's also the case where
   the name isn't a link at all, so it carries no link colour to lean on. */
.visibility-icon-only_me { opacity: 0.75; }

/* A private activity in the public ride/rider history lines: same layout as
   .cell-strava-link so the row doesn't shift, but in muted body text rather
   than Strava orange, and with no hover affordance — there's nothing to
   click through to. The Strava mark is dropped for the same reason. */
.cell-strava-link.is-private {
  color: var(--navy-mute);
  cursor: default;
  text-decoration: none;
}

/* The admin tables' equivalent: plain muted text with the marker, sitting
   where a .table-link would otherwise be. */
.table-link-private {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--navy-mute);
  cursor: default;
}

/* Stands in for a stat hidden because the whole ride is private on Strava
   (see private_activity_stat_placeholder()). Muted, so a row of these reads
   as "nothing to see" rather than drawing the eye down the column, and
   centred in the cell the way the "—" placeholder it replaces would be. */
.stat-private {
  display: inline-flex;
  align-items: center;
  color: var(--navy-mute);
  cursor: default;
}

/* Stands alone in a stat cell rather than trailing a label, so it can be a
   touch larger and softer than the inline markers. */
.stat-private .visibility-icon {
  width: 13px;
  height: 13px;
  opacity: 0.5;
}

/* The per-stat "rider keeps this private" lock (see hidden_stat_lock_icon()),
   which replaced a 🔒 emoji so it matches the linked SVG markers in size and
   palette. Same muted treatment as .stat-private, since both mean "there's a
   value here you can't see" — they differ only in why. */
.stat-lock {
  display: inline-flex;
  align-items: center;
  color: var(--navy-mute);
  cursor: default;
}

.stat-lock .lock-icon {
  width: 13px;
  height: 13px;
  opacity: 0.5;
}

/* In the rider profile's KPI blocks the lock replaces a large stat number, so
   it needs to hold that visual weight rather than sitting as a tiny mark. */
.kpi-value .stat-lock .lock-icon {
  width: 22px;
  height: 22px;
  opacity: 0.45;
}


.rider-name a { text-decoration: none; }
.rider-name a:hover { color: var(--coral-deep); text-decoration: underline; }

/* Same treatment as .ride-title on rides.php — used for links inside
   table cells on ride.php, riders.php, and rider.php (e.g. the ride
   history table's title links). */
.table-link { text-decoration: none; }
.table-link:hover { color: var(--coral-deep); text-decoration: underline; }
.rider-nickname { color: var(--navy-mute); font-size: 12.5px; }

.rider-avatar-wrap {
  position: relative;
  flex-shrink: 0;
}

.rider-rank-bubble {
  position: absolute;
  bottom: -4px;
  left: -4px;
  background: var(--navy);
  color: #fff;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 700;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border-radius: 999px;
  border: 2px solid var(--card);
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

.rider-last-ride { color: var(--navy-mute); font-size: 11px; margin-top: 2px; }

.stat-mono {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

/* Recent rides list */

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

.ride-item {
  padding: 16px 22px;
  border-bottom: 1px solid var(--line);
}

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

.ride-item-top {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
}

.ride-title {
  font-weight: 700;
  font-size: 14.5px;
  color: var(--text);
  text-decoration: none;
}

a.ride-title:hover { color: var(--coral-deep); text-decoration: underline; }

.ride-date {
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--navy-mute);
  white-space: nowrap;
}

.ride-route {
  font-size: 13px;
  color: var(--navy-mute);
  margin: 3px 0 8px;
}

.detail-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.detail-list li {
  font-size: 14px;
  color: var(--text);
}

.ride-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 14px;
  font-size: 12.5px;
}

.ride-meta-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: var(--cream);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 3px 10px;
  font-family: var(--font-mono);
  color: var(--text);
  white-space: nowrap;
}

/* Solid Strava-orange "View on Strava" button under a route/ride's map
   thumbnail (routes.php, upcoming.php) — deliberately distinct from the
   plain outlined .ride-meta-chip look, so this one reads as Strava's own
   brand action rather than just another neutral metadata chip. */
.strava-map-link {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  background: var(--cream);
  color: var(--strava);
  border: 1px solid var(--strava);
  border-radius: 999px;
  padding: 4px 10px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-weight: 600;
  text-decoration: none;
  text-shadow: none;
  box-shadow: none;
  white-space: nowrap;
}

.strava-map-link:hover { background: #FFF1E9; }

/* Empty state */

.empty-state {
  padding: 40px 22px;
  text-align: center;
  color: var(--navy-mute);
}

.empty-state strong { display: block; color: var(--text); margin-bottom: 4px; font-family: var(--font-display); font-size: 15px; }

/* --------------------------------------------------------------------
   Strava connect callout
   -------------------------------------------------------------------- */

.connect-callout {
  margin-top: 24px;
  background: linear-gradient(135deg, var(--navy) 0%, var(--navy-soft) 100%);
  border-radius: var(--radius-lg);
  padding: 22px 26px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  color: var(--cream);
  flex-wrap: wrap;
}

.connect-callout h3 {
  font-family: var(--font-display);
  font-size: 17px;
  margin: 0 0 4px;
}

.connect-callout p {
  margin: 0;
  font-size: 13.5px;
  color: #C9C9EA;
  max-width: 46ch;
}

/* Sign-in card (connect.php): one focused, centred column that reads as
   an auth screen rather than a page of prose. Narrow wrap, logo on top,
   numbered-badge steps, everything on one type scale. */
.connect-wrap {
  max-width: 500px;
  margin: 0 auto;
}

.connect-signin {
  text-align: center;
}

/* Navy header band. Full-bleed to the card edges (no padding on the card
   itself), clipped to the rounded top corners by the card's overflow. */
.connect-banner {
  background: var(--navy);
  padding: 26px 32px;
}

.connect-body {
  padding: 28px 32px 32px;
}

.connect-logo {
  height: 56px;
  width: auto;
  margin: 0 auto;
  display: block;
}

.connect-title {
  margin: 0 0 8px;
  font-family: var(--font-display);
  font-size: 26px;
}

.connect-sub {
  margin: 0 auto 26px;
  max-width: 40ch;
  color: var(--navy-mute);
  font-size: 15px;
  line-height: 1.6;
}

.connect-sub em { font-style: italic; color: var(--navy); }

/* Steps: a left-aligned column centred as a block inside the card, each
   with a circular navy number badge instead of a default list marker. */
.connect-steps {
  list-style: none;
  counter-reset: step;
  margin: 0 auto 26px;
  padding: 0;
  max-width: 360px;
  text-align: left;
}

.connect-steps li {
  counter-increment: step;
  position: relative;
  padding: 3px 0 3px 42px;
  margin-bottom: 14px;
  min-height: 28px;
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--navy);
}

.connect-steps li:last-child { margin-bottom: 0; }

.connect-steps li::before {
  content: counter(step);
  position: absolute;
  left: 0;
  top: 0;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--navy);
  color: var(--cream);
  font-family: var(--font-mono);
  font-size: 13px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.connect-action {
  display: flex;
  justify-content: center;
  margin: 0 0 22px;
}

.connect-fineprint {
  margin: 0 auto 8px;
  max-width: 44ch;
  line-height: 1.6;
  text-align: center;
}

.connect-fineprint:last-child { margin-bottom: 0; }

/* --------------------------------------------------------------------
   Forms + admin UI
   -------------------------------------------------------------------- */

.form-group { margin-bottom: 18px; }

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

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

.form-hint {
  font-size: 12px;
  color: var(--navy-mute);
  margin-top: 4px;
}

.form-input,
.form-textarea {
  width: 100%;
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--text);
  background: var(--cream);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
}

.form-input:focus,
.form-textarea:focus {
  outline: none;
  border-color: var(--coral);
}

.form-input:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.form-textarea { resize: vertical; min-height: 90px; }

.form-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 22px;
}

/* Shared by every "Filter by..." bar (rides.php, riders.php, stats.php,
   and the admin matching tools) — a row of form-groups plus a trailing
   button/link. flex-wrap alone still lets a wide control (e.g. a route
   dropdown with a long name selected) claim more than its share of a
   wrapped row on a narrow phone, crowding whatever wraps in beside it.
   Below 620px every child goes full-width and stacks one per line
   instead, so nothing has to fight for horizontal space. */
.filter-bar {
  display: flex;
  gap: 14px;
  align-items: flex-end;
  flex-wrap: wrap;
}

.btn-primary,
.btn-secondary,
.btn-danger,
.btn-whatsapp {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  font-size: 14px;
  text-decoration: none;
  padding: 10px 18px;
  border-radius: 999px;
  border: none;
  cursor: pointer;
  font-family: var(--font-body);
}

/* Shared inline confirm (see the data-confirm handler in footer.php) — the
   cross-site replacement for native confirm(), which Brave on mobile
   suppresses. A form's button is swapped for "message [Yes] [Cancel]" in
   place. Wraps on narrow widths so it fits a table cell or a card alike. */
.inline-confirm {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  font-size: 13.5px;
}
.inline-confirm-msg { font-weight: 600; }
.inline-confirm-actions { display: inline-flex; gap: 8px; flex-shrink: 0; }
.inline-confirm-yes,
.inline-confirm-no {
  font: inherit;
  font-weight: 600;
  padding: 6px 14px;
  border-radius: 8px;
  cursor: pointer;
  white-space: nowrap;
}
.inline-confirm-yes { background: var(--coral); color: #fff; border: 1px solid var(--coral); }
.inline-confirm-yes:hover { background: var(--coral-deep); }
.inline-confirm-no { background: transparent; color: var(--text); border: 1px solid var(--line); }
.inline-confirm-no:hover { background: var(--cream); }

.btn-primary {
  background: var(--navy);
  color: var(--cream);
}

.btn-secondary {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--line);
}

.btn-danger {
  background: transparent;
  color: var(--coral-deep);
  border: 1px solid var(--coral);
}

.btn-primary:disabled,
.btn-secondary:disabled,
.btn-danger:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.btn-whatsapp {
  background: #25D366;
  color: #fff;
}

.btn-whatsapp:hover { background: #1EBE5A; }

.btn-sm {
  padding: 6px 12px;
  font-size: 12.5px;
}

.alert-error {
  background: #FFF0F1;
  border: 1px solid var(--coral);
  color: var(--coral-deep);
  border-radius: var(--radius-sm);
  padding: 12px 16px;
  font-size: 13.5px;
  margin-bottom: 20px;
}

.alert-error ul { margin: 4px 0 0; padding-left: 18px; }

.alert-success {
  background: #EEF8F2;
  border: 1px solid var(--green);
  color: #2F7550;
  border-radius: var(--radius-sm);
  padding: 12px 16px;
  font-size: 13.5px;
  margin-bottom: 20px;
}

.alert-info {
  display: block;
  background: #FFFBEA;
  border: 1px solid var(--yellow-deep);
  color: var(--text);
  border-radius: var(--radius-sm);
  padding: 12px 16px;
  font-size: 13.5px;
  font-weight: 600;
  margin-bottom: 20px;
  text-decoration: none;
}

.alert-info:hover { background: #FFF6D6; }

/* The dismissable "finish setting up" nudge on the dashboard: a message
   link that fills the row, and a dismiss control pinned to the end.
   Overrides .alert-info's own display:block (declared after it, equal
   specificity). */
.settings-nudge {
  display: flex;
  align-items: center;
  gap: 12px;
}

.settings-nudge-msg {
  flex: 1;
  color: inherit;
  text-decoration: none;
}

.settings-nudge-msg:hover { text-decoration: underline; }

.settings-nudge-dismiss {
  flex-shrink: 0;
  color: var(--navy-mute);
  text-decoration: none;
  font-size: 22px;
  line-height: 1;
  padding: 0 4px;
}

.settings-nudge-dismiss:hover { color: var(--text); }

.table-actions { display: flex; gap: 10px; }

.table-actions a { font-size: 13px; font-weight: 600; text-decoration: none; }

.link-edit { color: var(--navy-soft); }
.link-delete { color: var(--coral-deep); }

/* --------------------------------------------------------------------
   Modal — currently just the "Full Weather Forecast" popup
   (includes/partials/weather-forecast-modal.php), generic enough to
   reuse if another one shows up later.
   -------------------------------------------------------------------- */

.modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(27, 27, 78, 0.55);
  z-index: 100;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

.modal-overlay.open { display: flex; }

.modal-box {
  background: var(--card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  width: 100%;
  max-width: 420px;
  max-height: 85vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 22px 14px;
  border-bottom: 1px solid var(--line);
  flex-shrink: 0;
}

.modal-header h3 {
  font-family: var(--font-display);
  font-size: 17px;
  margin: 0;
}

.modal-close {
  background: none;
  border: none;
  font-size: 22px;
  line-height: 1;
  color: var(--navy-mute);
  cursor: pointer;
  padding: 4px;
}

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

.modal-body {
  padding: 18px 22px 22px;
  overflow-y: auto;
}

.weather-forecast-header {
  margin-bottom: 12px;
}

.weather-forecast-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.weather-forecast-table th {
  text-align: left;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--navy-mute);
  padding: 6px 8px;
  border-bottom: 1px solid var(--line);
}

.weather-forecast-table td {
  padding: 6px 8px;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
}

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

/* Dark hours (before sunrise / after sunset). A cool navy tint, so night
   reads as cooler/darker against the warm cream page. Declared BEFORE the
   ride-window highlight below so that when a row is both night and in the
   ride window, the yellow highlight still wins on equal specificity. */
.weather-forecast-row-night td {
  background: rgba(27, 27, 78, 0.06);
  color: var(--navy-mute);
}

.weather-forecast-row-highlight td {
  background: var(--yellow);
  font-weight: 700;
}

/* Little legend under the table explaining the shading. The swatch is the
   same tint as a night row, so the key is unmistakable. */
.weather-forecast-legend {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 12px 0 0;
  font-size: 12px;
  color: var(--navy-mute);
}

.weather-forecast-legend-swatch {
  width: 22px;
  height: 14px;
  border-radius: 3px;
  background: rgba(27, 27, 78, 0.06);
  border: 1px solid var(--line);
  flex-shrink: 0;
}

/* Peak-UV line sitting above the hourly table. */
.weather-forecast-uvmax {
  margin: 0 0 12px;
  font-size: 13px;
  color: var(--navy);
}

/* Keeps a wide table scrolling inside its own box rather than spilling past
   the edge of its card, or pushing the page/modal sideways on a narrow
   screen. Used by the hourly weather table (weather.php + the ride forecast
   modal) and by admin/simulate-matching.php, whose seven columns include
   some deliberately wordy diagnostic labels. Was .weather-forecast-scroll
   when the weather table was the only wide one. */
.table-scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* ---- admin/simulate-matching.php ---------------------------------------
   This is the tuning surface for the matching constants, so it's laid out as
   one card per activity rather than one row of a wide table. Two gate
   figures, two confidence scores, an evidence state and two verdicts don't
   fit a table row without either clipping or abbreviating past the point of
   legibility — the gate limit was rendered "/ 3" before this, which read as
   a fraction to everyone but its author. Namespaced .sim-* like review.php's
   .rv-*, since this shares the site-wide stylesheet. */
.sim-list {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.sim-row {
  background: var(--card);
  border: 1px solid var(--line);
  /* Left edge is the mismatch signal, so it's always present and only
     changes colour — otherwise every card would shift 3px when flagged. */
  border-left: 4px solid var(--line);
  border-radius: var(--radius-md);
  padding: 15px 18px;
}

/* A row where the simulation disagrees with what's actually stored. That
   disagreement is the entire point of the page, so it's marked on the card's
   edge rather than by tinting the whole background, which would compete with
   the status chips sitting inside it. */
.sim-row.is-mismatch {
  border-left-color: var(--coral);
}

.sim-head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: 6px 18px;
  padding-bottom: 12px;
  margin-bottom: 13px;
  border-bottom: 1px solid var(--line);
}

.sim-when {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px 12px;
}

/* auto-fit rather than a fixed count: the blocks are independent, so on a
   narrow window they should reflow to fewer columns instead of each being
   squeezed — which is the failure this layout exists to fix. */
.sim-metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(155px, 1fr));
  gap: 15px 22px;
}

/* Same idiom as .kpi-label, retoned for a light card instead of navy. */
.sim-metric-label {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--navy-mute);
  margin-bottom: 7px;
}

.sim-metric-value {
  font-size: 15px;
  line-height: 1.35;
}

.sim-metric-score {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 21px;
  line-height: 1;
}

.sim-metric-note {
  margin-top: 6px;
  font-size: 12px;
  color: var(--navy-mute);
  line-height: 1.45;
}

/* The verdict carries the longest label here ("Blocked by start gate
   (clustering 38%)"), so it gets two tracks and its chip is allowed to wrap,
   rather than that one string dictating the width of every other block. */
.sim-metric-verdict {
  grid-column: span 2;
}

.sim-metric-verdict .ride-meta-chip {
  white-space: normal;
  text-align: left;
}

@media (max-width: 620px) {
  .sim-metrics { grid-template-columns: repeat(auto-fit, minmax(115px, 1fr)); }
  .sim-metric-verdict { grid-column: 1 / -1; }
}

/* "Conditions right now" card on the weather page. A hero row (big
   emoji + temperature) above a wrapping row of stat tiles. */
.weather-now {
  padding: 24px 26px;
}

.weather-now-hero {
  display: flex;
  align-items: center;
  gap: 18px;
}

.weather-now-emoji {
  font-size: 52px;
  line-height: 1;
}

.weather-now-temp {
  font-family: var(--font-mono);
  font-size: 40px;
  font-weight: 500;
  color: var(--navy);
  line-height: 1.05;
}

.weather-now-label {
  font-weight: 600;
  color: var(--navy);
  margin-top: 2px;
}

.weather-now-asof {
  font-size: 12px;
  color: var(--navy-mute);
  margin-top: 4px;
}

.weather-now-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 20px;
  padding-top: 18px;
  border-top: 1px solid var(--line);
}

.weather-now-stat {
  flex: 1 1 120px;
  background: var(--cream);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
}

.weather-now-stat-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--navy-mute);
  font-weight: 700;
  margin-bottom: 4px;
}

.weather-now-stat-value {
  font-size: 15px;
  color: var(--navy);
}

/* Gusts sit under the main wind reading rather than beside it, so the
   tile stays one line wide on a phone. */
.weather-now-stat-sub {
  display: block;
  font-size: 12px;
  color: var(--navy-mute);
  margin-top: 1px;
}

/* Static Mapbox location map banner at the top of the weather page's
   forecast card. Sits flush to the card's rounded top corners (the card
   is overflow:hidden), a strip rather than a square so it sets the place
   without pushing the actual forecast below the fold. */
.weather-location-map {
  display: block;
  width: 100%;
  height: 180px;
  object-fit: cover;
  border-bottom: 1px solid var(--line);
}

@media (max-width: 620px) {
  .weather-location-map { height: 130px; }
}

/* --------------------------------------------------------------------
   Responsive
   -------------------------------------------------------------------- */

.table-mobile-hint {
  display: none;
}

/* Before collapsing entirely, tighten the nav links so they keep fitting
   alongside the (full-size, never-shrinking) logo as the row narrows —
   otherwise the jump straight to the hamburger happens while there's still
   obvious space, leaving the header looking half-empty. This buys ~80px, so
   the collapse below can sit much closer to where the row is genuinely
   full. */
@media (max-width: 1024px) {
  .main-nav { gap: 1px; }
  .main-nav a { padding: 6px 8px; font-size: 13px; }
}

/* The horizontal nav collapses into the hamburger below this width — still
   wider than the other mobile rules (860px), since the 8-item nav plus the
   brand logo and account controls genuinely need this much room even after
   the shrink above. Below here the nav links live in the menu dropdown
   (.rider-menu-nav-section) instead, freeing the space for a full-size logo.
   This value and the shrink breakpoint above are the two tuning knobs: if
   the nav still collapses with visible space left, lower this; if the logo
   and nav ever touch/overflow just above it, raise it (a very wide logo
   needs a higher value). */
@media (max-width: 900px) {
  .main-nav { display: none; }

  .rider-menu-dropdown { min-width: 220px; }

  .rider-menu-nav-section {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-bottom: 8px;
    padding-bottom: 8px;
  }
}

@media (max-width: 860px) {
  /* Desktop reads top-to-bottom within each of the 2 columns (Leaderboard
     then Birthdays on the left; Last ride, then Recent Milestones on the
     right) — a sequence that has nothing to do with the order riders
     actually want them in once there's only one column. .dashboard-col
     switching to display:contents un-boxes each column wrapper, so
     .dashboard-grid (now a single flex column) sees all 4 cards directly
     and can lay them out by their own `order` below, independent of
     source position or desktop column. */
  .dashboard-grid {
    display: flex;
    flex-direction: column;
    gap: 20px;
  }
  .dashboard-col {
    display: contents;
  }
  .dashboard-card-lastride    { order: 1; }
  .dashboard-card-leaderboard { order: 2; }
  .dashboard-card-birthdays   { order: 3; }
  .dashboard-card-milestones  { order: 4; }
}

/* Portrait phones only — landscape phones and anything wider than
   860px keep the full table. Deliberately keyed on orientation, not
   just width, since a phone turned sideways has plenty of room for
   every column. */
@media (max-width: 860px) and (orientation: portrait) {
  .table-mobile-hint {
    display: block;
    text-align: center;
    font-size: 12px;
    color: var(--navy-mute);
    padding: 10px 16px;
    background: var(--cream);
    border-bottom: 1px solid var(--line);
  }

  .col-extra { display: none; }
}

@media (max-width: 620px) {
  /* 3 x 2 rather than 2 x 3: six KPIs divide evenly either way, but
     three columns keeps the whole strip to two rows so it takes up
     less of the screen before the actual dashboard content starts. */
  .kpi-strip { grid-template-columns: repeat(3, 1fr); gap: 8px; }

  /* Tighter than the desktop card, since three across a phone leaves
     roughly a third of the width each. */
  .kpi-card { padding: 12px 10px; }
  .kpi-label { font-size: 9.5px; letter-spacing: 0.04em; margin-bottom: 6px; }
  .kpi-value { font-size: 17px; }
  .kpi-unit { font-size: 11px; margin-left: 2px; }

  /* The bike keeps its size on a phone; only the profile below it
     shortens, so the two stay in contact. */
  .elevation-divider { height: 40px; margin-bottom: 24px; }
  .elevation-divider-line { height: 22px; }
  .elevation-divider-bike { bottom: 18.7px; }

  /* Less room for a long Strava title once the table is down to its
     two visible columns. */
  .cell-strava-link { max-width: 170px; }

  .filter-bar {
    align-items: stretch;
  }

  .filter-bar > .form-group,
  .filter-bar > button,
  .filter-bar > a,
  .filter-bar > span.form-hint {
    width: 100%;
  }

  .filter-bar > button,
  .filter-bar > a.btn-secondary {
    justify-content: center;
  }
}

/* Shared by the "Next ride" card (index.php) and each ride on
   upcoming.php: a route image sits beside the ride's title/details on
   desktop, but a fixed-width image column squashes the details column
   into too little space on a narrow phone. Grid areas let the exact
   same markup reorder into a clean single-column stack on mobile
   (title, then image, then the rest) instead - no separate markup
   needed for each breakpoint. */
.ride-card-layout {
  display: grid;
  grid-template-columns: 160px 1fr;
  grid-template-areas:
    "image title"
    "image details";
  column-gap: 18px;
  row-gap: 6px;
}

.ride-card-layout.no-image {
  grid-template-columns: 1fr;
  grid-template-areas:
    "title"
    "details";
}

.ride-card-image-col { grid-area: image; }
.ride-card-title-row { grid-area: title; margin: 0; }
.ride-card-details { grid-area: details; min-width: 0; }

@media (max-width: 620px) {
  .ride-card-layout,
  .ride-card-layout.no-image {
    grid-template-columns: 1fr;
    grid-template-areas:
      "title"
      "image"
      "details";
    row-gap: 12px;
  }
}

/* Same image/details split as .ride-card-layout, minus the "title" row
   — for ride.php, whose card has no title of its own (that's already
   the page's own <h1>, above the card). Reuses .ride-card-image-col
   and .ride-card-details directly, since those just assign a grid-area
   name and don't care which template defined it. */
.ride-detail-layout {
  display: grid;
  grid-template-columns: 160px 1fr;
  grid-template-areas: "image details";
  column-gap: 18px;
}

.ride-detail-layout.no-image {
  grid-template-columns: 1fr;
  grid-template-areas: "details";
}

@media (max-width: 620px) {
  .ride-detail-layout,
  .ride-detail-layout.no-image {
    grid-template-columns: 1fr;
    grid-template-areas:
      "image"
      "details";
    row-gap: 12px;
  }
}

@media (max-width: 520px) {
  /* Deliberately no .kpi-strip override here: it used to drop to a
     single column, which turned six KPIs into six full-width rows and
     pushed the dashboard itself well below the fold. The 3 x 2 grid
     from the 620px breakpoint carries all the way down instead. */
  .page { padding: 28px 18px 60px; }
  .form-row { grid-template-columns: 1fr; }

  /* At full padding, riders.php's three tabs ("Summary" / "Personal
     Bests" / "Leaderboard") are too wide for a narrow phone and press
     against the edges of the pill. Tightening them lets the whole
     switcher sit on screen with room to spare.

     Scoped away from .leaderboard-tabs deliberately. That variant sets
     its own smaller sizing higher up the file, and since this rule has
     equal specificity but comes later, an unscoped .period-toggle a
     here would override it and make the dashboard's dense tabs LARGER
     on a phone than on desktop, which is the opposite of the intent. */
  .period-toggle:not(.leaderboard-tabs) a { padding: 7px 9px; font-size: 12px; }

  /* Reclaims 24px for the dashboard's leaderboard strip, which
     otherwise lands a few pixels over a 375px phone's width.

     Centred rather than right-aligned down here too: once the strip
     takes up most of the available width, a right-aligned pill sits
     just off-centre, which reads as a mistake rather than as
     deliberate alignment. Right-aligned still looks intentional on
     desktop, where there's real space to the left of it. */
  .tab-strip-row {
    margin-left: 10px;
    margin-right: 10px;
    justify-content: center;
  }
}

/* ==========================================================================
   Year/Month in Review (review.php)
   A standalone immersive "Wrapped"-style deck: a vertical scroll of
   full-height panels, each committing to one club colour. Namespaced
   .rv-* so nothing here touches the rest of the site, which shares this
   stylesheet. Deliberately single-theme — it's a branded deck.
   ========================================================================== */
.rv-body { margin: 0; background: var(--navy); color: var(--text); font-family: var(--font-body); -webkit-font-smoothing: antialiased; line-height: 1.5; }

.rv-back {
  position: fixed; top: 14px; left: 16px; z-index: 20;
  font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.04em;
  color: #fff; text-decoration: none; background: rgba(0,0,0,0.35);
  padding: 7px 12px; border-radius: 999px; backdrop-filter: blur(4px);
}
.rv-back:hover { background: rgba(0,0,0,0.55); }

.rv-deck { display: flex; flex-direction: column; }

.rv-panel { min-height: 92vh; display: flex; flex-direction: column; justify-content: center; padding: 76px 28px; }
.rv-inner { width: 100%; max-width: 620px; margin: 0 auto; }

.rv-g-navy   { background: var(--navy);   color: var(--cream); }
.rv-g-cream  { background: var(--cream);  color: var(--text); }
.rv-g-coral  { background: var(--coral);  color: #fff; }
.rv-g-yellow { background: var(--yellow); color: #23306e; }

.rv-eyebrow { font-family: var(--font-mono); font-size: 12.5px; letter-spacing: 0.18em; text-transform: uppercase; opacity: 0.72; margin: 0 0 18px; }

.rv-h1, .rv-h2, .rv-stat-huge { font-family: var(--font-display); font-weight: 800; line-height: 1.03; text-wrap: balance; margin: 0; }
.rv-h1 { font-size: clamp(38px, 8.5vw, 72px); }
.rv-h2 { font-size: clamp(27px, 5.5vw, 38px); }

/* Cover */
.rv-cover .rv-logo { height: 60px; width: auto; display: block; margin: 0 0 20px; }
.rv-brand { font-family: var(--font-mono); font-size: 12.5px; letter-spacing: 0.16em; text-transform: uppercase; opacity: 0.82; margin: 0 0 20px; }
.rv-sub { font-size: 17px; opacity: 0.88; margin: 18px 0 0; max-width: 34ch; }
.rv-sub-dark { font-size: 16px; color: var(--navy-mute); margin: 16px 0 0; max-width: 40ch; }
.rv-scrollhint { margin-top: 42px; font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.6; }

/* Totals hero */
.rv-stat-huge { font-size: clamp(70px, 19vw, 148px); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.rv-stat-huge .rv-unit { font-size: 0.32em; letter-spacing: 0.01em; }
.rv-trio { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin: 30px 0 26px; }
.rv-cell { border-top: 2px solid rgba(30,40,102,0.16); padding-top: 12px; }
.rv-num { font-family: var(--font-mono); font-size: clamp(21px, 5vw, 29px); font-weight: 500; font-variant-numeric: tabular-nums; }
.rv-num-unit { font-size: 0.6em; }
.rv-cap { font-size: 12.5px; opacity: 0.7; margin-top: 2px; }
.rv-landmark { margin: 4px 0 0; font-size: 16px; line-height: 1.55; background: rgba(255,255,255,0.55); border-radius: 14px; padding: 16px 18px; }
.rv-landmark strong { font-family: var(--font-display); }

/* Biggest days */
.rv-records { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; margin-top: 26px; }
.rv-record { background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3); border-radius: 16px; padding: 18px; }
.rv-rlabel { font-family: var(--font-mono); font-size: 11.5px; letter-spacing: 0.12em; text-transform: uppercase; opacity: 0.9; }
.rv-rval { font-family: var(--font-display); font-weight: 800; font-size: clamp(28px, 7vw, 40px); margin: 6px 0 4px; font-variant-numeric: tabular-nums; }
.rv-rval-unit { font-size: 0.5em; }
.rv-rwhere { font-size: 13.5px; opacity: 0.92; }

/* Your roads */
.rv-bars { margin-top: 26px; display: flex; flex-direction: column; gap: 16px; }
.rv-barhead { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 7px; }
.rv-bname { font-weight: 600; }
.rv-bcount { font-family: var(--font-mono); font-size: 14px; font-variant-numeric: tabular-nums; }
.rv-track { height: 14px; background: rgba(30,40,102,0.10); border-radius: 999px; overflow: hidden; }
.rv-fill { height: 100%; background: var(--coral); border-radius: 999px; }

/* Your people */
.rv-people { margin-top: 26px; display: flex; flex-direction: column; gap: 12px; }
.rv-person { display: flex; align-items: center; gap: 14px; }
.rv-avatar { width: 46px; height: 46px; border-radius: 50%; flex-shrink: 0; object-fit: cover; background: rgba(255,255,255,0.2); }
.rv-pname { font-weight: 600; font-size: 17px; }
.rv-ptog { font-family: var(--font-mono); font-size: 13px; opacity: 0.82; }
.rv-person.rv-lead .rv-avatar { width: 58px; height: 58px; box-shadow: 0 0 0 3px rgba(255,255,255,0.5); }
.rv-person.rv-lead .rv-pname { font-size: 20px; }

/* What you earned */
.rv-chipset + .rv-chipset { margin-top: 22px; }
.rv-chipset { margin-top: 20px; }
.rv-clab { font-family: var(--font-mono); font-size: 11.5px; letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.72; margin-bottom: 12px; }
.rv-chips { display: flex; flex-wrap: wrap; gap: 10px; }
.rv-chip { background: #23306e; color: var(--yellow); border-radius: 999px; padding: 9px 15px; font-weight: 600; font-size: 14px; display: inline-flex; align-items: center; gap: 8px; }
.rv-chip.rv-trophy { background: #fff; color: #23306e; }
.rv-chip-who { opacity: 0.72; font-weight: 500; }

/* Highlight cards — a 2x2 grid of standout achievements on the club
   review's earned panel. */
.rv-highlights { margin-top: 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.rv-highlight { background: rgba(255,255,255,0.6); border-radius: 14px; padding: 15px 16px; }
.rv-hl-emoji { font-size: 22px; line-height: 1; }
.rv-hl-title { font-family: var(--font-display); font-weight: 700; font-size: 16px; color: #23306e; margin-top: 8px; }
.rv-hl-sub { font-size: 12.5px; color: #23306e; opacity: 0.7; margin-top: 2px; }

/* Summary — the final "hero screenshot" card recapping the headline
   numbers in a 2x2 stat grid. */
.rv-summary .rv-logo { height: 52px; width: auto; display: block; margin: 0 0 18px; }
.rv-summary-title { font-family: var(--font-display); font-weight: 800; font-size: clamp(30px, 7vw, 46px); line-height: 1.05; margin: 0; }
.rv-summary-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px 20px; margin: 28px 0 0; }
.rv-sum-num { font-family: var(--font-mono); font-weight: 500; font-size: clamp(30px, 8vw, 44px); font-variant-numeric: tabular-nums; line-height: 1; }
.rv-sum-cap { font-size: 13px; opacity: 0.75; margin-top: 4px; }
.rv-summary-tag { margin: 26px 0 0; font-size: 16px; opacity: 0.9; }
.rv-summary-tag strong { font-family: var(--font-display); }
.rv-summary-share { margin: 26px 0 0; font-family: var(--font-mono); font-size: 12.5px; letter-spacing: 0.04em; opacity: 0.7; }

/* Rank */
.rv-ranks { margin-top: 24px; display: flex; flex-direction: column; gap: 16px; }
.rv-rank { display: flex; align-items: baseline; gap: 18px; padding-bottom: 16px; border-bottom: 1px solid rgba(251,246,233,0.2); }
.rv-rank:last-child { border-bottom: none; }
.rv-place { font-family: var(--font-display); font-weight: 800; font-size: clamp(40px, 10vw, 58px); line-height: 1; color: var(--yellow); min-width: 2.4ch; }
.rv-rk { font-size: 17px; font-weight: 600; }
.rv-rof { font-family: var(--font-mono); font-size: 13px; opacity: 0.8; }

/* Closing */
.rv-yearswitch { margin-top: 26px; display: flex; flex-wrap: wrap; align-items: center; gap: 12px; font-family: var(--font-mono); font-size: 14px; }
.rv-yearswitch-lab { opacity: 0.8; letter-spacing: 0.06em; text-transform: uppercase; font-size: 12px; }
.rv-yearswitch a { color: #fff; text-decoration: none; border-bottom: 2px solid rgba(255,255,255,0.4); }
.rv-yearswitch a.rv-year-current { border-bottom-color: #fff; font-weight: 700; }

/* Scroll reveal */
.rv-reveal { opacity: 0; transform: translateY(22px); transition: opacity 0.6s ease, transform 0.6s ease; }
.rv-reveal.rv-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) { .rv-reveal { opacity: 1; transform: none; transition: none; } }

@media (max-width: 480px) {
  .rv-trio { grid-template-columns: 1fr 1fr; }
  .rv-records { grid-template-columns: 1fr; }
}
