:root {
  /* Dark is the default palette; :root[data-theme="light"] below overrides it. */
  --brand: #1f6feb;
  --brand-dark: #14508f;
  --bg: #0f1420;
  --card: #1a2233;
  --text: #eef1f6;
  --muted: #97a3b8;
  --border: #2a3448;
  --danger: #ff6b6b;
  --warn: #f5c14b;
  --ok: #4ade80;
  /* Material 3-inspired shape scale: rounded squares for surfaces, a true pill for buttons. */
  --radius-lg: 24px;
  --radius-md: 16px;
  --radius-pill: 999px;
  --elevation-1: 0 1px 2px rgba(0,0,0,0.4), 0 1px 3px rgba(0,0,0,0.3);
  --elevation-2: 0 4px 8px rgba(0,0,0,0.45), 0 2px 4px rgba(0,0,0,0.3);
}

:root[data-theme="light"] {
  --bg: #f5f6f8;
  --card: #ffffff;
  --text: #1a1f26;
  --muted: #6b7280;
  --border: #e2e5ea;
  --danger: #d64545;
  --warn: #b5860a;
  --ok: #1e8a4c;
}

* { box-sizing: border-box; }

/* The page scrolls at the document level (no inner scroll container), so the scrollbar
   to hide is html's, not a wrapper div's. Scrolling itself stays fully functional -
   only the visible track/thumb are suppressed. */
html {
  scrollbar-width: none;
  -ms-overflow-style: none;
}
html::-webkit-scrollbar { display: none; }

body {
  margin: 0;
  font-family: -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  color: var(--text);
}

a { color: var(--brand); }
a:visited { color: var(--brand); }

.app-shell { min-height: 100vh; display: flex; flex-direction: column; }

