/* ============================================================
   Pump & Iron Gym Loyalty — shared design system
   Dark gym theme: near-black + gold accent, red for danger.
   ============================================================ */
:root{
  --bg:#0a0a0b;
  --bg-2:#0f1012;
  --card:#141518;
  --card-2:#1b1c20;
  --field:#151517;
  --field-2:#1c1b1f;
  --line:#26282e;
  --line-2:#3a3a40;
  --text:#f5f4f2;
  --text-soft:#ededed;
  --muted:#8a8a8e;
  --muted-2:#67666b;
  --gold:#f5b301;
  --gold-2:#ffc21a;
  --gold-3:#c98f00;
  --red:#e3322a;
  --red-dark:#c4271f;
  --green:#2fbf71;
  --amber:#f5a524;
  --radius:14px;
  --maxw:1200px;
  --font:'Inter',-apple-system,BlinkMacSystemFont,system-ui,sans-serif;
  --head:'Oswald',var(--font);
}
*{ box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html,body{ margin:0; padding:0; }
/* ---- stop the engine inflating type on its own ----
   iOS Safari and Android Chrome both "boost" font sizes inside blocks they
   judge to be wide relative to the viewport. It is meant to help desktop pages
   read on a phone, but on a layout that is already mobile-first it silently
   scales some sections and not others — which is exactly what "this section
   looks zoomed in" is. Pinned to 100% so a declared 14px renders as 14px on
   every engine. This does NOT disable the user's own font-size setting: that is
   applied by the OS on top of the computed value. */
html{ -webkit-text-size-adjust:100%; text-size-adjust:100%; }
/* ---- no pinch-zoom ----
   `pan-x pan-y` keeps every scroll and swipe (including the member app's
   pull-to-refresh, which is a vertical pan) and drops ONLY pinch-zoom and
   double-tap-zoom. Deliberately not `touch-action:none`, which would kill
   scrolling outright, and not `manipulation`, which still permits pinch on some
   engines. The viewport meta covers Android; this covers the engines that
   ignore it. Text is unaffected — the browser/OS font-size setting still
   scales the UI, because every size here is relative. */
html{ touch-action:pan-x pan-y; }
body{ touch-action:pan-x pan-y; overscroll-behavior-y:contain; }
body{
  background:var(--bg); color:var(--text); font-family:var(--font);
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
  min-height:100vh; min-height:100dvh;
}
a{ color:inherit; }
img{ max-width:100%; }
.hidden{ display:none !important; }

/* ============================================================
   LOGIN SCREEN (single-viewport app) — from PumpGym member-login
   ============================================================ */
.app{
  width:100%; max-width:460px; margin:0 auto; min-height:100vh; min-height:100dvh;
  display:flex; flex-direction:column;
  padding-top:calc(env(safe-area-inset-top) + clamp(12px,2.4vh,26px));
  padding-bottom:calc(env(safe-area-inset-bottom) + clamp(12px,2.2vh,24px));
  padding-left:calc(env(safe-area-inset-left) + 24px);
  padding-right:calc(env(safe-area-inset-right) + 24px);
}
.flex{ flex:1 1 0; min-height:clamp(6px,1.6vh,22px); }
.header{ text-align:center; flex:0 0 auto; }
.header__logo{ width:clamp(118px,22vh,min(184px,48vw)); height:auto; display:block; margin:0 auto clamp(10px,1.8vh,20px); filter:drop-shadow(0 6px 20px rgba(0,0,0,.5)); }
.eyebrow{ font-size:clamp(11px,1.5vh,13px); font-weight:600; letter-spacing:.28em; text-transform:uppercase; color:var(--muted); margin:0 0 clamp(7px,1.1vh,12px); }
.title{ font-weight:800; font-size:clamp(30px,8.4vw,46px); line-height:1.02; letter-spacing:-.01em; margin:0 0 clamp(9px,1.4vh,14px); }
.subtitle{ font-size:clamp(13px,1.95vh,16px); line-height:1.5; color:var(--muted); margin:0 auto; max-width:34ch; }

.form{ display:flex; flex-direction:column; flex:0 0 auto; }
.field{ margin-bottom:clamp(12px,2vh,20px); }
.field > label{ display:block; font-size:clamp(13px,1.85vh,15px); font-weight:500; color:var(--text-soft); margin-bottom:clamp(6px,1vh,10px); }
.input-wrap{ position:relative; display:flex; align-items:center; }
.input-wrap .lead-icon{ position:absolute; left:16px; top:50%; transform:translateY(-50%); width:19px; height:19px; color:var(--muted); pointer-events:none; }
.field input, .inp, select.inp, textarea.inp{
  width:100%; background:var(--field); border:1px solid var(--line); color:var(--text);
  border-radius:var(--radius); padding:clamp(13px,1.9vh,16px) 16px; font-size:16px; font-family:var(--font);
  outline:none; -webkit-appearance:none; appearance:none; transition:border-color .15s ease, background .15s ease;
}
.field input{ padding-left:46px; }
/* A compact variant for inputs that live INSIDE a table cell, where the full
   16px/16px control is taller than the row it sits in. 16px font is kept on
   purpose: anything smaller makes iOS Safari zoom the page on focus. */
.inp--sm{ width:auto; min-width:112px; padding:6px 10px; border-radius:10px; }
.field input::placeholder,.inp::placeholder{ color:var(--muted-2); }
.field input:focus,.inp:focus{ border-color:var(--gold); background:var(--field-2); }
.password-wrap input{ padding-right:48px; }
/* ============================================================
   Switch — an on/off setting
   ============================================================
   Built on a real <input type="checkbox"> rather than a styled <div>: it is
   focusable, operable with the keyboard, announced correctly by screen
   readers and submits like a form control, all for free. The input itself is
   visually hidden but NOT display:none, which would remove it from the tab
   order entirely.
   The whole row is the label, so the tap target is the full width — a 44px
   switch alone is a fiddly target on a phone at the counter.
   ============================================================ */
/* background-COLOR, not the `background` shorthand, and it is not transitioned.
   A shorthand whose value contains var() becomes a pending-substitution value;
   putting that in a `transition` leaves the browser unable to interpolate it and
   the used colour gets stuck at whatever it first resolved to — so the row kept
   the dark palette after a switch to light while --field itself was correctly
   white. Only the border animates, which is all the hover affordance needs. */
.switchrow{ display:flex; align-items:center; justify-content:space-between; gap:14px;
  padding:13px 14px; border:1px solid var(--line); border-radius:12px; background-color:var(--field);
  cursor:pointer; transition:border-color .15s ease; }
.switchrow:hover{ border-color:var(--line-2); }
.switchrow__t{ display:flex; flex-direction:column; gap:3px; min-width:0; }
.switchrow__l{ font-size:14px; font-weight:600; color:var(--text); }
.switchrow__d{ font-size:12px; line-height:1.4; color:var(--muted); }
.switchrow input{ position:absolute; opacity:0; width:0; height:0; }
/* The OFF track needs its own contrast, not just a tint of the row.
   At --line it measured 1.24:1 against the card — WCAG 1.4.11 asks 3:1 for a
   control's boundary, and below that an "off" switch reads as empty space
   rather than as a thing you can turn on. #6a6a73 lands at 3.41:1 on the dark
   card and its light counterpart at 3.24:1 on white. */
.switch{ position:relative; flex:0 0 46px; width:46px; height:27px; border-radius:999px;
  background-color:#6a6a73; transition:background-color .18s ease; }
html[data-theme="light"] .switch{ background-color:#8e8e98; }
.switch::after{ content:''; position:absolute; top:3px; left:3px; width:21px; height:21px; border-radius:50%;
  background-color:var(--text); transition:transform .18s ease, background-color .18s ease; }
.switchrow input:checked + .switch{ background-color:var(--gold); }
.switchrow input:checked + .switch::after{ transform:translateX(19px); background-color:#111; }
/* Focus has to be visible on the SWITCH, because the input it belongs to is
   invisible — without this the control is unusable by keyboard. */
.switchrow input:focus-visible + .switch{ box-shadow:0 0 0 3px rgba(245,179,1,.45); }
.switchrow:has(input:checked){ border-color:rgba(245,179,1,.45); }
html[data-theme="light"] .switchrow input:checked + .switch{ background-color:var(--gold-3); }
html[data-theme="light"] .switchrow input:checked + .switch::after{ background-color:#fff; }
html[data-theme="light"] .switch::after{ background-color:#fff; box-shadow:0 1px 2px rgba(0,0,0,.25); }

.toggle-pass{ position:absolute; right:8px; top:50%; transform:translateY(-50%); width:38px; height:38px; background:transparent; border:none; color:var(--muted); display:flex; align-items:center; justify-content:center; cursor:pointer; padding:0; }
.toggle-pass svg{ width:20px; height:20px; }
.row{ display:flex; align-items:center; justify-content:space-between; margin:clamp(2px,.4vh,4px) 0 clamp(14px,2.4vh,24px); flex-wrap:wrap; gap:10px; }
.checkbox{ display:flex; align-items:center; gap:10px; font-size:clamp(13px,1.85vh,15px); cursor:pointer; user-select:none; }
.checkbox input{ position:absolute; opacity:0; width:0; height:0; }
.checkbox__box{ width:20px; height:20px; border-radius:6px; border:1.5px solid var(--line-2); background:var(--field); display:flex; align-items:center; justify-content:center; flex:0 0 auto; }
.checkbox__box svg{ width:13px; height:13px; opacity:0; }
.checkbox input:checked + .checkbox__box{ background:var(--gold); border-color:var(--gold); }
.checkbox input:checked + .checkbox__box svg{ opacity:1; stroke:#111; }
.link{ font-size:clamp(13px,1.85vh,15px); text-decoration:underline; text-underline-offset:3px; text-decoration-color:var(--muted-2); cursor:pointer; }
.link:hover{ text-decoration-color:var(--text); }

.btn-login{ width:100%; background:var(--red); color:#fff; border:none; border-radius:var(--radius); padding:clamp(14px,2vh,18px); font-family:var(--font); font-size:clamp(15px,1.9vh,16px); font-weight:700; letter-spacing:.08em; text-transform:uppercase; cursor:pointer; box-shadow:0 8px 22px rgba(227,50,42,.22); transition:background .15s ease, transform .1s ease; }
.btn-login:hover{ background:var(--red-dark); }
.btn-login:active{ transform:scale(.985); }
.btn-login:disabled{ opacity:.65; cursor:not-allowed; box-shadow:none; }
.divider{ display:flex; align-items:center; gap:16px; margin:clamp(14px,2.6vh,28px) 0 clamp(10px,1.7vh,16px); color:var(--muted); font-size:clamp(12px,1.7vh,14px); flex:0 0 auto; }
.divider::before,.divider::after{ content:""; flex:1 1 auto; height:1px; background:var(--line); }
.btn-outline{ width:100%; display:flex; align-items:center; justify-content:center; gap:12px; background:transparent; border:1px solid var(--line-2); color:var(--text); border-radius:var(--radius); padding:clamp(13px,1.9vh,16px); font-family:var(--font); font-size:clamp(14px,1.85vh,15px); font-weight:600; cursor:pointer; flex:0 0 auto; }
.btn-outline:hover{ background:var(--field); border-color:#4a4a50; }
.btn-outline svg{ width:20px; height:20px; color:var(--muted); flex:0 0 auto; }
.footer-note{ margin:clamp(12px,2vh,18px) 0 0; text-align:center; font-size:clamp(12px,1.7vh,14px); line-height:1.55; color:var(--muted); flex:0 0 auto; }
.footer-note b{ color:var(--text-soft); } .footer-note a{ color:var(--gold-2); font-weight:600; text-decoration:none; }

.install-banner{ display:none; align-items:center; gap:12px; flex:0 0 auto; background:var(--field); border:1px solid var(--line); border-radius:var(--radius); padding:12px 14px; margin-bottom:clamp(10px,1.6vh,16px); }
.install-banner.show{ display:flex; }
.install-banner__icon{ width:38px; height:38px; flex:0 0 38px; border-radius:9px; display:grid; place-items:center; background:linear-gradient(145deg,#ffe08a,#f5b301 45%,#c98f00); color:#14161a; }
.install-banner__icon svg{ width:19px; height:19px; }
.install-banner__text{ flex:1 1 auto; min-width:0; }
.install-banner__title{ font-weight:600; font-size:13px; }
.install-banner__sub{ font-size:12px; color:var(--muted); margin-top:1px; }
.install-banner__btn{ flex:0 0 auto; background:var(--gold); color:#111; border:none; border-radius:9px; padding:9px 14px; font-weight:700; font-size:12.5px; cursor:pointer; }
.install-banner__close{ flex:0 0 auto; background:none; border:none; color:var(--muted-2); width:28px; height:28px; cursor:pointer; font-size:18px; }

/* ---------- Toast ---------- */
/* Frosted glass rather than a flat slab. `backdrop-filter` is already used by
   the top bar, so this is the same technique the app chrome uses, not a new
   dependency — and -webkit- is spelled out because iOS Safari still needs it,
   which is where most of these are read.

   A translucent panel only reads as glass if it is legible over whatever it
   floats above, so the tint stays heavy (.72) and the text is a solid colour.
   Where backdrop-filter is unsupported the @supports block below never applies
   and the opaque fallback here is what ships. */
/* DARK THEME (the default): a dark frosted panel. Glass matches the surface it
   floats over — a white slab on a dark page is a slab, not glass. */
.toast{
  position:fixed; left:50%; bottom:calc(env(safe-area-inset-bottom) + 24px);
  background:#24242a; color:#fff;
  padding:13px 20px; border-radius:16px; font-size:13.5px; font-weight:500;
  border:1px solid rgba(255,255,255,.16);
  z-index:9999; box-shadow:0 18px 40px rgba(0,0,0,.45), 0 2px 8px rgba(0,0,0,.25);
  opacity:0; pointer-events:none;
  max-width:calc(100% - 40px); text-align:center;
  /* Rises and settles with a small overshoot, so it reads as arriving rather
     than appearing. Opacity resolves faster than the movement, which is what
     keeps it from feeling sluggish. */
  transform:translateX(-50%) translateY(14px) scale(.94);
  transition:opacity .18s ease, transform .34s cubic-bezier(.22,1.15,.36,1);
}
.toast.show{ opacity:1; transform:translateX(-50%) translateY(-4px) scale(1); }
/* Error and success keep their own tint in both themes: they carry a matched
   background AND foreground, so they stay legible without a per-theme rule. */
.toast.error{ background:#fdecea; color:#8a1c14; border-color:rgba(138,28,20,.18); }
.toast.success{ background:#eafaf0; color:#0f5c33; border-color:rgba(15,92,51,.18); }

@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .toast{
    background:rgba(36,36,42,.62);
    -webkit-backdrop-filter:blur(20px) saturate(180%);
    backdrop-filter:blur(20px) saturate(180%);
  }
  .toast.error{ background:rgba(253,236,234,.82); }
  .toast.success{ background:rgba(234,250,240,.82); }
}

/* Somebody who has asked for less motion still gets the fade — the toast has to
   be noticeable — but nothing slides or scales. */
@media (prefers-reduced-motion: reduce){
  .toast{ transition:opacity .15s linear; transform:translateX(-50%) translateY(-4px) scale(1); }
}

/* ============================================================
   APP CHROME (staff / admin / member dashboard)
   ============================================================ */
.topbar{ position:sticky; top:0; z-index:40; background:rgba(10,10,11,.92); backdrop-filter:blur(10px); border-bottom:1px solid var(--line); }
.topbar__inner{ max-width:var(--maxw); margin:0 auto; display:flex; align-items:center; gap:12px; padding:12px 18px; padding-top:calc(env(safe-area-inset-top) + 12px); }
.topbar__brand{ display:flex; align-items:center; gap:10px; font-family:var(--head); font-weight:700; letter-spacing:.02em; }
.topbar__brand img{ width:30px; height:30px; object-fit:contain; }
.topbar__brand b{ color:var(--gold); }
.topbar__spacer{ flex:1 1 auto; }
.who{ font-size:13px; color:var(--muted); text-align:right; line-height:1.3; }
.who b{ color:var(--text); display:block; }
.container{ max-width:var(--maxw); margin:0 auto; padding:18px; padding-bottom:calc(env(safe-area-inset-bottom) + 40px); }

.tabs{ display:flex; gap:6px; overflow-x:auto; padding:10px 18px 0; max-width:var(--maxw); margin:0 auto; scrollbar-width:none; }
.tabs::-webkit-scrollbar{ display:none; }
.tab{ flex:0 0 auto; background:transparent; border:none; color:var(--muted); font-family:var(--font); font-size:14px; font-weight:600; padding:10px 14px; border-radius:10px 10px 0 0; cursor:pointer; white-space:nowrap; border-bottom:2px solid transparent; }
.tab.active{ color:var(--gold); border-bottom-color:var(--gold); }
.tab:hover{ color:var(--text); }
/* Count on a tab, for something the owner has to act on (an unpaid salary).
   Red rather than gold: gold is "selected" here, so a gold count would read as
   decoration. Both themes get a value — this sits on the app bar, which is one
   of the surfaces that hard-codes a dark colour. */
.tab__badge{ display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; margin-left:6px; padding:0 5px; border-radius:999px; background:rgba(229,52,42,.9); color:#fff; font-size:11px; font-weight:800; line-height:1; vertical-align:middle; }
.tab__badge.hidden{ display:none; }
html[data-theme="light"] .tab__badge{ background:#b3251d; color:#fff; }
.panel{ display:none; }
.panel.active{ display:block; animation:fade .2s ease; }
@keyframes fade{ from{ opacity:0; transform:translateY(4px); } to{ opacity:1; transform:none; } }

/* ---------- Cards / sections ---------- */
.card{ background:var(--card); border:1px solid var(--line); border-radius:16px; padding:18px; margin-bottom:16px; }
.card h2, .card h3{ font-family:var(--head); font-weight:600; margin:0 0 12px; letter-spacing:.02em; }
.section-title{ font-family:var(--head); font-size:20px; font-weight:600; margin:4px 0 14px; letter-spacing:.03em; }
.muted{ color:var(--muted); }
.small{ font-size:12.5px; }
.gold{ color:var(--gold); }

.grid{ display:grid; gap:14px; }
.grid.cols-2{ grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); }
.grid.cols-3{ grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); }
.grid.cols-4{ grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); }

.stat{ background:var(--card); border:1px solid var(--line); border-radius:14px; padding:16px; }
.stat__label{ font-size:12px; text-transform:uppercase; letter-spacing:.08em; color:var(--muted); }
.stat__value{ font-family:var(--head); font-size:30px; font-weight:700; margin-top:6px; }
.stat__sub{ font-size:12px; color:var(--muted); margin-top:2px; }
.stat--gold .stat__value{ color:var(--gold); }

/* ---------- Buttons ---------- */
.btn{ display:inline-flex; align-items:center; justify-content:center; gap:8px; border:1px solid var(--line-2); background:var(--field); color:var(--text); border-radius:11px; padding:12px 16px; font-family:var(--font); font-size:14.5px; font-weight:600; cursor:pointer; transition:background .15s,border-color .15s,transform .1s; }
.btn:hover{ background:var(--field-2); }
.btn:active{ transform:scale(.98); }
.btn:disabled{ opacity:.6; cursor:not-allowed; }
.btn--primary{ background:var(--gold); border-color:var(--gold); color:#111; }
.btn--primary:hover{ background:var(--gold-2); }
.btn--danger{ background:var(--red); border-color:var(--red); color:#fff; }
.btn--danger:hover{ background:var(--red-dark); }
.btn--ghost{ background:transparent; }
.btn--sm{ padding:8px 12px; font-size:13px; border-radius:9px; }
.btn--block{ width:100%; }
.btn-row{ display:flex; gap:10px; flex-wrap:wrap; }

/* ---------- Form fields (panels) ---------- */
.f{ margin-bottom:14px; }
.f > label{ display:block; font-size:13.5px; font-weight:500; color:var(--text-soft); margin-bottom:7px; }
.f-inline{ display:flex; gap:10px; flex-wrap:wrap; }
.f-inline > *{ flex:1 1 140px; }
.seg{ display:flex; gap:8px; flex-wrap:wrap; }
.seg button{ flex:1 1 auto; background:var(--field); border:1px solid var(--line); color:var(--text); border-radius:10px; padding:11px; font-weight:600; cursor:pointer; }
.seg button.active{ background:var(--gold); border-color:var(--gold); color:#111; }

/* ---------- Badges / pills ---------- */
.badge{ display:inline-flex; align-items:center; gap:6px; font-size:12px; font-weight:700; padding:4px 10px; border-radius:999px; letter-spacing:.02em; }
.badge--active{ background:rgba(47,191,113,.15); color:#67e29b; }
.badge--warn{ background:rgba(245,165,36,.15); color:var(--amber); }
.badge--expired{ background:rgba(227,50,42,.16); color:#ff7a72; }
.badge--muted{ background:var(--field-2); color:var(--muted); }
/* Paused: deliberately NOT amber or red. Amber means "act soon" and red means
   "expired"; a held membership is neither, it is parked on purpose. */
.badge--paused{ background:rgba(96,165,250,.16); color:#93c5fd; }
.badge--gold{ background:rgba(245,179,1,.15); color:var(--gold-2); }

/* ---------- Membership card (member portal) ---------- */
.mcard{ position:relative; border-radius:20px; padding:22px; overflow:hidden; background:linear-gradient(135deg,#1a1b1f 0%,#101114 60%,#0c0c0e 100%); border:1px solid #2c2d33; box-shadow:0 18px 40px rgba(0,0,0,.5); }
.mcard::before{ content:""; position:absolute; inset:0; background:radial-gradient(120% 90% at 100% 0%, rgba(245,179,1,.16), transparent 55%); pointer-events:none; }
.mcard__top{ display:flex; justify-content:space-between; align-items:flex-start; gap:14px; position:relative; }
.mcard__name{ font-family:var(--head); font-size:26px; font-weight:700; }
.mcard__id{ font-size:13px; color:var(--gold-2); letter-spacing:.06em; margin-top:2px; }
.mcard__qr{ background:#fff; border-radius:12px; padding:8px; width:96px; height:96px; display:grid; place-items:center; cursor:pointer; flex:0 0 auto; }
.mcard__qr svg{ width:100%; height:100%; }
.mcard__days{ position:relative; margin-top:18px; display:flex; align-items:baseline; gap:10px; }
.mcard__days b{ font-family:var(--head); font-size:46px; line-height:1; font-weight:700; color:var(--gold); }
.mcard__meta{ position:relative; display:flex; justify-content:space-between; gap:12px; margin-top:16px; font-size:13px; color:var(--muted); flex-wrap:wrap; }
.mcard__meta b{ color:var(--text); display:block; font-size:14px; }
.progress{ position:relative; height:8px; background:#26282e; border-radius:999px; overflow:hidden; margin-top:16px; }
.progress__fill{ position:absolute; inset:0 auto 0 0; background:linear-gradient(90deg,var(--gold),var(--gold-2)); border-radius:999px; }

/* ---------- Referral chip ---------- */
.refcode{ display:flex; align-items:center; gap:12px; background:var(--card-2); border:1px dashed var(--gold-3); border-radius:12px; padding:14px 16px; }
.refcode__code{ font-family:var(--head); font-size:22px; letter-spacing:.14em; color:var(--gold); }

/* ---------- Timeline / history ---------- */
.tl{ list-style:none; margin:0; padding:0; }
.tl__group{ font-size:12px; text-transform:uppercase; letter-spacing:.08em; color:var(--muted); margin:14px 0 8px; }
.tl__item{ display:flex; gap:12px; padding:12px 0; border-top:1px solid var(--line); }
.tl__icon{ width:36px; height:36px; flex:0 0 36px; border-radius:10px; background:var(--field-2); display:grid; place-items:center; font-size:16px; }
.tl__body{ flex:1 1 auto; min-width:0; }
.tl__title{ font-weight:600; font-size:14px; }
.tl__time{ font-size:12px; color:var(--muted); margin-top:2px; }

/* ---------- Tables ---------- */
.table-wrap{ overflow-x:auto; border:1px solid var(--line); border-radius:12px; }
/* Footer under a paged list: the "Show more" button, or the note that says the
   whole list is now on screen. Centred and outside .table-wrap on purpose — it
   belongs to the list, not to the horizontally scrolling table, so it must not
   slide out of view when a wide table is scrolled sideways on a phone. */
.mem-more{ display:flex; justify-content:center; align-items:center; padding:12px 4px 2px; }
.mem-more .btn{ min-width:160px; min-height:40px; }
table{ width:100%; border-collapse:collapse; font-size:13.5px; min-width:520px; }
th,td{ text-align:left; padding:11px 12px; border-bottom:1px solid var(--line); white-space:nowrap; }
th{ font-size:11.5px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); font-weight:600; background:var(--bg-2); }
tr:last-child td{ border-bottom:none; }
td .btn{ padding:6px 10px; font-size:12.5px; }

/* ---------- Modal ---------- */
.modal{ position:fixed; inset:0; background:rgba(0,0,0,.72); display:none; align-items:center; justify-content:center; padding:20px; z-index:100; }
.modal.open{ display:flex; }
.modal__box{ background:var(--card); border:1px solid var(--line); border-radius:18px; width:100%; max-width:440px; max-height:90vh; overflow-y:auto; padding:22px; }
.modal__box.wide{ max-width:640px; }
.modal__head{ display:flex; justify-content:space-between; align-items:center; margin-bottom:14px; }
.modal__head h3{ font-family:var(--head); margin:0; font-size:20px; }
.modal__close{ background:none; border:none; color:var(--muted); font-size:24px; cursor:pointer; line-height:1; }
.qr-big{ background:#fff; border-radius:16px; padding:16px; display:grid; place-items:center; margin:0 auto; width:min(300px,80vw); }

/* ---------- Announcement banner ---------- */
.ann{ border-radius:12px; padding:14px 16px; margin-bottom:12px; border:1px solid var(--line); background:var(--card-2); }
.ann--promo{ border-color:var(--gold-3); background:linear-gradient(135deg,rgba(245,179,1,.1),transparent); }
.ann--warning{ border-color:var(--red-dark); background:rgba(227,50,42,.08); }
.ann__title{ font-weight:700; font-size:14.5px; }
.ann__msg{ font-size:13.5px; color:var(--text-soft); margin-top:4px; }

/* ---------- Empty / skeleton ---------- */
.empty{ text-align:center; color:var(--muted); padding:34px 16px; font-size:14px; }
/* A row that is present for completeness but needs no action — someone with no
   pay day set. Dimmed rather than hidden: the owner still has to be able to put
   them ON the payroll from here, so the action cell keeps full contrast. */
.row--muted > td{ opacity:.6; }
.row--muted > td:last-child{ opacity:1; }
/* Inline amber text inside an otherwise muted caption ("3 late"). Uses the same
   token the warning badge does, so the two cannot drift apart. */
/* ============================================================
   Dashboard hierarchy
   ============================================================
   The console printed ~30 figures at one weight, so the eye had nowhere to
   land and every number shouted equally. The fix is not fewer facts, it is a
   RANK: one dominant figure per zone, everything else demoted to supporting
   text, and anything that is not a fact-you-act-on pushed behind a disclosure.

   Zone 1 "Needs you"  — verbs, and absent when there is nothing to do
   Zone 2 "Today"      — one hero number
   Zone 3 "Last 30 days" — one hero number, two supporting, one proportion bar */

/* --- Zone 1: what needs a human ------------------------------------------ */
.needs{ border-left:3px solid var(--red, #e5342a); }
.needs__head{ font-size:11.5px; font-weight:800; letter-spacing:.09em; text-transform:uppercase; color:var(--muted); margin-bottom:10px; }
.needs__items{ display:flex; flex-wrap:wrap; gap:8px; }
.needs__item{ display:inline-flex; align-items:baseline; gap:7px; background:var(--field-2); border:1px solid var(--line); border-radius:10px;
  padding:9px 13px; cursor:pointer; font-family:var(--font); font-size:13px; color:var(--text); transition:border-color .15s,background .15s; }
.needs__item:hover{ border-color:var(--line-2); background:var(--field); }
.needs__item b{ font-size:16px; font-weight:800; }
.needs__item--urgent b{ color:#ff6a60; }
html[data-theme="light"] .needs__item--urgent b{ color:#b3251d; }
/* The reassuring empty state. Deliberately a quiet line rather than a card of
   zeros: an empty inbox should look empty. */
.dash-clear{ margin:0 0 16px; color:var(--muted); font-size:13px; }

/* --- Zone 2: the day's money, one number --------------------------------- */
.card--today{ border-left:3px solid var(--gold); }
html[data-theme="light"] .card--today{ border-left-color:var(--gold-3); }
.hero__head{ display:flex; align-items:baseline; justify-content:space-between; gap:12px; margin-bottom:6px; }
.hero__label{ font-size:11.5px; font-weight:800; letter-spacing:.09em; text-transform:uppercase; color:var(--muted); }
.hero__value{ font-family:Oswald,sans-serif; font-size:38px; font-weight:700; line-height:1.05; color:var(--gold); letter-spacing:-.01em; }
html[data-theme="light"] .hero__value{ color:var(--gold-3); }
.hero__value--down{ color:#ff6a60; }
html[data-theme="light"] .hero__value--down{ color:#b3251d; }
.hero__sub{ margin-top:6px; color:var(--muted); font-size:13px; }

/* The day's parts, beside the net rather than buried in the sentence under it.
   Present and findable, visibly NOT the headline. */
.hero__row{ display:flex; align-items:flex-end; justify-content:space-between; gap:20px; flex-wrap:wrap; }
.hero__vlabel{ margin-top:2px; color:var(--muted); font-size:12.5px; }
.hero__side{ display:flex; gap:26px; }
.hero__side div{ display:flex; flex-direction:column; gap:2px; }
.hero__side b{ font-family:Oswald,sans-serif; font-size:20px; font-weight:600; }
.hero__side span{ color:var(--muted); font-size:12px; }
.hero__spent{ color:#ff6a60; }
html[data-theme="light"] .hero__spent{ color:#b3251d; }
/* A zero that is genuinely nothing, rather than a figure worth reading. */
.monthrow__note{ color:var(--muted); font-weight:600; }

/* --- Zone 3/4: the matrices ----------------------------------------------
   The same measure across several windows is TABULAR data, and a table is the
   one layout that labels every cell twice — once by its row, once by its
   column — without repeating a word. Twelve figures in a grid read faster than
   six scattered cards, which is why the answer to "too many numbers" here was
   structure rather than deletion. */
.matrix{ width:100%; border-collapse:collapse; font-size:13.5px; }
.matrix thead th{ text-align:right; font-size:11px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
  color:var(--muted); padding:0 0 9px 16px; white-space:nowrap; }
.matrix thead th:first-child{ text-align:left; padding-left:0; }
.matrix th[scope="row"]{ text-align:left; font-weight:600; color:var(--text); padding:9px 16px 9px 0; white-space:nowrap; }
.matrix td{ text-align:right; padding:9px 0 9px 16px; font-variant-numeric:tabular-nums; white-space:nowrap; }
.matrix tbody tr + tr th, .matrix tbody tr + tr td{ border-top:1px solid var(--line); }
.matrix tfoot th, .matrix tfoot td{ border-top:2px solid var(--line-2); padding-top:11px; font-weight:700; }
.matrix__na{ color:var(--muted-2); }
.matrix .split-key__dot{ display:inline-block; margin-right:7px; vertical-align:middle; }

/* The programme split as a proportion. Two numbers side by side cannot answer
   "how do these compare"; a bar answers it before the numbers are read. */
.split{ display:flex; height:9px; border-radius:999px; overflow:hidden; background:var(--field-2); }
.split span{ display:block; transition:width .25s ease; }
.split__gym{ background:var(--gold); }
.split__mt{ background:#c78bff; }
html[data-theme="light"] .split__gym{ background:var(--gold-3); }
html[data-theme="light"] .split__mt{ background:#6d28d9; }
.split-key{ display:flex; flex-wrap:wrap; gap:8px 26px; margin-top:10px; }
.split-key > div{ display:flex; align-items:baseline; gap:7px; }
.split-key b{ font-family:Oswald,sans-serif; font-size:17px; font-weight:600; }
.split-key__dot{ width:9px; height:9px; border-radius:3px; flex:0 0 auto; }
.split-key__dot--gym{ background:var(--gold); }
.split-key__dot--mt{ background:#c78bff; }
html[data-theme="light"] .split-key__dot--gym{ background:var(--gold-3); }
html[data-theme="light"] .split-key__dot--mt{ background:#6d28d9; }

@media (max-width:560px){
  .hero__value{ font-size:32px; }
  .monthrow{ gap:14px; }
  .monthrow__side{ gap:18px; }
}
.warn-ink{ color:var(--amber); font-weight:700; }
html[data-theme="light"] .warn-ink{ color:#8a5b00; }
/* The qualifier under a status badge ("4 days late", "due in 3 days"). */
.badge__sub{ margin-top:4px; }
/* Buttons at the end of a table row: never wrap, never crowd each other. */
.cell--actions{ white-space:nowrap; text-align:right; }
.cell--actions .btn + .btn{ margin-left:6px; }
.spinner{ width:22px; height:22px; border:2.5px solid var(--line-2); border-top-color:var(--gold); border-radius:50%; animation:spin .7s linear infinite; margin:24px auto; }
@keyframes spin{ to{ transform:rotate(360deg); } }

@media (prefers-reduced-motion: reduce){ *{ transition:none !important; animation:none !important; } }
@media (max-width:520px){ .stat__value{ font-size:26px; } .container{ padding:14px; } }

/* ============================================================
   THEME REFINEMENT v2 — red brand signature + gold premium
   ============================================================ */
:root{
  --brand:#e5342a;          /* signature red (from the login button) */
  --brand-2:#c4271f;
  --brand-soft:rgba(229,52,42,.14);
}
/* Primary actions = brand red across all panels (login stays red too). */
.btn--primary{ background:var(--brand); border-color:var(--brand); color:#fff; box-shadow:0 8px 22px rgba(229,52,42,.22); }
.btn--primary:hover{ background:var(--brand-2); border-color:var(--brand-2); }
.btn svg{ width:18px; height:18px; flex:0 0 auto; }
.btn-login{ background:var(--brand); box-shadow:0 8px 22px rgba(229,52,42,.24); }
.btn-login:hover{ background:var(--brand-2); }

/* Status badge dot */
.badge i.dot{ width:7px; height:7px; border-radius:50%; background:currentColor; display:inline-block; }
.badge--active{ background:rgba(34,197,94,.14); color:#38d17f; }
.badge--warn{ background:rgba(245,165,36,.16); color:#f5a524; }
.badge--expired{ background:rgba(229,52,42,.16); color:#ff6a60; }
.badge--paused{ background:rgba(96,165,250,.16); color:#7cb2f8; }

/* ---------- App bar (member dashboard) ---------- */
.appbar{ position:sticky; top:0; z-index:40; background:rgba(10,10,11,.86); backdrop-filter:blur(12px); border-bottom:1px solid rgba(255,255,255,.06);
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:14px 18px; padding-top:calc(env(safe-area-inset-top) + 14px); }
.appbar__brand{ display:flex; align-items:center; gap:11px; }
.appbar__brand img{ width:38px; height:38px; object-fit:contain; }
.wordmark{ display:flex; flex-direction:column; line-height:1; }
.wordmark b{ font-family:var(--head); font-size:19px; font-weight:700; letter-spacing:.06em; color:#fff; }
.wordmark .wm-gold{ color:var(--gold); }
.wordmark__tag{ font-size:9.5px; letter-spacing:.34em; color:var(--muted-2); margin-top:4px; text-transform:uppercase; }
.logout-btn{ display:inline-flex; align-items:center; gap:8px; background:transparent; border:1px solid var(--line-2); color:var(--text);
  border-radius:11px; padding:9px 14px; font-family:var(--font); font-size:14px; font-weight:600; cursor:pointer; transition:border-color .15s,background .15s; }
.logout-btn:hover{ border-color:var(--brand); color:#fff; background:var(--brand-soft); }
.logout-btn svg{ width:17px; height:17px; }

/* ---------- Main view + bottom nav ---------- */
.view{ max-width:560px; margin:0 auto; padding:16px 16px calc(env(safe-area-inset-bottom) + 92px); }
.page-title{ font-family:var(--head); font-size:24px; font-weight:600; letter-spacing:.02em; margin:2px 0 14px; }
.tabbar{ position:fixed; left:0; right:0; bottom:0; z-index:50; display:flex; background:rgba(12,12,14,.94); backdrop-filter:blur(12px);
  border-top:1px solid rgba(255,255,255,.07); padding-bottom:env(safe-area-inset-bottom); }
.tabbar__wrap,.tabbar{ }
.tabbtn{ flex:1 1 0; position:relative; display:flex; flex-direction:column; align-items:center; gap:5px; background:none; border:none;
  color:var(--muted); font-family:var(--font); font-size:11.5px; font-weight:600; padding:12px 4px 14px; cursor:pointer; letter-spacing:.02em; }
.tabbtn svg{ width:23px; height:23px; }
.tabbtn.active{ color:var(--gold); }
/* No indicator bar. The active tab is already said twice — the outline icon
   swaps to its filled twin and the whole button turns gold — so a third signal
   was redundant, and because it hung off the very top edge of the bar it read as
   a stray line floating above the icon rather than as part of it. It also drew
   attention to itself on every switch, which is the opposite of what a tab bar
   should do. Removed here and in the v3 block below; nothing replaces it. */

/* ---------- Membership card v2 (bg image + duotone glow) ---------- */
/* ============================================================
   Membership card — ALWAYS a dark card
   ============================================================
   THE BUG: switching to light mode made the member's name and their plan /
   started / expires values disappear. This card is not a themed surface — its
   background is a fixed dark photograph under a dark gradient, in BOTH themes.
   But its contents inherited the global --text and --muted, which flip to near
   black in light mode, so the app faithfully painted #16161a text onto a nearly
   black photo. The name measured 1.1:1.

   Rather than chase every descendant with a light-mode override (and miss the
   next one added), the card REDECLARES the palette tokens for its own subtree.
   Anything inside it therefore resolves against the dark artwork automatically,
   in either theme, including elements written later.
   ============================================================ */
/* ---- the stage: the gym's own ambience, behind the glass ----
   The photograph lives HERE, not inside the card, and the card is translucent
   on top of it. That is what makes the glass read as glass: the room is visible
   around the panel and blurred through it. Previously the photo was a layer
   INSIDE an opaque card, so there was nothing for a backdrop-filter to work on.
   The stage bleeds slightly past the view's padding so the ambience reaches the
   screen edges the way a hero image should. */
.mstage{
  position:relative; margin:0 -6px 18px; padding:13px; border-radius:30px;
  isolation:isolate; overflow:hidden;
  background:#0b0c0f;
  box-shadow:0 24px 52px rgba(0,0,0,.42);
}
.mstage__bg{
  position:absolute; inset:0; z-index:0;
  background:url('/card-bg.jpg') center/cover no-repeat;
  /* Lift the room a little so the blur has real structure to smear, then damp
     it so it never competes with the text sitting on top. */
  filter:saturate(115%) contrast(104%);
}
/* Ambience: a warm key light top-right and a cool fill bottom-left, over a
   vignette. Same two-source lighting as the brand's photography. */
/* Every alpha here was solved for, not eyeballed. The card is translucent, so
   whatever the photograph does behind it lands under the text — and this gym
   photo has ceiling lights in it. Compositing the real pixels through this stack
   and measuring the BRIGHTEST result is how the numbers below were chosen; the
   first attempt used a warmer key light and a white-tinted pane and bottomed out
   at 2.79:1 on the member's own name. */
.mstage__bg::after{
  content:""; position:absolute; inset:0;
  background:
    radial-gradient(90% 70% at 88% 4%, rgba(245,179,1,.20), transparent 58%),
    radial-gradient(85% 75% at 4% 100%, rgba(229,52,42,.18), transparent 60%),
    radial-gradient(120% 100% at 50% 50%, transparent 34%, rgba(6,6,9,.60) 100%),
    linear-gradient(180deg, rgba(8,8,11,.26), rgba(8,8,11,.58));
}
/* The ambience ring: the strip of room visible AROUND the pane. It is lit more
   brightly than the area under the pane, because nothing sits on it — the whole
   point of the stage is that you can see the gym. The pane does the contrast
   work for the text; darkening the ring as well just produced a black box with
   a card in it. */
.mstage::after{
  content:""; position:absolute; inset:0; z-index:0; pointer-events:none; border-radius:inherit;
  background:radial-gradient(60% 44% at 84% 2%, rgba(255,206,102,.20), transparent 62%);
  mix-blend-mode:screen;
}

/* ---- the glass panel itself ---- */
.mcard{ position:relative; z-index:1; border-radius:22px; padding:20px 18px;
  /* Frosted: blur what is behind, and lift saturation so the gold and red in the
     photograph still read as colour rather than grey mush. */
  -webkit-backdrop-filter:blur(22px) saturate(150%);
  backdrop-filter:blur(22px) saturate(150%);
  /* SMOKED glass, not white-tinted. A white frosting brightens whatever is
     behind it, which on a photograph containing ceiling lights pushed the
     backdrop under the member's name to rgb(167,147,102) — 2.79:1. Tinting the
     pane dark instead guarantees a floor no matter what the photo does, and
     still passes 38% of the blurred room through, which is what reads as glass.
     Measured worst case with this value: name 9.87:1, muted 6.04:1. */
  background:linear-gradient(158deg, rgba(24,26,33,.66) 0%, rgba(14,15,20,.70) 45%, rgba(10,11,15,.76) 100%);
  border:1px solid rgba(255,255,255,.16);
  /* The inset top highlight is what sells a pane of glass — light catching the
     top edge — and the inset bottom line is its thickness. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.22),
    inset 0 -1px 0 rgba(255,255,255,.05),
    0 16px 34px rgba(0,0,0,.34);
  /* The card's own palette. Fixed, because the surface underneath is fixed:
     dark artwork in BOTH themes. Without this the light theme paints near-black
     text onto a near-black photograph. */
  --text:#f7f7f5;
  --text-soft:#ededed;
  --muted:#c3c3c9;          /* lifted for a translucent surface: 8.5:1 here */
  --muted-2:#a8a8b0;
  --line:rgba(255,255,255,.14);
  --line-2:rgba(255,255,255,.22);
  --gold:#f5b301;
  --gold-2:#ffc21a;
  --gold-3:#f5b301;         /* the light-mode gold is too dark to sit on this card */
  --gold-4:#f5b301;
  color:var(--text);
}
/* A diagonal sheen across the pane, as if a window were reflected in it. */
.mcard::before{ content:""; position:absolute; inset:0; z-index:0; pointer-events:none; border-radius:inherit;
  background:linear-gradient(112deg, rgba(255,255,255,.06) 0%, rgba(255,255,255,.015) 26%, transparent 46%); }
/* SPECULAR EDGE — the single most convincing glass cue, and the one the first
   pass was missing. Real glass catches light along the top-left edge and loses
   it towards the bottom-right, so a uniform 1px border reads as a flat outline
   instead of a thickness. This is a gradient-filled border drawn with two
   masked layers (border-image cannot do rounded corners), plus a fine grain so
   the large translucent areas do not band on OLED panels. */
.mcard::after{
  content:""; position:absolute; inset:0; z-index:2; pointer-events:none; border-radius:inherit;
  padding:1px;
  background:
    linear-gradient(140deg, rgba(255,255,255,.55) 0%, rgba(255,255,255,.14) 22%, rgba(255,255,255,.03) 46%, rgba(255,255,255,.10) 78%, rgba(255,255,255,.30) 100%);
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude;
}
/* Grain. 2% opacity of monochrome noise over the pane — invisible as texture,
   but it removes the flat plastic look that large blurred areas otherwise get,
   and it hides gradient banding in the dark tones. Inline data URI so it costs
   no request and cannot be blocked by the CSP's connect-src. */
.mcard__content::before{
  content:""; position:absolute; inset:0; z-index:-1; pointer-events:none; opacity:.5; border-radius:inherit;
  background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.045'/%3E%3C/svg%3E");
}
/* No backdrop-filter (older Android WebView, Firefox with it disabled): the
   panel would be a barely-tinted sheet over a busy photo and the text would sit
   on noise. Fall back to a mostly-opaque surface — less pretty, still legible,
   which is the right way round. */
@supports not ((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px))) {
  .mcard{ background:linear-gradient(158deg, rgba(20,21,26,.90) 0%, rgba(12,13,17,.94) 100%); }
}
.mcard__content{ position:relative; z-index:1; }
.mcard__top{ display:flex; justify-content:space-between; align-items:flex-start; gap:14px; }
.mcard__idblock{ min-width:0; }
.mcard__name{ font-family:var(--head); font-size:26px; font-weight:700; letter-spacing:.02em; text-transform:uppercase;
  line-height:1.1; overflow-wrap:anywhere; }
.mcard__id{ font-size:13.5px; color:var(--muted); letter-spacing:.06em; margin-top:3px; }
.mcard__qrwrap{ display:flex; flex-direction:column; align-items:center; gap:8px; flex:0 0 auto; }
/* A WHITE island inside the dark card, in both themes — a QR code has to be
   dark-on-light to scan. So it needs its own palette: the card above sets a
   light --muted for its dark artwork, which rendered the "QR" placeholder (and
   the offline fallback text) at 2.16:1 on this white tile. */
/* A WHITE island inside the glass, in both themes — a QR code has to be
   dark-on-light to scan, so this one tile deliberately breaks the frosting. It
   keeps its own palette because the card's light --muted would render the
   placeholder text at 2:1 on white. */
.mcard__qr{ background:#fff; border-radius:14px; padding:7px; width:88px; height:88px; display:grid; place-items:center; cursor:pointer;
  border:1px solid rgba(255,255,255,.55); box-shadow:0 8px 20px rgba(0,0,0,.34);
  --text:#16161a; --muted:#6a6a73; color:var(--text); }
/* max-* as well as width/height, and this is the point rather than belt-and-braces:
   renderQR() sets an INLINE svg.style.width in pixels, which beats a class rule,
   so `width:100%` alone lost. The tile is border-box, so a 96px svg was being
   asked to sit in an 80px content box and simply overflowed the white tile —
   the QR spilled past the card edge and covered the Show QR button beneath it.
   max-width is not overridden by an inline width, so it clamps regardless of
   what any caller passes. */
.mcard__qr svg{ width:100%; height:100%; max-width:100%; max-height:100%; display:block; }
/* The "Show QR" affordance is a glass pill rather than bare text, so it reads as
   pressable against a busy photograph. */
.mcard__showqr{ display:inline-flex; align-items:center; justify-content:center;
  background:rgba(255,255,255,.10); border:1px solid rgba(255,255,255,.18); border-radius:999px;
  padding:5px 12px; color:var(--gold); font-family:var(--head); font-size:11px; font-weight:600;
  letter-spacing:.12em; cursor:pointer; -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.16);
  transition:background .15s ease, border-color .15s ease; }
.mcard__showqr:hover{ background:rgba(255,255,255,.16); border-color:rgba(255,255,255,.3); }


/* ---- nested glass panels ----
   The reference stacks panels inside the pane: each is a slightly brighter sheet
   with its own hairline, which is what gives the card depth instead of one flat
   sheet of text. Kept subtle — every extra layer costs contrast. */
.mpanel{ position:relative; border-radius:16px;
  /* A second sheet of glass ON glass: its own blur, so it frosts the pane
     beneath it rather than merely tinting it, and a darker fill so the numbers
     it holds sit on the safest surface in the card. */
  background:linear-gradient(160deg, rgba(255,255,255,.07), rgba(255,255,255,.025) 60%, rgba(0,0,0,.10));
  -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);
  border:1px solid rgba(255,255,255,.10);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.14), 0 6px 18px rgba(0,0,0,.18); }
/* The same specular edge as the pane, at half strength — panels nested inside
   glass catch less light than the outer surface. */
.mpanel::after{
  content:""; position:absolute; inset:0; pointer-events:none; border-radius:inherit; padding:1px;
  background:linear-gradient(140deg, rgba(255,255,255,.30) 0%, rgba(255,255,255,.05) 40%, rgba(255,255,255,.16) 100%);
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude;
}
.mcard__daysbox{ margin-top:18px; padding:14px 16px 16px; }
/* ---- one panel per membership (a member holding Gym AND Muay Thai) ----
   Two independent plans, so two independent clocks. The figure is smaller than
   the single-membership one because there are two of them and the card still
   has to fit a phone without scrolling to reach the referral code. */
.mcard__prog{ margin-top:12px; padding:12px 14px 14px; }
.mcard__proghead{ display:flex; align-items:baseline; justify-content:space-between; gap:10px; margin-bottom:6px; }
.mcard__progname{ font-family:var(--head); font-size:13px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--text); }
.mcard__progmeta{ font-size:11.5px; color:var(--muted); text-align:right; min-width:0; overflow-wrap:anywhere; }
.mcard__days--sm b{ font-size:38px; }
.mcard__days--sm span{ font-size:12px; }
.mcard__days{ display:flex; align-items:baseline; gap:12px; }
.mcard__days b{ font-family:var(--head); font-size:58px; line-height:.92; font-weight:700; color:var(--gold);
  text-shadow:0 2px 14px rgba(245,179,1,.28); transition:color .3s var(--ease), text-shadow .3s var(--ease); }
.mcard__days span{ font-family:var(--head); font-size:14px; letter-spacing:.14em; color:var(--muted); text-transform:uppercase; }
/* Three steps, not two. The number used to go straight from gold to red on the
   day the warning window opened; the amber step means the card starts leaning on
   the member a fortnight out, which is when renewing is still easy. */
.mcard__days--caution b{ color:#ffa93d; text-shadow:0 2px 14px rgba(255,169,61,.30); }
/* #ff8b83, not the #ff6a60 used elsewhere for "expired". The number sits inside
   a nested .mpanel, which lays another 5.5% white over the glass, and against
   THAT backdrop the darker red measured 2.87:1 — under the 3:1 floor even for
   58px type. Measured against the card alone it looked fine, which is exactly
   how the panel layer got missed the first time. This is the same red the card's
   own expired chip uses, so the palette gains nothing new. */
.mcard__days--warn b{ color:#ff8b83; text-shadow:0 2px 14px rgba(255,139,131,.34); }
.progress{ position:relative; height:7px; background:rgba(255,255,255,.14); border-radius:999px; overflow:hidden; margin-top:14px; }
/* The fill is the days REMAINING, so it starts full and drains. Width is
   transitioned so a refresh that changes the figure slides rather than jumps. */
.progress__fill{ position:absolute; inset:0 auto 0 0; background:linear-gradient(90deg,var(--gold-3),var(--gold-2)); border-radius:999px;
  box-shadow:0 0 12px rgba(245,179,1,.45);
  transition:width .55s var(--ease), background .3s var(--ease), box-shadow .3s var(--ease); }
.progress__fill--caution{ background:linear-gradient(90deg,#e08a10,#ffb43d); box-shadow:0 0 12px rgba(255,169,61,.45); }
.progress__fill--warn{ background:linear-gradient(90deg,var(--brand-2),var(--brand)); box-shadow:0 0 12px rgba(229,52,42,.5); }
/* Held: full and blue, because nothing is running down. */
.progress__fill--held{ background:linear-gradient(90deg,#4d7cfe,#7cb2f8); box-shadow:0 0 12px rgba(124,178,248,.35); }
.mcard__stats{ display:grid; grid-template-columns:repeat(3,1fr); margin-top:12px; padding:14px 4px; }
.mstat{ min-width:0; padding:0 10px; }
.mstat__label{ display:flex; align-items:center; gap:7px; font-size:10.5px; font-weight:600; letter-spacing:.08em; }
/* The icon sits in its own little glass chip, as in the reference — it separates
   the label from the value without needing a divider rule. */
.mstat__ic{ display:grid; place-items:center; flex:0 0 auto; width:22px; height:22px; border-radius:8px;
  background:rgba(255,255,255,.09); border:1px solid rgba(255,255,255,.13); color:var(--gold); }
.mstat__ic svg{ width:13px; height:13px; display:block; }
.mstat__label span{ color:var(--muted); text-transform:uppercase; }
.mstat__val{ font-weight:600; font-size:14.5px; margin-top:7px; line-height:1.3; overflow-wrap:anywhere; }
.mcard__stats .mstat:not(:first-child){ border-left:1px solid rgba(255,255,255,.10); }

/* Narrow phones: the three stats stop fitting side by side once the value is a
   full date, so they stack rather than truncate. */
@media (max-width:359px){
  .mcard__stats{ grid-template-columns:1fr; gap:12px; padding:14px 14px; }
  .mcard__stats .mstat:not(:first-child){ border-left:0; border-top:1px solid rgba(255,255,255,.10); padding-top:12px; }
  .mstat{ padding:0; }
  .mcard__days b{ font-size:50px; }
}

/* ---------- Refer-a-friend card ---------- */
.sec{ font-family:var(--head); font-size:17px; font-weight:600; letter-spacing:.06em; text-transform:uppercase; margin:0; }
.refcard__head{ display:flex; align-items:flex-start; gap:14px; margin-bottom:16px; }
.refcard__icon{ width:52px; height:52px; flex:0 0 52px; border-radius:50%; border:1.5px solid rgba(245,179,1,.5); display:grid; place-items:center; color:var(--gold); }
.refcard__icon svg{ width:24px; height:24px; }
.refbox{ display:flex; align-items:center; justify-content:space-between; gap:12px; border:1px dashed rgba(245,179,1,.4); border-radius:14px; padding:14px 16px; }
.refbox__label{ font-size:11.5px; letter-spacing:.1em; color:var(--muted); text-transform:uppercase; }
.refbox__code{ font-family:var(--head); font-size:26px; letter-spacing:.12em; color:var(--gold); margin-top:4px; }
.sec-row{ display:flex; align-items:center; justify-content:space-between; }

/* ============================================================
   Admin dashboard — density
   ============================================================
   The console showed roughly fourteen figures above the fold, several of them
   the SAME figure (all-time revenue, the combined programme total and
   "revenue 30d" were one number printed three times). The fix is not smaller
   cards, it is fewer claims: headline numbers stay, supporting numbers move
   behind a disclosure, and duplicates were deleted rather than shrunk.
   ============================================================ */

/* Head-count under the revenue it explains. Deliberately low-contrast: it is
   context for the money above it, not a competing headline. */
.progstrip{ display:flex; gap:10px; margin-top:16px; padding-top:14px; border-top:1px solid var(--line); flex-wrap:wrap; }
.progstrip > div{ flex:1 1 160px; display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; }
.progstrip__k{ font-size:11px; letter-spacing:.09em; text-transform:uppercase; color:var(--muted); font-weight:700; }
.progstrip b{ font-size:19px; font-weight:800; color:var(--text); font-family:var(--head); }
.progstrip__d{ font-size:12px; color:var(--muted); }

/* Progressive disclosure for the numbers nobody opens the console to check. */
.moredetail{ margin-bottom:16px; border:1px solid var(--line); border-radius:14px; background:var(--card); padding:0 16px; }
.moredetail[open]{ padding-bottom:16px; }
.moredetail__s{ list-style:none; cursor:pointer; padding:14px 0; font-size:13px; font-weight:700;
  letter-spacing:.06em; text-transform:uppercase; color:var(--muted); display:flex; align-items:center; gap:8px; }
.moredetail__s::-webkit-details-marker{ display:none; }
/* A chevron that rotates, so the control reads as expandable before it is used. */
.moredetail__s::after{ content:''; width:7px; height:7px; border-right:2px solid currentColor;
  border-bottom:2px solid currentColor; transform:rotate(45deg); transition:transform .18s ease; margin-left:2px; }
.moredetail[open] .moredetail__s::after{ transform:rotate(-135deg); }
.moredetail__s:hover{ color:var(--text); }

/* ---- Settings, grouped ---- */
.setgroup{ margin-bottom:26px; }
.setgroup__h{ font-family:var(--head); font-size:13px; font-weight:700; letter-spacing:.12em;
  text-transform:uppercase; color:var(--gold); margin:0 0 14px; padding-bottom:9px; border-bottom:1px solid var(--line); }
html[data-theme="light"] .setgroup__h{ color:var(--gold-4,#7a5800); }
.setgroup__switches{ display:flex; flex-direction:column; gap:10px; margin-bottom:16px; }
.linkbtn{ display:inline-flex; align-items:center; gap:3px; background:none; border:none; color:var(--gold); font-family:var(--font); font-size:14px; font-weight:600; cursor:pointer; }
.linkbtn svg{ width:16px; height:16px; }

/* ---------- Profile ---------- */
.profile-card{ text-align:center; }
.avatar{ width:74px; height:74px; border-radius:50%; margin:0 auto 12px; display:grid; place-items:center; font-family:var(--head); font-size:28px; font-weight:700; color:#111;
  background:linear-gradient(145deg,#ffe08a,var(--gold) 55%,var(--gold-3)); box-shadow:0 8px 22px rgba(245,179,1,.22); }
.profile-name{ font-family:var(--head); font-size:22px; font-weight:600; }
.kv{ display:flex; justify-content:space-between; gap:14px; padding:11px 0; border-top:1px solid var(--line); font-size:14px; }
/* :first-of-type, not :first-child — these lists now sit under an <h3 class="sec">
   section header, which would otherwise leave a stray rule between the header
   and the first row. */
.kv:first-of-type{ border-top:none; }
.kv span{ color:var(--muted); } .kv b{ text-align:right; }
.tl__time{ line-height:1.5; }

/* ============================================================
   v3 — hide scrollbar, animated tab icons, view transitions
   ============================================================ */
:root{
  --ease:cubic-bezier(.22,.61,.36,1);
  /* A slight overshoot, for the one element that should feel like it landed:
     the tab icon you just pressed. Used nowhere else — overshoot on more than
     one thing at a time reads as bounciness rather than responsiveness. */
  --ease-out-back:cubic-bezier(.34,1.4,.64,1);
}
/* Kill the "cheap" browser scrollbar everywhere (scroll still works). */
html{ scrollbar-width:none; -ms-overflow-style:none; }
html::-webkit-scrollbar, body::-webkit-scrollbar, .view::-webkit-scrollbar{ width:0; height:0; display:none; }

/* Bottom nav: the outline icon swaps to its filled twin and lifts slightly.
   That is the entire active state — no indicator bar (see .tabbtn.active above).

   The two icons are stacked in the same 24px box and cross-fade, so nothing
   reflows and the label never shifts. Only opacity and transform animate, both
   of which the compositor can run off the main thread — which is what keeps
   this smooth on the low-end Androids this app is actually used on. */
.tabbtn__ic{ position:relative; width:24px; height:24px; display:grid; place-items:center; }
.tabbtn__ic svg{ position:absolute; inset:0; margin:auto; width:24px; height:24px; }
.ic-line{ opacity:1; transform:scale(1); transition:opacity .2s var(--ease), transform .2s var(--ease); }
.ic-fill{ opacity:0; transform:scale(.72); transition:opacity .2s var(--ease), transform .2s var(--ease); }
.tabbtn.active .ic-line{ opacity:0; transform:scale(1.08); }
.tabbtn.active .ic-fill{ opacity:1; transform:scale(1); animation:iconPop .34s var(--ease-out-back); }
/* The whole button lifts a hair — the tab you are on sits a little proud of the
   ones you are not. Cheaper and quieter than a moving indicator. */
.tabbtn{ transition:color .2s var(--ease), transform .2s var(--ease); }
.tabbtn.active{ transform:translateY(-1px); }
/* Pressing gives immediately, so a tap feels answered before the view changes. */
.tabbtn:active{ transform:translateY(0) scale(.94); transition-duration:.09s; }
.tabbtn span:last-child{ transition:color .2s var(--ease); }
@keyframes iconPop{ 0%{ transform:scale(.72); } 100%{ transform:scale(1); } }

/* View switch: fade-up with a gentle per-card stagger.
   Shorter and shallower than it was (.42s/14px -> .34s/10px): a stagger you
   notice on every single tab press stops reading as polish and starts reading
   as lag, and the last card in a four-card view used to begin a quarter of a
   second after the first. The steps are tighter too, so the whole sequence
   settles in ~.44s instead of ~.65s. */
@keyframes cardIn{ from{ opacity:0; transform:translateY(10px); } to{ opacity:1; transform:none; } }
.view.animate > *{ animation:cardIn .34s var(--ease) both; }
.view.animate > *:nth-child(1){ animation-delay:0s; }
.view.animate > *:nth-child(2){ animation-delay:.05s; }
.view.animate > *:nth-child(3){ animation-delay:.1s; }
.view.animate > *:nth-child(4){ animation-delay:.15s; }
/* Anything past the fourth card is below the fold on a phone; giving it a delay
   only means it is still arriving when the member scrolls to it. */
.view.animate > *:nth-child(n+5){ animation-delay:.18s; }

/* History: custom vector icons, colour-coded by type */
.tl__icon{ font-size:0; background:rgba(245,179,1,.10); color:var(--gold); }
.tl__icon svg{ width:20px; height:20px; }
.tl__icon--first{ background:rgba(229,52,42,.13); color:#ff6a60; }
.tl__icon--renew{ background:rgba(245,179,1,.11); color:var(--gold); }
.tl__icon--bonus{ background:rgba(34,197,94,.13); color:#38d17f; }
.tl__icon--adjust{ background:var(--field-2); color:var(--muted); }

/* ============================================================
   v4 — charts, refined admin surfaces, notification/install UI
   ============================================================ */
/* ---- charts ---- */
.chart-legend{ display:flex; flex-wrap:wrap; gap:16px; margin-top:12px; font-size:12.5px; color:var(--muted); }
.chart-legend--col{ flex-direction:column; gap:9px; }
.chart-legend .lg{ display:inline-flex; align-items:center; gap:7px; }
.chart-legend .lg i{ width:10px; height:10px; border-radius:3px; flex:0 0 auto; }
.chart-legend .lg b{ color:var(--text); }
.donut-wrap{ display:flex; align-items:center; gap:22px; flex-wrap:wrap; justify-content:center; }
/* ---------- Chart hover readout ----------
   Replaces the native <svg><title> tooltips, which took about a second to
   appear, could not be styled, showed one series at a time and were invisible to
   keyboard users. The plot is focusable and the same readout is driven by arrow
   keys, so the numbers behind a chart are reachable without a mouse. */
.chart-plot{ position:relative; }
.chart-plot svg{ outline:none; }
.chart-plot svg:focus-visible{ outline:2px solid var(--gold-3); outline-offset:3px; border-radius:8px; }
.chart-tip{
  position:absolute; top:6px; z-index:3; pointer-events:none;
  background:var(--card-2); border:1px solid var(--line-2); border-radius:10px;
  padding:8px 10px; min-width:104px; font-size:12px; line-height:1.45;
  box-shadow:0 10px 26px rgba(0,0,0,.34);
}
.chart-tip[hidden]{ display:none; }
.chart-tip__d{ color:var(--muted); font-size:11px; letter-spacing:.04em; text-transform:uppercase; margin-bottom:4px; }
.chart-tip__r{ display:flex; align-items:center; gap:7px; white-space:nowrap; }
.chart-tip__r i{ width:9px; height:9px; border-radius:3px; flex:0 0 auto; }
.chart-tip__r span{ color:var(--muted); }
.chart-tip__r b{ margin-left:auto; color:var(--text); font-variant-numeric:tabular-nums; }
html[data-theme="light"] .chart-tip{ box-shadow:0 10px 26px rgba(16,17,20,.16); }

.chart-card{ background:var(--card); border:1px solid var(--line); border-radius:16px; padding:18px; margin-bottom:16px; }
.chart-head{ display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:14px; }
.chart-title{ font-family:var(--head); font-size:17px; font-weight:600; letter-spacing:.05em; text-transform:uppercase; margin:0; }
.chart-sub{ font-size:12.5px; color:var(--muted); margin:3px 0 0; }
.chip-group{ display:inline-flex; gap:4px; background:var(--bg-2); border:1px solid var(--line); border-radius:11px; padding:4px; }
.chip{ background:transparent; border:none; color:var(--muted); font-family:var(--font); font-size:12.5px; font-weight:600; padding:7px 13px; border-radius:8px; cursor:pointer; transition:background .15s,color .15s; white-space:nowrap; }
.chip:hover{ color:var(--text); }
.chip.active{ background:var(--gold); color:#111; }

/* ---- hero KPI cards ---- */
.kpi-row{ display:grid; gap:14px; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); margin-bottom:14px; }
.kpi{ position:relative; overflow:hidden; background:linear-gradient(150deg,#17181c 0%,#121317 60%,#0e0f12 100%);
  border:1px solid #262730; border-radius:18px; padding:18px 18px 16px; }
.kpi::after{ content:""; position:absolute; inset:0; pointer-events:none;
  background:radial-gradient(120% 90% at 100% 0%, rgba(245,179,1,.10), transparent 55%); }
.kpi--brand::after{ background:radial-gradient(120% 90% at 100% 0%, rgba(229,52,42,.14), transparent 55%); }
.kpi__top{ position:relative; display:flex; align-items:center; justify-content:space-between; gap:10px; }
.kpi__label{ font-size:11.5px; text-transform:uppercase; letter-spacing:.11em; color:var(--muted); }
.kpi__ic{ width:34px; height:34px; border-radius:10px; display:grid; place-items:center; flex:0 0 auto;
  background:rgba(245,179,1,.12); color:var(--gold); }
.kpi--brand .kpi__ic{ background:rgba(229,52,42,.14); color:#ff6a60; }
.kpi--good .kpi__ic{ background:rgba(34,197,94,.13); color:#38d17f; }
.kpi__ic svg{ width:18px; height:18px; }
.kpi__value{ position:relative; font-family:var(--head); font-size:34px; font-weight:700; line-height:1.05; margin-top:12px; }
.kpi--gold .kpi__value{ color:var(--gold); }
.kpi--brand .kpi__value{ color:#ff7a72; }
.kpi__sub{ position:relative; font-size:12.5px; color:var(--muted); margin-top:5px; }

/* ---- refined small stats ---- */
.stat{ transition:border-color .15s, transform .15s; }
.stat:hover{ border-color:#33343d; transform:translateY(-1px); }
.stat__label{ font-size:11px; letter-spacing:.1em; }

/* ---- section heading ---- */
.panel-head{ margin:2px 0 14px; }
.panel-head h2{ font-family:var(--head); font-size:22px; font-weight:600; letter-spacing:.03em; margin:0; }
.panel-head p{ font-size:13px; color:var(--muted); margin:5px 0 0; }

/* ---- tabs (admin/staff) refined ---- */
.tabs{ gap:4px; padding:10px 18px 2px; }
.tab{ border-radius:10px; border-bottom:none; padding:9px 14px; }
.tab.active{ background:rgba(245,179,1,.12); color:var(--gold); }
.tab:hover{ background:var(--field); }

/* ---- tables refined ---- */
table tbody tr{ transition:background .12s; }
table tbody tr:hover{ background:rgba(255,255,255,.028); }

/* ---- notification / install prompt cards ---- */
.note-card{ display:flex; gap:14px; align-items:flex-start; background:linear-gradient(135deg,rgba(245,179,1,.09),transparent 60%);
  border:1px solid rgba(245,179,1,.32); border-radius:16px; padding:16px; margin-bottom:16px; }
.note-card--brand{ background:linear-gradient(135deg,rgba(229,52,42,.10),transparent 60%); border-color:rgba(229,52,42,.32); }
.note-card__ic{ width:42px; height:42px; flex:0 0 42px; border-radius:12px; display:grid; place-items:center; background:rgba(245,179,1,.14); color:var(--gold); }
.note-card--brand .note-card__ic{ background:rgba(229,52,42,.14); color:#ff6a60; }
.note-card__ic svg{ width:21px; height:21px; }
.note-card__body{ flex:1 1 auto; min-width:0; }
.note-card__title{ font-weight:700; font-size:14.5px; }
.note-card__msg{ font-size:13px; color:var(--muted); margin-top:3px; line-height:1.5; }
.note-card__actions{ display:flex; gap:8px; margin-top:12px; flex-wrap:wrap; }

/* ---- settings rows with a state pill ---- */
.srow{ display:flex; align-items:center; justify-content:space-between; gap:12px; padding:13px 0; border-top:1px solid var(--line); }
.srow:first-of-type{ border-top:none; }
.srow__t{ font-size:14.5px; font-weight:600; }
.srow__d{ font-size:12.5px; color:var(--muted); margin-top:2px; }

/* ---- manual install steps ---- */
.steps{ margin:8px 0 0; padding-left:20px; color:var(--text-soft); font-size:14px; line-height:1.7; }
.steps li{ margin-bottom:4px; }

/* ============================================================
   v10 — light/dark theming, pull-to-refresh, busy buttons, undo
   ============================================================ */

/* ---------- Light theme ----------------------------------------------------
   Opt-in: only pages that declare <html data-theme-support="1"> get a theme
   applied (currently the member portal). The staff and admin consoles keep the
   fixed dark palette, so their charts and dense tables are unaffected.
   Almost everything follows the CSS variables below; the handful of rules that
   hard-code a dark value are corrected underneath.                          */
html[data-theme="light"]{
  --bg:#f4f4f6;
  --bg-2:#ffffff;
  --card:#ffffff;
  --card-2:#f7f7f9;
  --field:#ffffff;
  --field-2:#f2f2f5;
  --line:#e3e3e8;
  --line-2:#cdcdd5;
  --text:#16161a;
  --text-soft:#2b2b31;
  --muted:#6a6a73;
  --muted-2:#8e8e98;
  --gold-3:#9a7000;          /* readable gold for text on white (4.5:1+) */
  /* --gold-3 is measured against WHITE. On a gold TINT (the active tab) or as
     a fill carrying white text (the active segment) it lands at 3.7-4.5:1,
     just under the 4.5:1 floor for normal text — so those two cases use a
     darker step. Measured in-browser, not guessed. */
  --gold-4:#7a5800;
}
html[data-theme="light"] .appbar{ background:rgba(255,255,255,.9); border-bottom-color:var(--line); }
/* App-bar icon buttons use --field, which is pure white in the light palette —
   i.e. white-on-white against the white app bar, leaving only a hairline border.
   Give them their own tinted surface so they stay readable in both themes. */
html[data-theme="light"] .iconbtn{ background:var(--field-2); border-color:var(--line); }
html[data-theme="light"] .iconbtn:hover{ background:#eaeaef; border-color:var(--line-2); }
/* Bright gold on a near-white button is only ~1.7:1 — below the 3:1 WCAG 1.4.11
   floor for meaningful UI graphics. Use the darker gold on light backgrounds. */
html[data-theme="light"] #themeBtn,
html[data-theme="light"] .ptr{ color:var(--gold-3); }
/* The pull-to-refresh puck sits over the page, so it needs a real edge on white. */
html[data-theme="light"] .ptr{ background:#fff; border-color:var(--line-2); box-shadow:0 6px 18px rgba(16,17,20,.16); }
html[data-theme="light"] .ptr__track{ stroke:#dcdce3; }
/* The wordmark is pure white in the dark theme and vanishes on a light app bar. */
html[data-theme="light"] .wordmark b{ color:var(--text); }
html[data-theme="light"] .tabbar{ background:rgba(255,255,255,.96); border-top-color:var(--line); }
html[data-theme="light"] .progress{ background:rgba(0,0,0,.09); }
html[data-theme="light"] table tbody tr:hover{ background:rgba(0,0,0,.035); }
/* The light theme used to invert the toast to a solid near-black slab. Glass
   matches the surface it floats over instead, so the light theme keeps a light
   frosted panel — the base rule above already is one, and only the opaque
   fallback needs restating for browsers with no backdrop-filter. */
html[data-theme="light"] .toast{ background:#f6f6f7; color:#111; border-color:rgba(0,0,0,.10); }
html[data-theme="light"] .toast.error{ background:#fdecea; color:#8a1c14; }
html[data-theme="light"] .toast.success{ background:#eafaf0; color:#0f5c33; }
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  html[data-theme="light"] .toast{ background:rgba(255,255,255,.72); }
  html[data-theme="light"] .toast.error{ background:rgba(253,236,234,.78); }
  html[data-theme="light"] .toast.success{ background:rgba(234,250,240,.78); }
}
html[data-theme="light"] .header__logo{ filter:none; }
html[data-theme="light"] .modal{ background:rgba(22,22,26,.45); }
html[data-theme="light"] .card{ box-shadow:0 1px 2px rgba(16,17,20,.06); }
/* Gold text needs a darker shade to stay legible on white.
   --gold-3 measures 4.48:1 on white — a hair UNDER the 4.5 floor for normal
   text — so body-sized gold steps to --gold-4 (6.51:1). Icons, which only need
   3:1 as graphics, keep --gold-3. */
html[data-theme="light"] .wm-gold,
html[data-theme="light"] .tl__icon--renew{ color:var(--gold-3); }
html[data-theme="light"] .gold{ color:var(--gold-4); }
/* The active tab's LABEL was full-brightness gold on a white tab bar: 1.85:1 on
   11.5px text, i.e. the one word telling you where you are was the hardest word
   on the screen to read. The 3px indicator bar above it keeps --gold, because a
   3px graphic only needs 3:1 and the brand colour is worth keeping there. */
html[data-theme="light"] .tabbtn.active{ color:var(--gold-4); }
/* The referral code was still full-brightness --gold on a white card: 1.85:1,
   which is the single least readable thing on the member's home screen and
   happens to be the one string they are meant to read out to a friend. */
html[data-theme="light"] .refbox__code{ color:var(--gold-4); }
html[data-theme="light"] .refbox{ border-color:rgba(122,88,0,.45); }
/* Status colours: the dark-theme pastels wash out on a light background.
   #0f7a45 on its own 12% tint measures 4.18:1 — fine as a graphic, short of the
   4.5:1 a 12px bold label needs, so the badge text uses a darker green. */
html[data-theme="light"] .badge--active{ background:rgba(15,122,69,.12); color:#0b6238; }
html[data-theme="light"] .badge--expired{ background:rgba(179,37,29,.10); color:#b3251d; }
html[data-theme="light"] .badge--warn{ background:rgba(138,91,0,.12); color:#8a5b00; }
/* 4.6:1 on the 12% tint — a 12px bold label needs 4.5:1. */
html[data-theme="light"] .badge--paused{ background:rgba(29,78,216,.12); color:#1d4ed8; }
html[data-theme="light"] .tl__icon--first{ background:rgba(179,37,29,.10); color:#b3251d; }
html[data-theme="light"] .tl__icon--bonus{ background:rgba(15,122,69,.12); color:#0f7a45; }
/* ...but NOT on the membership card — see the single card-badge definition at
   the end of this file. The card is dark artwork in BOTH themes, so its status
   chip is defined once, after every theme override, rather than being patched
   per theme here. */
/* The membership card stays deliberately dark in both themes — it is the
   "physical card" of the product and its white text/QR must stay high contrast. */

/* ---------- Theme picker (Settings) ---------- */
.theme-seg{ display:flex; gap:6px; margin-top:12px; }
.theme-seg button{
  flex:1 1 0; display:flex; flex-direction:column; align-items:center; gap:6px;
  padding:12px 6px; border-radius:12px; cursor:pointer; font:inherit;
  font-size:12.5px; font-weight:600; letter-spacing:.01em;
  background:var(--field); border:1.5px solid var(--line); color:var(--muted);
  transition:border-color .15s, color .15s, background .15s;
}
.theme-seg button svg{ width:20px; height:20px; }
.theme-seg button:hover{ color:var(--text-soft); }
.theme-seg button.active{ border-color:var(--gold); color:var(--text); background:var(--card-2); }

/* ---------- App-bar icon buttons ---------- */
.appbar__actions{ display:flex; align-items:center; gap:8px; margin-left:auto; }
.iconbtn{
  width:38px; height:38px; flex:0 0 38px; display:grid; place-items:center;
  border-radius:11px; background:var(--field); border:1px solid var(--line);
  color:var(--text-soft); cursor:pointer; padding:0;
  transition:border-color .15s, color .15s, transform .12s;
}
.iconbtn svg{ width:18px; height:18px; }
.iconbtn:hover{ border-color:var(--line-2); color:var(--text); }
.iconbtn:active{ transform:scale(.94); }
.iconbtn:disabled{ opacity:.55; cursor:default; }
.iconbtn.is-spinning svg{ animation:spin .8s linear infinite; }

/* ---------- Pull to refresh ----------------------------------------------
   A floating circular indicator, the way a native app does it. It starts
   hidden behind the app bar, follows the finger, fills its arc as you pull,
   snaps to a spinner while refreshing, then retracts. Everything is driven
   with transform/opacity only, so it stays on the compositor and never
   reflows the page (the old version animated `height`, which pushed the
   whole view down and left its "Pull to refresh" label sitting in the
   layout whenever the stylesheet was stale).                              */
.ptr{
  position:fixed; top:0; left:50%; z-index:39;
  width:34px; height:34px; margin-left:-17px;
  display:grid; place-items:center;
  border-radius:50%; background:var(--card); border:1px solid var(--line);
  box-shadow:0 8px 22px rgba(0,0,0,.38);
  color:var(--gold);
  transform:translate3d(0,-44px,0) scale(.6); opacity:0;
  pointer-events:none; will-change:transform,opacity;
}
.ptr__svg{ width:20px; height:20px; transform:rotate(-90deg); overflow:visible; }
.ptr__track{ fill:none; stroke:var(--line-2); stroke-width:2.4; }
.ptr__arc{
  fill:none; stroke:currentColor; stroke-width:2.4; stroke-linecap:round;
  /* dasharray/offset are set from JS (circumference of r=9) */
}
/* Release/settle animation — only applied when the finger is off the glass,
   so dragging itself stays 1:1 with the finger. */
.ptr.is-animating{ transition:transform .3s var(--ease), opacity .22s linear; }
.ptr.is-armed{ border-color:var(--gold); }
.ptr.is-busy{ border-color:var(--line); }
.ptr.is-busy .ptr__svg{ animation:spin .7s linear infinite; }
@media (prefers-reduced-motion:reduce){
  .ptr.is-animating{ transition:none; }
  .ptr.is-busy .ptr__svg{ animation-duration:1.4s; }
}
/* Stop iOS from rubber-banding the document away before our gesture starts. */
html,body{ overscroll-behavior-y:contain; }

/* ---------- App-bar theme toggle ----------
   Shows the theme you'd switch TO: a sun while you're in dark, a moon while
   you're in light. */
.iconbtn__sun,.iconbtn__moon{ display:none; }
html[data-theme="dark"] .iconbtn__sun{ display:block; }
html[data-theme="light"] .iconbtn__moon{ display:block; }
/* Before the theme attribute is applied (first paint) default to the sun so the
   button is never blank. */
html:not([data-theme]) .iconbtn__sun{ display:block; }
#themeBtn{ color:var(--gold); }
#themeBtn:hover{ color:var(--gold-2, var(--gold)); }

/* ---------- Busy buttons (double-submit protection) ---------- */
.btn.is-busy, .btn-login.is-busy{ opacity:.85; cursor:progress; }
.btn:disabled, .btn-login:disabled{ opacity:.6; cursor:not-allowed; }
.spin{
  width:14px; height:14px; flex:0 0 14px; display:inline-block; border-radius:50%;
  border:2px solid currentColor; border-top-color:transparent;
  animation:spin .65s linear infinite; vertical-align:-2px;
}

/* ---------- Undo bar (staff) ---------- */
.undobar{
  position:fixed; left:50%; transform:translateX(-50%) translateY(140%);
  bottom:calc(env(safe-area-inset-bottom) + 20px); z-index:9998;
  width:min(560px, calc(100vw - 28px));
  display:flex; align-items:center; gap:12px;
  padding:13px 14px 13px 16px; border-radius:14px;
  background:var(--card-2); border:1px solid var(--line-2);
  box-shadow:0 16px 40px rgba(0,0,0,.45);
  transition:transform .26s cubic-bezier(.2,.9,.3,1.2), opacity .2s;
  opacity:0;
}
.undobar.show{ transform:translateX(-50%) translateY(0); opacity:1; }
.undobar__text{ flex:1 1 auto; font-size:13.5px; line-height:1.35; }
.undobar__text b{ color:var(--text); }
.undobar__sub{ display:block; color:var(--muted); font-size:12px; margin-top:2px; }
.undobar__btn{
  flex:0 0 auto; background:transparent; border:1.5px solid var(--gold); color:var(--gold);
  border-radius:10px; padding:9px 16px; font:inherit; font-size:13px; font-weight:700;
  cursor:pointer; letter-spacing:.02em;
}
.undobar__btn:hover{ background:rgba(245,179,1,.12); }
.undobar__btn:disabled{ opacity:.5; cursor:default; }
.undobar__close{
  flex:0 0 auto; background:none; border:none; color:var(--muted-2);
  font-size:20px; line-height:1; cursor:pointer; padding:2px 4px;
}

/* ---------- Reversed ledger entries ---------- */
.tl__item--reverted .tl__title{ text-decoration:line-through; color:var(--muted); }
.tl__item--reverted .tl__icon{ opacity:.5; }
.tag-reverted{
  display:inline-block; margin-left:6px; padding:2px 7px; border-radius:6px;
  background:var(--field-2); color:var(--muted); font-size:10.5px; font-weight:700;
  letter-spacing:.05em; text-transform:uppercase; text-decoration:none; vertical-align:1px;
}

/* ---------- Renewal reminder card (member home) ---------- */
.renew-card{
  display:flex; gap:13px; align-items:flex-start;
  border-radius:16px; padding:16px; margin-bottom:16px;
  background:rgba(245,165,36,.10); border:1px solid rgba(245,165,36,.35);
}
.renew-card--expired{ background:rgba(229,52,42,.10); border-color:rgba(229,52,42,.38); }
.renew-card__ic{
  width:38px; height:38px; flex:0 0 38px; border-radius:10px; display:grid; place-items:center;
  background:rgba(245,165,36,.16); color:var(--amber);
}
.renew-card--expired .renew-card__ic{ background:rgba(229,52,42,.16); color:#ff6a60; }
html[data-theme="light"] .renew-card--expired .renew-card__ic{ color:#b3251d; }
.renew-card__title{ font-weight:700; font-size:14.5px; margin-bottom:3px; }
.renew-card__msg{ color:var(--muted); font-size:13px; line-height:1.45; }
.renew-card__actions{ margin-top:11px; display:flex; gap:8px; flex-wrap:wrap; }

/* ============================================================
   Biometric sign-in + staff attendance (v13)
   ============================================================
   Everything here reuses the existing tokens (--gold, --line, --field-2,
   --muted) so the new surfaces inherit the light-theme overrides at the bottom
   of this file for free. */

/* ---- Biometric sign-in ----
   The member login screen presents this as a CARD, not another outline button:
   it sat directly under the red LOG IN button and read as a second, equal
   choice. As a card with its own sensor glyph, a title and a one-line promise
   it reads as the faster alternative it is — and the PIN form stays the
   obvious default. Staff and admin keep the plain .btn-bio button. */
.bio-login{ width:100%; }
.btn-bio{ display:flex; align-items:center; justify-content:center; gap:10px; width:100%; }
.btn-bio svg{ width:22px; height:22px; flex:none; }
.btn-bio #bioLoginIcon{ display:inline-flex; flex:none; }
.btn-bio.is-busy{ opacity:.6; pointer-events:none; }

.divider--cap{ font-size:11px; letter-spacing:.22em; text-transform:uppercase; }

.biocard{
  width:100%; display:flex; align-items:center; gap:14px; text-align:left;
  background:var(--card); border:1px solid var(--line); border-radius:16px;
  padding:13px 15px; cursor:pointer; font-family:var(--font); color:var(--text);
  transition:border-color .15s, background .15s, transform .1s;
}
.biocard:hover{ background:var(--card-2); border-color:var(--line-2); }
.biocard:active{ transform:scale(.99); }
.biocard:disabled,.biocard.is-busy{ opacity:.6; cursor:progress; }
.biocard__ic{
  width:50px; height:50px; flex:0 0 50px; border-radius:50%;
  display:grid; place-items:center;
  background:var(--field-2); border:1px solid var(--line);
  color:var(--brand);
}
.biocard__ic svg{ width:26px; height:26px; }
.biocard__txt{ flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:3px; }
.biocard__t{ font-size:15.5px; font-weight:700; letter-spacing:.01em; }
.biocard__d{ font-size:12.5px; color:var(--muted); }
.biocard__go{ width:17px; height:17px; flex:0 0 auto; color:var(--muted-2); }
/* Bright brand red on a near-white circle is fine, but the hairline ring
   disappears — give it a tinted face so the glyph still sits on something. */
html[data-theme="light"] .biocard__ic{ background:rgba(229,52,42,.08); border-color:rgba(229,52,42,.22); }

/* ---- registered devices list (member settings) ---- */
.devrow{ display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:11px 0; border-bottom:1px solid var(--line); }
.devrow:last-child{ border-bottom:0; }
.devrow__t{ font-weight:700; font-size:14px; display:flex; align-items:center; gap:8px; }
.devrow__d{ font-size:12px; color:var(--muted); margin-top:2px; }

/* ---- attendance chip in the staff top bar ---- */
.att-chip{ display:inline-flex; align-items:center; gap:6px; padding:5px 11px; border-radius:999px;
  font-size:12px; font-weight:700; cursor:pointer; white-space:nowrap;
  background:rgba(245,165,36,.16); color:var(--amber); border:1px solid rgba(245,165,36,.3); }
.att-chip::before{ content:""; width:7px; height:7px; border-radius:50%; background:currentColor; }
.att-chip--ok{ background:rgba(47,191,113,.15); color:#67e29b; border-color:rgba(47,191,113,.3); }
.att-chip:hover{ filter:brightness(1.1); }

/* ---- check-in sheet ---- */
.checkin-ok{ text-align:center; padding:14px 0 4px; }
.checkin-ok__tick{ width:56px; height:56px; margin:0 auto 12px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; font-size:28px; font-weight:800;
  background:rgba(47,191,113,.15); color:#67e29b; }
.checkin-ok h4{ margin:0 0 4px; font-size:17px; }
.notice{ border:1px solid var(--line); border-radius:12px; padding:12px 14px; background:var(--field-2); }
.notice--warn{ border-color:rgba(245,165,36,.35); background:rgba(245,165,36,.08); }
.attlist__row{ display:grid; grid-template-columns:1fr auto auto; gap:10px; align-items:center;
  padding:9px 0; border-bottom:1px solid var(--line); font-size:13.5px; }
.attlist__row:last-child{ border-bottom:0; }

/* ---- admin attendance report ---- */
.late{ color:var(--amber); font-weight:700; font-size:12.5px; white-space:nowrap; }
.ontime{ color:#67e29b; font-weight:600; font-size:12.5px; white-space:nowrap; }

/* Light theme: the surfaces above that hard-code a dark value need an override,
   exactly like the rest of this file (member portal only). */
html[data-theme="light"] .notice{ background:#f2f2f5; }
html[data-theme="light"] .devrow{ border-color:rgba(0,0,0,.09); }
html[data-theme="light"] .attlist__row{ border-color:rgba(0,0,0,.09); }

/* ============================================================
   v14 — two programmes (Gym / Muay Thai)
   ============================================================ */

/* ---- programme pill ----
   Deliberately an OUTLINE pill, while every status badge is filled. The two
   sit side by side in the staff and admin lists, and "outline = what kind of
   membership" / "filled = how it is doing" is readable at a glance without
   having to learn two colour codes. */
.badge--program{
  background:transparent; border:1px solid var(--line-2); color:var(--muted);
  font-size:10.5px; font-weight:700; letter-spacing:.06em; text-transform:uppercase;
}
.badge--gym{ border-color:rgba(245,179,1,.55); color:var(--gold); }
/* Muay Thai is VIOLET, not red.
   Red is already the busiest colour in this console — it means expired, it is
   the brand accent, and it is the Delete button — so a red pill on a perfectly
   healthy Muay Thai member read as a warning at a glance. The pill answers
   "what did they buy", which is never good or bad news, so it needs a hue that
   carries no status meaning at all.
   Violet is the only one left: gold is Gym, green is active, amber is expiring,
   red is expired, blue is on hold. At 271° it is at least 93° from every status
   colour and 57° from the on-hold blue, and the two are never confusable in
   practice — this pill is an outline, every status badge is filled.
   7.45:1 on the dark card, which sits between the Gym pill (9.85) and the red
   it replaces (7.20). */
.badge--mt{ border-color:rgba(199,139,255,.5); color:#c78bff; }
/* ---- holds BOTH programmes ----
   Not a third colour. Inventing one would mean learning another token, and it
   would be wrong anyway — this is not a third product, it is the two existing
   ones at once. So the pill borrows both: a border that runs from the Gym gold
   into the Muay Thai violet, with neutral text, which reads as "both" without
   competing with either. The gradient is drawn as a masked ring because a
   plain border-color cannot hold a gradient and border-image loses the radius.
   Text is --text, so it inherits whatever surface it lands on and needs no
   per-theme value of its own. */
.badge--both{ position:relative; border-color:transparent; color:var(--text); }
.badge--both::before{
  content:""; position:absolute; inset:0; border-radius:inherit; padding:1px; pointer-events:none;
  background:linear-gradient(100deg, rgba(245,179,1,.85) 0%, rgba(245,179,1,.5) 38%, rgba(199,139,255,.55) 62%, rgba(199,139,255,.9) 100%);
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor; mask-composite:exclude;
}
html[data-theme="light"] .badge--both::before{
  background:linear-gradient(100deg, rgba(154,112,0,.9) 0%, rgba(154,112,0,.55) 38%, rgba(109,40,217,.6) 62%, rgba(109,40,217,.95) 100%);
}

/* ---- programme line on the membership card ---- */
.mcard__program{
  font-size:11px; letter-spacing:.16em; text-transform:uppercase;
  color:var(--muted); margin-top:4px;
}

/* ---- sub-tabs (admin Members: All / Gym / Muay Thai) ----
   A second, quieter level below the main tab bar, so it never competes with it. */
.subtabs{ display:flex; gap:6px; flex-wrap:wrap; margin:0 0 16px; }
.subtab{
  display:inline-flex; align-items:center; gap:7px;
  background:var(--field); border:1px solid var(--line); color:var(--muted);
  font-family:var(--font); font-size:13px; font-weight:600;
  padding:8px 13px; border-radius:999px; cursor:pointer; white-space:nowrap;
  transition:background .15s, color .15s, border-color .15s;
}
.subtab:hover{ color:var(--text); border-color:var(--line-2); }
.subtab.active{ background:var(--gold); border-color:var(--gold); color:#111; }
.subtab__n{
  display:inline-grid; place-items:center; min-width:20px; height:18px; padding:0 6px;
  border-radius:999px; background:rgba(255,255,255,.09); font-size:11px; font-weight:700;
}
.subtab.active .subtab__n{ background:rgba(0,0,0,.18); }

/* ============================================================
   v14 — light theme for the STAFF and ADMIN consoles
   ============================================================
   Both consoles now default to light like the member app, so the surfaces that
   hard-code a dark value need the same treatment the member portal already had.
   Everything else follows the CSS variables and needed no change.            */

/* The console header was a near-black slab regardless of theme. */
html[data-theme="light"] .topbar{
  background:rgba(255,255,255,.92);
  border-bottom-color:var(--line);
}
/* Bright gold drops to ~1.8:1 on white — below the 3:1 floor for UI graphics
   and unreadable for the tab that tells you where you are. */
html[data-theme="light"] .topbar__brand b,
html[data-theme="light"] .stat--gold .stat__value,
html[data-theme="light"] .kpi--gold .kpi__value,
html[data-theme="light"] .refcode__code,
html[data-theme="light"] .badge--gym{ color:var(--gold-4); }
/* Text on a gold tint needs the darker step to clear 4.5:1. */
html[data-theme="light"] .tab.active{ background:rgba(245,179,1,.16); color:var(--gold-4); }
html[data-theme="light"] .badge--gym{ border-color:rgba(154,112,0,.45); }
/* The dark-theme violet is far too light for a white page (1.9:1), so light mode
   steps down to violet-700: 7.10:1 on white, in the same band as the Gym pill
   (6.51) and the on-hold badge (6.70). */
html[data-theme="light"] .badge--mt{ color:#6d28d9; border-color:rgba(109,40,217,.4); }
html[data-theme="light"] .badge--gold{ background:rgba(154,112,0,.12); color:var(--gold-3); }

/* KPI cards are a dark gradient slab; give them the card surface instead. */
html[data-theme="light"] .kpi{
  background:linear-gradient(150deg,#ffffff 0%,#fbfbfd 60%,#f7f7f9 100%);
  border-color:var(--line);
  box-shadow:0 1px 2px rgba(16,17,20,.06);
}
html[data-theme="light"] .kpi--brand .kpi__ic,
html[data-theme="light"] .kpi--brand .kpi__value,
html[data-theme="light"] .note-card--brand .note-card__ic{ color:#b3251d; }
html[data-theme="light"] .kpi--good .kpi__ic{ color:#0f7a45; }

/* The pale green used for "checked in" / "on time" is ~1.9:1 on white. */
html[data-theme="light"] .att-chip--ok,
html[data-theme="light"] .checkin-ok__tick,
html[data-theme="light"] .ontime{ color:#0f7a45; }
html[data-theme="light"] .att-chip{ color:#8a5b00; }
html[data-theme="light"] .late{ color:#8a5b00; }

/* Table headers sit on --bg-2, which is pure white in the light palette and so
   lost their separation from the rows. */
html[data-theme="light"] th{ background:var(--field-2); }
html[data-theme="light"] .chip-group{ background:var(--field-2); }
/* Gold fills that carry WHITE text use the darker step for the same reason. */
html[data-theme="light"] .chip.active{ background:var(--gold-4); color:#fff; }
html[data-theme="light"] .subtab__n{ background:rgba(0,0,0,.07); }
html[data-theme="light"] .subtab.active{ background:var(--gold-4); border-color:var(--gold-4); color:#fff; }
html[data-theme="light"] .subtab.active .subtab__n{ background:rgba(255,255,255,.22); }
html[data-theme="light"] .btn-outline:hover{ border-color:var(--line-2); }
html[data-theme="light"] .undobar{ box-shadow:0 16px 40px rgba(16,17,20,.18); }
html[data-theme="light"] .undobar__btn{ border-color:var(--gold-3); color:var(--gold-3); }
html[data-theme="light"] .seg button.active{ background:var(--gold-4); border-color:var(--gold-4); color:#fff; }

/* ============================================================
   v14 — Settings screen: disclosures instead of walls of text
   ============================================================
   The member Settings tab carried four explanatory paragraphs that pushed the
   actual controls off the screen. The help still exists — it now costs one
   line until someone asks for it. Built on <details>/<summary> so it works
   with no JavaScript and stays keyboard- and screen-reader-accessible. */
.hint{ margin-top:12px; border-top:1px solid var(--line); padding-top:11px; }
.hint > summary{
  cursor:pointer; list-style:none; display:flex; align-items:center; gap:8px;
  font-size:13px; font-weight:600; color:var(--muted);
  transition:color .15s;
}
/* Safari/Chrome draw their own triangle; we supply a chevron instead. */
.hint > summary::-webkit-details-marker{ display:none; }
.hint > summary::marker{ content:""; }
.hint > summary::after{
  content:""; margin-left:auto; width:6px; height:6px; flex:0 0 auto;
  border-right:1.7px solid currentColor; border-bottom:1.7px solid currentColor;
  transform:translateY(-2px) rotate(45deg); transition:transform .18s ease;
}
.hint[open] > summary::after{ transform:translateY(1px) rotate(-135deg); }
.hint > summary:hover{ color:var(--text); }
.hint > summary:focus-visible{ outline:2px solid var(--gold); outline-offset:3px; border-radius:6px; }
.hint__body{ font-size:12.5px; line-height:1.55; color:var(--muted); margin:9px 0 0; }

/* The "synced" pill on a registered passkey row referenced .badge--ok, which
   was never defined — so it rendered as unstyled text. */
.badge--ok{ background:rgba(47,191,113,.15); color:#67e29b; }
html[data-theme="light"] .badge--ok{ background:rgba(15,122,69,.12); color:#0b6238; }

/* ---- printed desk check-in poster (admin) ---- */

/* Print just the poster. A pop-up window would need an inline script, which the
   CSP forbids, and would not work on a phone. */
@media print {
  body * { visibility:hidden !important; }
  @page { margin:18mm; }
}

/* ============================================================
   Membership-card status chip — ONE definition, deliberately last
   ============================================================
   The global .badge--* rules are filled lozenges, and the light theme overrides
   them again for a white page. On a sheet of glass a filled block of colour
   reads as a sticker dropped on top: opacity is the one thing that cannot be
   part of a translucent surface. Inside the card the chip becomes glass too —
   the surface stays frosted and only the LED and the label carry the status
   colour, which is how a status light on a physical panel behaves.

   It lives at the END of the file on purpose. `.mcard .badge--active` is
   specificity 0-2-0 and loses to `html[data-theme="light"] .badge--active`
   (0-2-1), so scoping alone was not enough — the light theme kept winning and
   the chip rendered as the old green lozenge. Defining it once, after every
   theme block, means the card is identical in both themes (which is the whole
   point of it having a fixed surface) without a per-theme copy to keep in step.
   Contrast measured over the brightest part of the photograph: active 5.80:1,
   paused 5.24:1.
   ============================================================ */
.mcard .badge,
html[data-theme="light"] .mcard .badge{
  background:rgba(255,255,255,.09);
  border:1px solid rgba(255,255,255,.18);
  -webkit-backdrop-filter:blur(10px) saturate(140%);
  backdrop-filter:blur(10px) saturate(140%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.20), 0 2px 10px rgba(0,0,0,.22);
  font-size:10.5px; font-weight:700; letter-spacing:.14em; text-transform:uppercase;
  padding:6px 12px; border-radius:999px;
}
/* The dot is an LED: small, and it glows rather than sitting flat. */
.mcard .badge i.dot,
html[data-theme="light"] .mcard .badge i.dot{
  width:6px; height:6px; box-shadow:0 0 8px currentColor, 0 0 3px currentColor;
}
.mcard .badge--active,   html[data-theme="light"] .mcard .badge--active{ color:#5ee39b; }
.mcard .badge--warn,     html[data-theme="light"] .mcard .badge--warn{ color:#ffc65c; }
.mcard .badge--expired,  html[data-theme="light"] .mcard .badge--expired{ color:#ff8b83; }
.mcard .badge--paused,   html[data-theme="light"] .mcard .badge--paused{ color:#9dc3ff; }
.mcard .badge--muted,    html[data-theme="light"] .mcard .badge--muted{ color:var(--muted); }

/* Attendance: expandable day rows (Admin -> Attendance).
   One row per person per day; the nested row holds that day's sessions. */
.att-day { cursor: pointer; }
.att-day__caret { width: 1.2rem; color: var(--muted); user-select: none; }
.att-sessions > td { background: rgba(127,127,127,.06); padding: 0 0 6px; }
.att-sessions.hidden { display: none; }