.topbar {
  /* Sticky (not fixed) so it keeps taking up its normal spot in the document flow while not
     scrolled - no compensating top-padding needed on .content the way position:fixed would
     require. The top offset matches .status-bar-safe-area's own height (env(safe-area-inset-top))
     so on MAUI the stuck header lands right below that status-bar spacer instead of underneath
     it; on Web there's no such spacer and the env() falls back to 0px, so it just sticks to the
     true top. z-index keeps it above scrolling page content but below the drawer/overlay
     (.topbar-controls/.nav-overlay), which should still cover it when open. */
  position: sticky;
  top: env(safe-area-inset-top, 0px);
  z-index: 100;
  display: flex;
  align-items: center;
  gap: 1rem;
  /* Tight padding on purpose - the logo/brand and the hamburger should sit close to the
     screen edges, not indented like the rest of the page's content. No safe-area-inset-top
     here: unlike position:fixed elements (see .topbar-controls below), this MAUI WebView
     already lays out normal in-flow content below the status bar on its own - adding the
     inset here double-counted it and pushed the whole row down with a dead gap above it. */
  padding: 0.5rem 0.05rem 0.5rem 0.15rem;
  /* Branded chrome stays dark regardless of the light/dark content theme toggle below it -
     matches how most product headers (and the reference design) keep the bar itself fixed. */
  background: linear-gradient(120deg, #0b0f1a 0%, #13224a 55%, var(--brand-dark) 100%);
  color: white;
  box-shadow: 0 2px 16px rgba(0,0,0,0.35);
}
/* .topbar-row (logo/brand/primary nav/hamburger) is the only thing in normal flow - it's
   always a single row. .topbar-controls (My plan/Account/language/logout) is a fixed
   right-to-left drawer toggled by the hamburger, at every viewport width, not just narrow
   ones - see its rule below. No flex-wrap on .topbar-row at any nesting level, or a
   fragment wrapping onto its own sub-line gets vertically centered against its sibling and
   visually splits the header (same bug class already fixed in the Admin app). */
/* flex: 1 is load-bearing: .topbar-row is .topbar's only in-flow flex child, so without a
   grow factor it shrink-wraps to its own content (brand + button) and sits flush LEFT,
   leaving the hamburger's own margin-left:auto with no leftover space to push into inside
   that shrunken box - the button ends up right after the brand instead of at the true
   right edge of the screen. Stretching the row to .topbar's full width is what gives
   margin-left:auto something real to consume. */
.topbar-row { display: flex; flex: 1; align-items: center; gap: 0.6rem; flex-wrap: nowrap; min-width: 0; }
.topbar-row .brand-link {
  display: flex; align-items: center; gap: 0.6rem; min-width: 0; cursor: pointer;
}
.topbar .brand {
  font-weight: 700; font-size: 1.1rem; letter-spacing: 0.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.topbar .topnav { display: flex; align-items: center; flex-wrap: wrap; gap: 0.125rem; }
.topbar .topnav a {
  color: white; text-decoration: none; opacity: 0.75;
  display: flex; align-items: center; min-height: 44px; padding: 0 0.5rem;
  font-size: 1.05rem; white-space: nowrap;
  position: relative; transition: opacity 0.15s ease;
}
.topbar .topnav a:hover { opacity: 1; }
.topbar .topnav a.active { opacity: 1; font-weight: 600; }
.topbar .topnav a.active::after {
  content: "";
  position: absolute; left: 0.5rem; right: 0.5rem; bottom: 4px;
  height: 2px; border-radius: 2px;
  background: linear-gradient(90deg, var(--brand) 0%, #7fb8ff 100%);
}
.topbar .spacer { flex: 1; }
/* Specificity must beat .link-button (also sets display, and this button carries both
   classes) - a plain .nav-toggle rule loses that tie by source order regardless of which
   class comes first in the HTML.
   margin-left:auto, not a sibling spacer - a spacer plus .topbar-row's own gap added a
   second, redundant chunk of fixed space between the spacer and the button on top of the
   space the spacer already grew to fill, leaving the button looking like just another item
   in the row instead of pinned to the true edge. auto-margin consumes the row's leftover
   space by itself, right up against the button. */
.topbar-row .nav-toggle {
  /* Horizontal padding here (not just the topbar's own edge inset) is what gives the glyph
     visible breathing room from the true edge - flush-to-the-pixel read as a layout bug,
     not intentional design. */
  display: inline-flex; font-size: 1.5rem; padding: 0.3rem 0.85rem; flex-shrink: 0;
  margin-left: auto; margin-right: 0;
  /* Plain glyph, no pill: overrides .link-button's circle so this reads as a bare icon
     flush against the edge, not another chip-styled control in the row. */
  border: none; background: none; border-radius: 0;
}
.link-button {
  background: none; border: 1px solid rgba(255,255,255,0.6); color: white;
  border-radius: var(--radius-pill); padding: 0.3rem 0.7rem; cursor: pointer;
  transition: background-color 0.15s ease;
  min-height: 44px; margin: 0; display: inline-flex; align-items: center; justify-content: center;
  white-space: nowrap;
}
.link-button:hover { background: rgba(255,255,255,0.15); }
.lang-select {
  background: rgba(255,255,255,0.15); color: white; border: 1px solid rgba(255,255,255,0.4);
  border-radius: var(--radius-pill); padding: 0.25rem 0.6rem; font-size: 0.85rem;
  width: auto; flex: 0 0 auto; min-height: 44px;
}
.lang-select option { color: var(--text); }

/* Dims the page behind the open drawer and closes it on an outside tap. Starts below the
   status bar, same reasoning as .topbar-controls below. */
.nav-overlay {
  position: fixed;
  top: env(safe-area-inset-top, 0px);
  right: 0; bottom: 0; left: 0;
  background: rgba(0,0,0,0.45);
  z-index: 150;
}

/* Right-to-left slide-in drawer, capped at half the viewport width at every screen size -
   this is now the only way to reach My plan/Account/language/logout, so it isn't gated
   behind a narrow-screen media query the way the Admin app's inline-vs-collapsed nav is.
   top/height are offset by env(safe-area-inset-top) rather than 0/100vh - position:fixed is
   relative to the true device viewport, which on Android edge-to-edge hosts extends behind
   the status bar even though normal in-flow content (like .topbar) doesn't, so without this
   the drawer paints underneath the status bar icons instead of stopping below them. */
.topbar-controls {
  position: fixed;
  top: env(safe-area-inset-top, 0px);
  right: 0;
  height: calc(100vh - env(safe-area-inset-top, 0px));
  width: min(50vw, 320px);
  max-width: 50vw;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.25rem;
  padding: 4.5rem 1rem 1.5rem;
  background: linear-gradient(160deg, #0b0f1a 0%, #13224a 60%, var(--brand-dark) 100%);
  color: white;
  box-shadow: -4px 0 24px rgba(0,0,0,0.45);
  transform: translateX(100%);
  transition: transform 0.25s ease;
  overflow-y: auto;
  z-index: 200;
}
.topbar-controls.nav-open { transform: translateX(0); }
.topbar-controls .topnav { flex-direction: column; }
/* Larger than the compact header nav - the drawer is half the screen wide with only a
   handful of links in it, so the smaller header font size just leaves dead space on both sides. */
.topbar-controls .topnav a { padding: 0.85rem 0.5rem; font-size: 1.15rem; }
.topbar-actions { display: flex; flex-direction: column; align-items: stretch; gap: 0.5rem; }
.topbar-controls .link-button { font-size: 1.05rem; }
.topbar-controls .lang-select { font-size: 1rem; }

.content { padding: 1rem; max-width: 520px; margin: 0 auto; width: 100%; position: relative; }

/* Floating shortcut for "Check in now" (MyMembership.razor), shown only while that button
   itself is scrolled out of view - JS toggles the "show" class via an IntersectionObserver
   watching #checkin-cta (see js/ctaVisibility.js). Kept below .nav-overlay's z-index so an
   open drawer covers it, but above ordinary page content. Hidden state keeps pointer-events
   off (not display:none) so the transition below can animate it. */
.checkin-fab {
  position: fixed;
  right: 1.25rem;
  bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px));
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--brand);
  color: white;
  border: none;
  box-shadow: var(--elevation-2);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 120;
  opacity: 0;
  transform: scale(0.4) translateY(28px);
  pointer-events: none;
  transition: opacity 0.25s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* The reverse (hiding) transition is quicker and has no overshoot - it should read as the FAB
   "collapsing into" the real button appearing below it, not bouncing away. */
.checkin-fab:not(.show) {
  transition: opacity 0.18s ease, transform 0.18s ease-in;
}
.checkin-fab.show {
  opacity: 1;
  transform: scale(1) translateY(0);
  pointer-events: auto;
}
.checkin-fab:hover { background: var(--brand-dark); }
.checkin-fab svg { width: 26px; height: 26px; }

.pull-indicator {
  height: 0; overflow: hidden; display: flex; align-items: center; justify-content: center;
  transition: height 0.2s ease; color: var(--brand);
}
.pull-spinner {
  width: 24px; height: 24px; border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--brand) 25%, transparent);
  border-top-color: var(--brand);
}
.pull-indicator.ready .pull-spinner { border-top-color: var(--brand-dark); }
.pull-indicator.spinning .pull-spinner { animation: pull-spin 0.7s linear infinite; }
@keyframes pull-spin { to { transform: rotate(360deg); } }

.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--elevation-1);
  padding: 1.25rem;
  margin-bottom: 1rem;
}

h1 { font-size: 1.4rem; margin-top: 0; }
h2 { font-size: 1.05rem; }

button, .btn {
  background: var(--brand);
  color: white;
  border: none;
  border-radius: var(--radius-pill);
  padding: 0.65rem 1.3rem;
  cursor: pointer;
  font-size: 1rem;
  font-weight: 600;
  transition: background-color 0.15s ease, box-shadow 0.15s ease;
}
button:hover, .btn:hover { background: var(--brand-dark); box-shadow: var(--elevation-1); }
button:disabled { opacity: 0.5; cursor: not-allowed; box-shadow: none; }
button.secondary { background: var(--card); color: var(--brand); border: 1px solid var(--brand); }
button.big { width: 100%; padding: 1rem; font-size: 1.1rem; font-weight: 600; }

input:not([type]), input[type=text], input[type=password], input[type=tel] {
  background: var(--card);
  color: var(--text);
  padding: 0.6rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  font-size: 1rem;
  width: 100%;
  transition: border-color 0.15s ease;
}
input:not([type]):focus, input[type=text]:focus, input[type=password]:focus, input[type=tel]:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--brand) 25%, transparent);
}

label { display: block; font-size: 0.9rem; color: var(--muted); margin-bottom: 0.2rem; margin-top: 0.6rem; }

.error-text { color: var(--danger); font-size: 0.9rem; }
.error-boundary { text-align: center; }
.error-boundary p { margin-top: 0; }
.muted { color: var(--muted); font-size: 0.9rem; }
.ok-text { color: var(--ok); }

.progress-row { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; }
.progress-bar { background: var(--border); border-radius: 999px; height: 8px; overflow: hidden; margin-top: 0.3rem; }
.progress-bar-fill { background: var(--brand); height: 100%; }

.scan-box {
  border: 2px dashed var(--border);
  border-radius: var(--radius-lg);
  aspect-ratio: 1;
  max-width: 320px;
  margin: 0 auto;
  overflow: hidden;
  position: relative;
  background: black;
}

.checkin-result { text-align: center; padding: 1rem; }
.checkin-result.success { color: var(--ok); }
.checkin-result.denied { color: var(--danger); }

.cal { width: 100%; }
.cal-nav { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.6rem; }
.cal-nav button { padding: 0.3rem 0.7rem; }
.cal-weekdays, .cal-days { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.cal-weekdays span { text-align: center; font-size: 0.75rem; color: var(--muted); padding-bottom: 0.25rem; }
.cal-day {
  aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius-md); background: var(--bg); font-size: 0.85rem; position: relative;
}
.cal-day-empty { background: transparent; }
.cal-day-highlight { background: var(--brand); color: white; font-weight: 600; }
.cal-day-hasdata { box-shadow: inset 0 0 0 2px var(--brand); }
.cal-day-clickable { cursor: pointer; }
.cal-day-clickable:hover { box-shadow: inset 0 0 0 2px var(--brand-dark); }
.cal-day-badge {
  position: absolute; bottom: 2px; right: 2px; background: var(--brand-dark); color: white;
  border-radius: 999px; font-size: 0.6rem; min-width: 14px; height: 14px; line-height: 14px; text-align: center; padding: 0 2px;
}

/* Gym switcher (only rendered for students who belong to more than one gym) */
.brand-wrap { position: relative; min-width: 0; }
.brand-caret { font-size: 0.9rem; opacity: 0.85; transition: transform 0.15s ease; flex-shrink: 0; }
.brand-caret.open { transform: rotate(180deg); }
.gym-menu-overlay { position: fixed; inset: 0; z-index: 140; }
.gym-menu {
  position: absolute; top: calc(100% + 0.4rem); left: 0; z-index: 160; min-width: 14rem; max-width: 80vw;
  margin: 0; padding: 0.25rem; list-style: none;
  background: var(--card); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--radius-md); box-shadow: 0 8px 24px rgba(0,0,0,0.4);
}
.gym-menu li {
  display: flex; justify-content: space-between; align-items: center; gap: 0.75rem;
  padding: 0.65rem 0.75rem; min-height: 44px; border-radius: var(--radius-sm, 6px); cursor: pointer;
}
.gym-menu li:hover { background: color-mix(in srgb, var(--brand) 15%, transparent); }
.gym-menu li.current { font-weight: 600; color: var(--brand); }
.gym-menu-error { padding: 0.65rem 0.75rem; color: #ff8a8a; }

/* Plan-due reminder on My plan */
.banner {
  display: flex; gap: 0.6rem; align-items: flex-start;
  padding: 0.85rem 1rem; margin-bottom: 1rem; border-radius: var(--radius-md); font-weight: 600; line-height: 1.35;
}
.banner-danger { background: #b91c1c; color: #fff; box-shadow: 0 2px 10px rgba(185, 28, 28, 0.45); }
.banner-info { background: color-mix(in srgb, var(--ok) 18%, var(--card)); color: var(--text); border: 1px solid var(--ok); }
.unpaid-reminder { margin-top: 0.75rem; padding: 0.75rem 1rem; border-radius: var(--radius-md); background: #b91c1c; color: #fff; font-weight: 600; text-align: left; }

/* Closable reminder banners */
.banner .banner-text { flex: 1; }
.banner-close {
  flex-shrink: 0; margin: -0.25rem -0.4rem -0.25rem 0; padding: 0 0.6rem; min-height: 32px; min-width: 32px;
  background: transparent; color: inherit; border: none; box-shadow: none; font-size: 1.5rem; line-height: 1; cursor: pointer;
}
.banner-close:hover { background: rgba(255, 255, 255, 0.18); box-shadow: none; }

/* Class reservations */
.class-row {
  display: flex; gap: 0.75rem; align-items: center; justify-content: space-between;
  padding: 0.7rem 0; border-top: 1px solid var(--border);
}
.class-row:first-of-type { border-top: none; }
.class-row.booked .class-info strong { color: var(--brand); }
.class-info { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.class-row button { flex-shrink: 0; }
.class-tag { font-size: 0.8rem; color: var(--muted); }
.class-tag-ok { color: var(--ok); font-weight: 600; }

/* Gym (or Gym Register) logo next to the name: keeps its aspect ratio, softly rounded like an app tile. */
.brand-logo { width: auto; border-radius: 22%; flex-shrink: 0; object-fit: contain; }
