/* G3 Mobile — design system (Phase M, Agent A1)
 *
 * Mobile-first base styles for the member portal living at
 * members.g3makerspace.org. Sibling to the desktop staff dashboard
 * stylesheet at ui/static/css/g3.css; the two coexist and do not
 * inherit from each other. Loaded by ui/templates/members/_base_mobile.html.
 *
 * Locked decisions (memory/project_mobile_ui_plan.md):
 *   - 4-tab bottom nav (Home / Book / Bills / Me)
 *   - Light default, dark via [data-theme="dark"]
 *   - Tap targets >=44pt
 *   - 200ms ease transitions; prefers-reduced-motion zeros them
 *   - Cards over tables, 8pt radius
 *   - Buttons >=48pt tall, 6pt radius
 *
 * A2 and A3 extend this file; they MUST NOT modify the token block
 * (see file ownership in docs/phase_m_agent_briefs/README.md).
 */


/* ============================================================
 * CROSS-DOCUMENT VIEW TRANSITIONS
 * ============================================================
 * Opt the member portal into the MPA View Transitions API so tab
 * switches between pages cross-fade instead of white-flashing.
 * Chrome 126+ honors this; Safari and older browsers ignore the
 * at-rule entirely and navigate normally — no regression.
 *
 * The tab bar + top header are given persistent view-transition-name
 * values so the browser PRESERVES them across navigations instead of
 * cross-fading them. Effect: the chrome stays rock-steady while the
 * page content swaps underneath.
 *
 * Duration is shortened to 140ms (vs the browser default ~250ms) so
 * the fade feels snappy, not draggy.
 */
@view-transition { navigation: auto; }

/* Cross-fade duration for the root content group. Snappy without
 * feeling dragged. */
::view-transition-group(root),
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 140ms;
}

/* Named chrome groups (tabbar / header / tabstrip) are visually
 * identical between old and new page — so the browser's default
 * cross-fade between them shows a brief moment of partial-opacity
 * that looks like a flash. Killing the animation entirely makes the
 * chrome snap from old → new (visually a no-op since they're the
 * same) while only the root content cross-fades. Fixes the "I see
 * through the nav bar briefly" flash on tab navigation. */
::view-transition-group(g3-tabbar),
::view-transition-old(g3-tabbar),
::view-transition-new(g3-tabbar),
::view-transition-group(g3-header),
::view-transition-old(g3-header),
::view-transition-new(g3-header),
::view-transition-group(g3-tabstrip),
::view-transition-old(g3-tabstrip),
::view-transition-new(g3-tabstrip) {
  animation: none !important;
}

/* Respect the user's motion preference — kill the root transition
 * entirely for members who've set reduce-motion at the OS level. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}


/* ============================================================
 * TOKENS — light + dark mode
 * ============================================================
 * Color tokens are the public contract. Any component pattern
 * that hardcodes a hex instead of referencing a token is a bug.
 */

:root {
  /* Surface colors */
  --bg: #FFFFFF;
  --bg-elev: #F8FAFB;
  --surface: #FFFFFF;
  --border: #E5E9ED;

  /* Text */
  --text: #1F2933;
  --text-dim: #6B7280;

  /* Brand — G3 orange is the primary interactive accent.
     Sampled from the round logo ring (rgb 241/107/44 = #F16B2C).
     Used for active tab, focus rings, link colors, primary CTAs,
     hover borders — every "this is interactive" moment. The
     previous setup used PolyForm blue (#2563EB) inherited from
     the parent fork, which made every tap-able moment feel like
     a different app. (2026-05-24 brand correction.)

     --accent-hot is a slightly darker / more terracotta shade
     reserved for hero emphasis (welcome-banner avatar, install
     card hero icon, day-of-month header in the slot picker) —
     gives the UI gradation without breaking the orange palette. */
  --accent: #F16B2C;        /* G3 logo orange */
  --accent-hot: #D14E18;    /* G3 terracotta (deeper, for emphasis) */

  /* Semantic */
  --good: #15803D;
  --alert: #B91C1C;

  /* Effects */
  --shadow: 0 1px 3px rgba(0, 0, 0, 0.08);

  /* Spacing scale (rem-free; mobile-first absolute units) */
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;
  --s-7: 48px;

  /* Radii */
  --r-card: 8px;
  --r-btn: 6px;
  --r-pill: 999px;

  /* Tap target minimums (per locked decision >=44pt; primary CTAs 48pt+) */
  --tap-min: 44px;
  --tap-cta: 48px;

  /* Layout */
  --header-h: 56px;
  --tabbar-h: 56px;

  /* Motion */
  --t-fast: 120ms;
  --t-med: 200ms;
  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
}

[data-theme="dark"] {
  --bg: #0F1419;
  --bg-elev: #1A2028;
  --surface: #232A33;
  --border: #2F3641;
  --text: #E8EDF2;
  --text-dim: #9BA5B0;
  /* Dark-mode orange — lifted a touch so it pops against the dark
     surfaces but stays clearly the same brand color. */
  --accent: #FF8755;
  --accent-hot: #FF6B33;
  --good: #34D399;
  --alert: #F87171;
  --shadow: none;
}


/* ============================================================
 * RESET + BASE
 * ============================================================ */

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: 'Inter', -apple-system, 'Segoe UI', Roboto, sans-serif;
  font-size: 15px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  /* Prevent horizontal scroll from accidental overflows */
  overflow-x: hidden;
}

body {
  min-height: 100vh;
  /* Padding so fixed header + tab bar don't cover content. Children
   * that opt out (auth-flow pages with no tab bar) override via .m-shell--bare */
  padding-top: calc(var(--header-h) + env(safe-area-inset-top, 0px));
  padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
}

/* Auth-flow shell variant: no header, no tab bar, just full-bleed
 * content centered. Used by login / login_sent / auth_failed / apply
 * trio so unauthenticated visitors don't see a 4-tab nav that 401s
 * everywhere they tap. */
body.m-shell--bare {
  padding-top: env(safe-area-inset-top, 0px);
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--bg-elev);
}

a {
  color: var(--accent);
  text-decoration: none;
}
a:hover { text-decoration: underline; }

p { margin: 0 0 var(--s-3); }

h1, h2, h3, h4 {
  margin: 0 0 var(--s-3);
  color: var(--text);
  font-weight: 700;
  line-height: 1.25;
}

/* Type scale (24/18/15/13/11pt) */
h1 { font-size: 24px; }
h2 { font-size: 18px; }
h3 { font-size: 15px; }
h4 { font-size: 13px; }
small, .m-fine { font-size: 11px; color: var(--text-dim); }


/* ============================================================
 * ICONS — inline SVG with currentColor
 * ============================================================
 * The .m-icon class is set on every <svg> rendered by the
 * `_icons.html` Jinja macros. Stroke uses currentColor so the
 * icon auto-themes with surrounding text. Default 20px square;
 * variants below for the common contexts (chrome 24px, chips 16px).
 * Always non-shrinking so list rows / flex containers don't deform
 * the icon when the label is long. */

.m-icon {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  display: inline-block;
  vertical-align: -3px;   /* baseline-align with adjacent text */
}
.m-icon--lg { width: 24px; height: 24px; vertical-align: -5px; }
.m-icon--sm { width: 16px; height: 16px; vertical-align: -2px; }
.m-icon--hot { color: var(--accent-hot); }
.m-icon--accent { color: var(--accent); }
.m-icon--ok { color: var(--ok); }
.m-icon--alert { color: var(--alert); }


/* ============================================================
 * INSTALL-GATED ELEMENTS — only visible when running as PWA
 * ============================================================
 * `.m-needs-install` elements are hidden in browser mode and
 * visible in installed-PWA mode. g3-theme.js sets
 * <html data-display-mode="standalone"|"browser"> synchronously
 * before first paint.
 *
 * INVERTED LOGIC (2026-05-24 fix): we only set `display: none`
 * when hiding — never override the display value when showing.
 * Earlier version used `display: revert !important` to unhide,
 * but `revert` on an <a class="m-list__row"> reverts to the
 * user-agent default (`inline`) which broke the row's flex
 * layout. The new rules leave the visible state alone so the
 * row's natural .m-list__row {display: flex} keeps working. */
html[data-display-mode="browser"] .m-needs-install {
  display: none !important;
}
html[data-display-mode="standalone"] .m-needs-browser {
  display: none !important;
}
/* Fallback: if data-display-mode is unset (rare — only when
   matchMedia throws), default to hiding install-only affordances
   to stay conservative. The full hide rule here matches the
   browser-mode rule. */
html:not([data-display-mode]) .m-needs-install {
  display: none !important;
}


/* ============================================================
 * HEADER — 56pt, page title left, optional 1 icon action right
 * ============================================================ */

.m-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 100;
  height: calc(var(--header-h) + env(safe-area-inset-top, 0px));
  padding-top: env(safe-area-inset-top, 0px);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  /* Persistent across cross-document view transitions — top header
     stays steady while the page content swaps. Paired with .m-tabbar
     view-transition-name to eliminate the top+bottom chrome flash. */
  view-transition-name: g3-header;
}

.m-header__title {
  flex: 1;
  padding: 0 var(--s-4);
  font-size: 17px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.m-header__actions {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding-right: var(--s-3);
}

.m-header__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  color: var(--text);
  border-radius: var(--r-btn);
  background: transparent;
  border: none;
  cursor: pointer;
  font-size: 20px;
  line-height: 1;
  transition: background var(--t-fast) var(--ease);
}
.m-header__icon:hover,
.m-header__icon:focus {
  background: var(--bg-elev);
  text-decoration: none;
  outline: none;
}


/* ============================================================
 * BOTTOM TAB BAR — 4 tabs: Home / Book / Bills / Me
 * ============================================================
 * Pinned to bottom; safe-area-aware so iPhone home indicator
 * doesn't overlap. Active tab uses --accent, inactive --text-dim.
 */

.m-tabbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 100;
  height: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
  padding-bottom: env(safe-area-inset-bottom, 0px);
  background: var(--surface);
  border-top: 1px solid var(--border);
  display: flex;
  align-items: stretch;
  /* Persistent across cross-document view transitions — the tab bar
     is the same element on every page, so the browser keeps it on
     screen while the rest of the page swaps. Kills the bottom-nav
     flash on Android Chrome. */
  view-transition-name: g3-tabbar;
}

.m-tabbar__tab {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 4px 0;
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 500;
  text-decoration: none;
  min-height: var(--tap-min);
  transition: color var(--t-fast) var(--ease);
}
.m-tabbar__tab:hover { text-decoration: none; }

.m-tabbar__tab-icon {
  font-size: 22px;
  line-height: 1;
  /* Center the inline SVG inside its row (icons replaced emojis
     2026-05-24). 22px square; flex-shrink prevents narrow viewports
     from collapsing the icon when the tab squeezes. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 22px;
  margin-bottom: 2px;
}

.m-tabbar__tab--active {
  color: var(--accent);
  font-weight: 600;
}
.m-tabbar__tab--active .m-tabbar__tab-icon {
  /* Subtle "filled" cue. Emojis themselves don't have a filled
   * variant, so we use a tiny color-accent indicator dot underneath
   * via ::after. Cheap and matches iOS / Material tab patterns. */
  position: relative;
}
.m-tabbar__tab--active .m-tabbar__tab-icon::after {
  content: '';
  position: absolute;
  bottom: -2px;
  left: 50%;
  transform: translateX(-50%);
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent);
}


/* ============================================================
 * VIEW-AS BANNER (staff/admin viewing as a member)
 * ============================================================
 * Yellow strip below the header. The header is fixed; this banner
 * scrolls with the page (lives at the top of the content flow).
 */

.m-viewas {
  background: #FEF3C7;
  color: #78350F;
  border-left: 4px solid #B45309;
  padding: var(--s-3) var(--s-4);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  font-size: 13px;
}
[data-theme="dark"] .m-viewas {
  background: #3F2D08;
  color: #FCD34D;
  border-left-color: #D97706;
}
.m-viewas__exit {
  color: inherit;
  text-decoration: underline;
  white-space: nowrap;
}


/* ============================================================
 * CONTENT WRAPPER
 * ============================================================ */

.m-content {
  /* Constrain on tablets/wider but stay 100% on phones. */
  max-width: 720px;
  margin: 0 auto;
  padding: var(--s-4);
}

/* Auth-flow centering helper for the bare shell. */
.m-shell--bare .m-content {
  min-height: calc(100vh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
  display: flex;
  align-items: center;
  justify-content: center;
  max-width: none;
}


/* ============================================================
 * CARD
 * ============================================================ */

.m-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  padding: var(--s-4);
  box-shadow: var(--shadow);
  transition: border-color var(--t-fast) var(--ease);
}
a.m-card,
.m-card[role="button"] {
  display: block;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
a.m-card:hover,
.m-card[role="button"]:hover {
  border-color: var(--accent);
  text-decoration: none;
}

.m-card__label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--text-dim);
  font-weight: 600;
  margin-bottom: var(--s-2);
}
.m-card__value {
  font-size: 24px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: var(--s-1);
}
.m-card__meta {
  font-size: 13px;
  color: var(--text-dim);
}


/* ============================================================
 * BUTTON
 * ============================================================ */

.m-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: var(--tap-cta);
  padding: 0 var(--s-5);
  background: var(--accent);
  color: #FFFFFF;
  border: none;
  border-radius: var(--r-btn);
  font-family: inherit;
  font-size: 15px;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  text-decoration: none;
  transition: filter var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.m-btn:hover { filter: brightness(1.08); text-decoration: none; }
.m-btn:active { transform: scale(0.98); }

.m-btn--ghost {
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--border);
}
.m-btn--ghost:hover { background: var(--bg-elev); filter: none; }

/* Secondary action: outlined accent. Use for "Send test", "Cancel",
 * etc. — sits between the filled --cta primary and the neutral --ghost.
 * Same shape as base .m-btn so the three variants stack as a clean
 * primary / secondary / tertiary set. */
.m-btn--secondary {
  background: transparent;
  color: var(--accent);
  border: 1.5px solid var(--accent);
}
.m-btn--secondary:hover {
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  filter: none;
}

/* Danger / destructive action: alert color (red) outlined. Use for
 * "Disable notifications", "Remove device", etc. */
.m-btn--danger {
  background: transparent;
  color: var(--alert);
  border: 1.5px solid var(--alert);
}
.m-btn--danger:hover {
  background: color-mix(in srgb, var(--alert) 8%, transparent);
  filter: none;
}

.m-btn--hot   { background: var(--accent-hot); }
.m-btn--block { width: 100%; }


/* ============================================================
 * PREFERENCE LIST + TOGGLES — settings-style switches
 * ============================================================
 * Used on /notifications for the channel-enable toggles. Each row
 * has a title + description on the left and an iOS-style switch on
 * the right (built from a plain <input type=checkbox> styled with
 * appearance:none). Theme-aware via accent + border tokens. */

.m-pref-list {
  display: flex;
  flex-direction: column;
}
.m-pref-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-top: 1px solid var(--border);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.m-pref-row:first-child { border-top: none; }
.m-pref-row:active { background: var(--bg-elev); }
.m-pref-row__body {
  flex: 1;
  min-width: 0;
}
.m-pref-row__title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
}
.m-pref-row__desc {
  font-size: 13px;
  color: var(--text-dim);
  margin-top: 4px;
  line-height: 1.5;
}

/* iOS-style switch built from a checkbox. 48×28 pill, accent fill
   on checked, neutral border on unchecked. The ::before pseudo-
   element is the knob — translates 20px on checked. Animates via
   transition on background + left. */
.m-toggle {
  -webkit-appearance: none;
  appearance: none;
  position: relative;
  flex-shrink: 0;
  width: 48px;
  height: 28px;
  background: var(--border);
  border: none;
  border-radius: 999px;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease);
  margin: 0;
}
.m-toggle::before {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 24px;
  height: 24px;
  background: #FFFFFF;
  border-radius: 50%;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
  transition: left var(--t-fast) var(--ease);
}
.m-toggle:checked { background: var(--accent); }
.m-toggle:checked::before { left: 22px; }
.m-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}


/* ============================================================
 * CHIP — pill-shaped tag/badge
 * ============================================================ */

.m-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 10px;
  background: var(--bg-elev);
  color: var(--text-dim);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  font-size: 12px;
  font-weight: 600;
  line-height: 1.4;
}
.m-chip--accent { background: rgba(241, 107, 44, 0.12); color: var(--accent); border-color: transparent; }
.m-chip--good   { background: rgba(21, 128, 61, 0.1); color: var(--good);  border-color: transparent; }
.m-chip--alert  { background: rgba(185, 28, 28, 0.1); color: var(--alert); border-color: transparent; }


/* ============================================================
 * BOTTOM SHEET — slides up from bottom for secondary tasks
 * ============================================================ */

.m-sheet-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(15, 20, 25, 0.55);
  z-index: 200;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--t-med) var(--ease);
}
.m-sheet-backdrop.is-open {
  opacity: 1;
  pointer-events: auto;
}

.m-sheet {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 201;
  background: var(--surface);
  border-top-left-radius: 18px;
  border-top-right-radius: 18px;
  box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15);
  padding: var(--s-5) var(--s-4) calc(var(--s-5) + env(safe-area-inset-bottom, 0px));
  /* Closed: fully off-screen below the viewport. */
  transform: translateY(100%);
  transition: transform var(--t-med) var(--ease);
  max-height: 80vh;
  overflow-y: auto;
}
/* Open: slide up so the sheet's bottom edge sits ON TOP of the tab
   bar (the 5 nav tiles stay visible while picking hours / confirming
   a scan). The lift = tab bar height + safe-area-inset. On pre-auth
   shell pages (.m-shell--bare) the tab bar isn't rendered, so the
   --sheet-lift variable resolves to 0 and the sheet snaps to the
   viewport bottom like a classic bottom sheet.

   This deliberately positions via translateY (not `bottom`) — using
   `bottom: tabbar-h` would only partially translate the sheet
   off-screen when closed, leaving the "Book a slot" header peeking
   out above the tab bar on the booking page (real bug, May 2026). */
.m-sheet.is-open {
  transform: translateY(calc(-1 * var(--sheet-lift, 0px)));
}

/* Default lift = tab bar height (mobile authed pages). */
:root {
  --sheet-lift: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
}
/* Pre-auth shell: no tab bar to clear, so don't lift at all. */
body.m-shell--bare {
  --sheet-lift: 0px;
}
/* Desktop: tab bar is hidden, so the sheet should snap to viewport
   bottom too (matches pre-V.T. behavior). */
@media (min-width: 768px) {
  :root { --sheet-lift: 0px; }
}


/* ============================================================
 * FORM CONTROLS — touch-tuned
 * ============================================================ */

.m-field {
  display: block;
  margin-bottom: var(--s-4);
}
/* Two markup patterns supported for the visible label text:
   1. `<div class="m-field"><label>NAME</label><input ...></div>`
   2. `<label class="m-field"><span>NAME</span><input ...></label>`
   The second is more accessible (the whole row is the clickable
   label for the input). Both render identically. */
.m-field > label,
label.m-field > span:first-child {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 1px;
  margin-bottom: var(--s-2);
}
.m-input,
input[type="email"].m-input,
input[type="text"].m-input,
input[type="number"].m-input,
input[type="tel"].m-input,
textarea.m-input,
select.m-input {
  width: 100%;
  min-height: var(--tap-cta);
  padding: var(--s-2) var(--s-3);
  background: var(--bg);
  color: var(--text);
  border: 1.5px solid var(--border);
  border-radius: var(--r-btn);
  font-family: inherit;
  font-size: 16px; /* 16px to prevent iOS zoom-on-focus */
  outline: none;
  transition: border-color var(--t-fast) var(--ease);
}
.m-input:focus { border-color: var(--accent); }


/* ============================================================
 * KEY-VALUE LIST — read-only label/value pairs
 * ============================================================
 * Used on profile.html for the identity panel (Name / Email /
 * Member level / Membership). Each row is label-on-left,
 * value-on-right with a bottom border between rows. Theme-aware. */

.m-kv-list {
  margin-top: var(--s-2);
}
.m-kv-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s-3);
  padding: var(--s-3) 0;
  border-bottom: 1px solid var(--border);
}
.m-kv-row:last-child { border-bottom: none; }
.m-kv-row__key {
  flex-shrink: 0;
  min-width: 110px;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-dim);
}
.m-kv-row__val {
  flex: 1;
  text-align: right;
  font-size: 14px;
  color: var(--text);
}


/* ============================================================
 * PILL — small status badge
 * ============================================================
 * Inline color-coded badge for membership state, slot state,
 * etc. Tighter than .m-chip — meant to sit inside a row of
 * text without dominating it. */

.m-pill {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
  white-space: nowrap;
}
.m-pill--ok    { background: color-mix(in srgb, var(--ok) 14%, transparent);    color: var(--ok); }
.m-pill--warn  { background: color-mix(in srgb, #D97706 14%, transparent);     color: #B45309; }
.m-pill--alert { background: color-mix(in srgb, var(--alert) 14%, transparent); color: var(--alert); }
.m-pill--neutral { background: var(--bg-elev); color: var(--text-dim); }
[data-theme="dark"] .m-pill--warn { color: #FCD34D; }


/* ============================================================
 * NOTIFICATION HISTORY ROW
 * ============================================================
 * Used on /notifications/history. Each row = one delivered (or
 * attempted) notification. Left column shows when it happened
 * (via datetime12 filter), right column is the notification
 * card with title + channel pill + body + optional deep link. */

.m-notif-row {
  display: flex;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
}
.m-notif-row--bordered { border-bottom: 1px solid var(--border); }
.m-notif-row__time {
  flex-shrink: 0;
  width: 96px;
  font-size: 11px;
  color: var(--text-dim);
  font-weight: 600;
  line-height: 1.4;
}
.m-notif-row__body {
  flex: 1;
  min-width: 0;
}
.m-notif-row__head {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  align-items: center;
  margin-bottom: 4px;
}
.m-notif-row__title {
  color: var(--text);
  font-size: 14px;
  font-weight: 700;
  flex-shrink: 0;
}
.m-notif-row__text {
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.55;
}
.m-notif-row__link {
  display: inline-block;
  margin-top: 6px;
  color: var(--accent);
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
}
.m-notif-row__link:hover { text-decoration: underline; }

/* On very narrow screens, drop the timestamp column under the body
   so wrapping doesn't squish the content too tight. */
@media (max-width: 380px) {
  .m-notif-row {
    flex-direction: column;
    gap: 4px;
  }
  .m-notif-row__time {
    width: auto;
  }
}


/* ============================================================
 * CHIP-PICK — checkbox grid styled as pill toggles
 * ============================================================
 * Used on profile.html for the "Interests" multi-select. Each
 * option is a wrapping label with a hidden checkbox and a visible
 * pill that flips to accent-tinted when checked. Mobile-friendly:
 * easy tap target, wraps gracefully. */

.m-chip-pick {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  margin-top: var(--s-2);
}
.m-chip-pick__opt {
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.m-chip-pick__opt > input[type="checkbox"] {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}
.m-chip-pick__opt > span {
  display: inline-block;
  padding: 8px 14px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 500;
  background: var(--bg-elev);
  color: var(--text);
  border: 1.5px solid var(--border);
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.m-chip-pick__opt > input[type="checkbox"]:checked + span {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--accent);
  border-color: var(--accent);
  font-weight: 600;
}
.m-chip-pick__opt > input[type="checkbox"]:focus-visible + span {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}


/* ============================================================
 * MAKES GALLERY — Phase N "Post a Make"
 * ============================================================
 * Photo grid for the Make tab + member portfolio. Mobile-first
 * (2-up) with a wider 3-up at >=480px. Each tile is square-ish
 * with caption overlay at the bottom.
 *
 * .m-fab — floating action button anchored bottom-right above the
 * tab bar. Camera icon, opens the composer. Only shown on the
 * gallery page (positioned absolute over content; the page sets
 * the parent to position:relative). */

.m-gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--s-3);
}
@media (min-width: 480px) {
  .m-gallery {
    grid-template-columns: repeat(3, 1fr);
  }
}
@media (min-width: 768px) {
  .m-gallery {
    grid-template-columns: repeat(4, 1fr);
    gap: var(--s-4);
  }
}

.m-gallery__tile {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--r-card);
  background: var(--bg-elev);
  text-decoration: none;
  color: var(--text);
  aspect-ratio: 1 / 1;
  transition: transform var(--t-fast) var(--ease);
}
.m-gallery__tile:hover { text-decoration: none; }
.m-gallery__tile:active { transform: scale(0.985); }
.m-gallery__tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.m-gallery__tile--featured {
  /* Subtle terracotta ring + glow for featured spots */
  outline: 2px solid var(--accent-hot);
  outline-offset: -2px;
}

.m-gallery__featured-badge {
  position: absolute;
  top: 8px;
  left: 8px;
  background: var(--accent-hot);
  color: #FFFFFF;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  padding: 3px 8px;
  border-radius: 999px;
  z-index: 2;
}

.m-gallery__status {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  /* The .m-pill base styles already set padding/radius/font; this
     just positions it. */
}

.m-gallery__caption {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  padding: var(--s-3) var(--s-3) var(--s-2);
  background: linear-gradient(to top,
              rgba(0, 0, 0, 0.75) 0%,
              rgba(0, 0, 0, 0.4) 50%,
              transparent 100%);
  color: #FFFFFF;
}
.m-gallery__caption-text {
  font-size: 12px;
  font-weight: 500;
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.m-gallery__caption-by {
  font-size: 10px;
  opacity: 0.8;
  font-weight: 600;
  margin-top: 2px;
}


/* ============================================================
 * FLOATING ACTION BUTTON — gallery composer entry point
 * ============================================================
 * Anchored to bottom-right, just above the tab bar. Tap → camera
 * composer. Single-purpose: only the gallery page uses it. */

.m-fab {
  position: fixed;
  right: var(--s-4);
  bottom: calc(var(--tabbar-h) + var(--s-4) + env(safe-area-inset-bottom, 0px));
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--accent);
  color: #FFFFFF;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
  z-index: 90;
  transition: transform var(--t-fast) var(--ease),
              box-shadow var(--t-fast) var(--ease);
}
.m-fab:hover {
  text-decoration: none;
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.3);
}
.m-fab:active {
  transform: scale(0.94);
}


/* ============================================================
 * MAKE COMPOSER — photo preview surface
 * ============================================================ */

.m-make__preview {
  border-radius: var(--r-card);
  overflow: hidden;
  background: var(--bg-elev);
}
.m-make__preview img {
  width: 100%;
  height: auto;
  display: block;
  max-height: 400px;
  object-fit: cover;
}


/* ============================================================
 * UTILITIES
 * ============================================================ */

.m-stack > * + * { margin-top: var(--s-4); }
.m-row { display: flex; align-items: center; gap: var(--s-3); }
.m-row--between { justify-content: space-between; }
.m-row--wrap    { flex-wrap: wrap; }

.m-mute { color: var(--text-dim); }
.m-strong { font-weight: 600; color: var(--text); }

.m-safe-bottom { padding-bottom: env(safe-area-inset-bottom, 0px); }
.m-safe-top    { padding-top: env(safe-area-inset-top, 0px); }


/* ============================================================
 * MOTION — global transition reset for reduced-motion users
 * ============================================================ */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0ms !important;
    animation-duration: 0ms !important;
    animation-iteration-count: 1 !important;
  }
}


/* ============================================================
 * RESPONSIVE — desktop chrome (≥768px)
 * ============================================================
 * Single template tree. Mobile chrome (.m-header + .m-tabbar) is
 * the default; desktop chrome (.m-chrome-desktop) is hidden on
 * narrow viewports. At ≥768px the mobile chrome is hidden and
 * desktop chrome shows. Phase L's existing member-portal pill nav
 * (_portal_nav.html) is preserved inside .m-chrome-desktop with
 * a simplified G3-branded header (no staff-dash nav leakage). */

/* Default state (<768px): desktop chrome hidden. */
.m-chrome-desktop { display: none; }

.m-desktop-header {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.m-desktop-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  text-decoration: none;
  color: var(--text);
}

.m-desktop-brand__logo {
  width: 32px;
  height: 32px;
  border-radius: 50%;
}

.m-desktop-brand__name {
  font-weight: 600;
  font-size: 16px;
  letter-spacing: 0.2px;
}


@media (min-width: 768px) {
  /* Show desktop chrome at tablet-landscape / laptop / desktop. */
  .m-chrome-desktop { display: block; }

  /* Hide mobile chrome at the same widths. */
  .m-header,
  .m-tabbar {
    display: none;
  }

  /* Without the fixed mobile header + tab bar, body padding for
     them is wasted space. Reset it. The view-as banner + desktop
     chrome + content all flow naturally. */
  body {
    padding-top: 0;
    padding-bottom: 0;
  }

  /* Content column widens a bit on desktop — still readable line
     lengths but doesn't waste the extra real estate. */
  .m-content {
    max-width: 960px;
  }
}


/* ============================================================
 * TWIN SUB-TREE GATES — per-tab desktop/mobile partials
 * ============================================================
 * Phase M responsive pattern (Agent A2). Each tab template includes
 * BOTH its _desktop.html and _mobile.html partials wrapped in
 * .layout-desktop / .layout-mobile divs. CSS hides whichever isn't
 * appropriate for the viewport. Identical breakpoint to the chrome
 * gates above (768px). See docs/phase_m_agent_briefs/A2_*.md.
 */

/* Default state: phones get mobile partial, hide desktop partial. */
.layout-desktop { display: none; }
.layout-mobile  { display: block; }

@media (min-width: 768px) {
  .layout-desktop { display: block; }
  .layout-mobile  { display: none; }
}


/* ============================================================
 * HYBRID TAB STRIP — Book a machine ↔ Log filament (Phase N)
 * ============================================================
 * Looks like a segmented control but each "tab" is a real <a> link
 * to its own URL. No JS, no shared partials — just two pages that
 * paint the same strip with a different `active` value. Replaces
 * the earlier sub-tab-with-JS approach (2026-05-24, Shawn picked
 * hybrid after testing).
 */

.m-tabstrip {
  display: flex;
  gap: 0;
  margin: 0 0 var(--s-4);
  /* Persistent across cross-document view transitions — both
     /reservations and /filament render the same strip, so the
     browser keeps it on screen while the page content swaps.
     Without this, the strip cross-fades during navigation and
     looks like it flashes empty for ~140ms. */
  view-transition-name: g3-tabstrip;
}

.m-tabstrip__tab {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  padding: var(--s-2) var(--s-3);
  background: var(--bg-elev);
  color: var(--text-dim);
  border: 1px solid var(--border);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease);
  -webkit-tap-highlight-color: transparent;
}
.m-tabstrip__tab:hover { text-decoration: none; }
.m-tabstrip__tab:first-child {
  border-top-left-radius: var(--r-btn);
  border-bottom-left-radius: var(--r-btn);
  border-right-width: 0;
}
.m-tabstrip__tab:last-child {
  border-top-right-radius: var(--r-btn);
  border-bottom-right-radius: var(--r-btn);
}
.m-tabstrip__tab--active {
  background: var(--accent);
  color: #FFFFFF;
  border-color: var(--accent);
}
.m-tabstrip__tab:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}


/* ============================================================
 * FILTER PILLS — machine-type filter under Book a machine
 * ============================================================
 * Horizontal row of tappable pills (with overflow-x scroll on
 * very narrow viewports). Uniform style; active fills brand
 * orange per locked decision. Default = "All".
 */

.m-pill-filter {
  display: flex;
  gap: var(--s-2);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  padding: var(--s-2) 0 var(--s-3);
  /* Hide scrollbar on iOS/Chrome — the cards have an obvious "more"
     fade indicator from the layout itself. */
  scrollbar-width: none;
}
.m-pill-filter::-webkit-scrollbar { display: none; }

.m-pill-filter__pill {
  flex: 0 0 auto;
  min-height: 36px;
  padding: 0 var(--s-3);
  background: var(--bg-elev);
  color: var(--text-dim);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  font: 500 13px / 36px inherit;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--t-fast) var(--ease),
              color var(--t-fast) var(--ease),
              border-color var(--t-fast) var(--ease);
  -webkit-tap-highlight-color: transparent;
}
.m-pill-filter__pill--active {
  background: var(--accent);
  color: #FFFFFF;
  border-color: var(--accent);
  font-weight: 600;
}
.m-pill-filter__pill:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}


/* ============================================================
 * BILL OUTSTANDING — accent variant of .m-card for the
 *   "you owe $X" callout on /bills mobile
 * ============================================================ */

.m-bill-outstanding {
  background: linear-gradient(180deg, #FFF5EE 0%, var(--surface) 100%);
  border-left: 4px solid var(--accent-hot);
}
[data-theme="dark"] .m-bill-outstanding {
  background: linear-gradient(180deg, #2A1810 0%, var(--surface) 100%);
}


/* ============================================================
 * BOOK TAB — hourly grid, segmented control, slot cells
 * ============================================================ */

/* Segmented control: Today / Week / Month */
.m-segmented {
  display: inline-flex;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: 2px;
  gap: 0;
  width: 100%;
  max-width: 360px;
}
.m-segmented__opt {
  flex: 1 1 0;
  padding: 8px 14px;
  border: none;
  background: transparent;
  color: var(--text-dim);
  border-radius: var(--r-pill);
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  text-align: center;
  text-decoration: none;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.m-segmented__opt--active {
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow);
}
.m-segmented__opt:hover { text-decoration: none; }

/* Machine block + hourly slot grid */
.m-machine-block {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  padding: var(--s-3);
  margin-bottom: var(--s-4);
  box-shadow: var(--shadow);
}
.m-machine-block__title {
  font-weight: 600;
  font-size: 15px;
  color: var(--text);
  margin-bottom: var(--s-2);
}
.m-slot-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
  gap: 6px;
}
.m-slot {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 56px;
  padding: 6px 4px;
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: var(--r-btn);
  font-family: inherit;
  font-size: 12px;
  font-weight: 600;
  color: var(--text);
  cursor: pointer;
  text-decoration: none;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
}
.m-slot:hover { border-color: var(--accent); text-decoration: none; }
.m-slot__hour { font-size: 13px; line-height: 1.1; }
.m-slot__label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-dim);
  font-weight: 600;
  margin-top: 2px;
}
.m-slot--booked {
  background: var(--bg-elev);
  color: var(--text-dim);
  cursor: not-allowed;
  pointer-events: none;
}
.m-slot--mine {
  background: var(--accent-hot);
  border-color: var(--accent-hot);
  color: #FFFFFF;
}
.m-slot--mine .m-slot__label { color: rgba(255, 255, 255, 0.85); }
.m-slot--held {
  background: repeating-linear-gradient(45deg, var(--bg-elev), var(--bg-elev) 4px, var(--border) 4px, var(--border) 8px);
  color: var(--text-dim);
  cursor: not-allowed;
  pointer-events: none;
}
.m-slot--closed {
  background: var(--bg);
  color: var(--text-dim);
  cursor: not-allowed;
  pointer-events: none;
  opacity: 0.45;
  border-style: dashed;
}
.m-slot--closed .m-slot__label::after {
  content: " ·";
}

/* ============================================================
 * DAY PICKER + DAY HEADER — per-machine day-of-week selector
 * ============================================================
 * Each machine block gets a native <select> that lists the next
 * ~14 open days. Selecting one swaps the visible .m-day-section.
 * The big date header inside each section makes the chosen day
 * obvious; the day number is rendered in --accent-hot (G3
 * terracotta) for maximum visual weight. */

.m-day-picker__wrap {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: var(--s-2) 0 var(--s-3);
}
.m-day-picker__label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--text-dim);
  font-weight: 600;
}
.m-day-picker {
  appearance: none;
  -webkit-appearance: none;
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: var(--r-btn);
  color: var(--text);
  font-family: inherit;
  font-size: 15px;
  font-weight: 600;
  padding: 12px 36px 12px 14px;
  cursor: pointer;
  /* Down chevron — flat SVG inlined so it stays theme-aware
     when the user flips dark mode. */
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%23999' stroke-width='2'><path d='M1 1l5 5 5-5'/></svg>");
  background-repeat: no-repeat;
  background-position: right 14px center;
  background-size: 12px;
}
.m-day-picker:focus {
  outline: none;
  border-color: var(--accent);
}

/* Prominent date header — focal point of the per-day view. */
.m-day-header {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
  margin: var(--s-2) 0 var(--s-3);
  padding-bottom: var(--s-2);
  border-bottom: 1px solid var(--border);
}
.m-day-header__weekday {
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
}
.m-day-header__month {
  font-size: 16px;
  font-weight: 500;
  color: var(--text-dim);
}
.m-day-header__day {
  /* The terracotta hero number — biggest type on the block. */
  font-size: 28px;
  font-weight: 800;
  color: var(--accent-hot);
  line-height: 1;
}
.m-day-header__year {
  font-size: 14px;
  color: var(--text-dim);
  font-weight: 500;
}

/* When a day section is hidden via [hidden] attribute, fully
   collapse it (the default HTML5 [hidden] would already do this
   but we set display:none explicitly to override any inherited
   flex/grid context from .m-machine-block). */
.m-day-section[hidden] {
  display: none !important;
}

/* Stepper for "How long?" — 1 / 2 / 3 / 4 / 4+ hr */
.m-stepper {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}
.m-stepper__opt {
  flex: 1 1 60px;
  min-width: 56px;
  min-height: var(--tap-min);
  padding: 8px 10px;
  background: var(--bg-elev);
  border: 1.5px solid var(--border);
  border-radius: var(--r-btn);
  color: var(--text);
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  text-align: center;
  transition: border-color var(--t-fast) var(--ease), background var(--t-fast) var(--ease);
}
.m-stepper__opt:hover { border-color: var(--accent); }
.m-stepper__opt--active {
  background: var(--accent);
  border-color: var(--accent);
  color: #FFFFFF;
}


/* ============================================================
 * ME TAB — avatar, settings rows
 * ============================================================ */

.m-avatar {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  background: var(--accent);
  color: #FFFFFF;
  font-size: 28px;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-transform: uppercase;
  letter-spacing: 1px;
  flex-shrink: 0;
}

.m-list {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  overflow: hidden;
}
.m-list__row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  min-height: var(--tap-cta);
  color: var(--text);
  text-decoration: none;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  font-family: inherit;
  font-size: 15px;
  background: var(--surface);
  border-left: none;
  border-right: none;
  border-top: none;
  width: 100%;
  text-align: left;
  transition: background var(--t-fast) var(--ease);
}
.m-list__row:last-child { border-bottom: none; }
.m-list__row:hover { background: var(--bg-elev); text-decoration: none; }
.m-list__row-icon {
  width: 28px;
  font-size: 20px;
  text-align: center;
  line-height: 1;
  /* Centering for inline SVG icons inside this slot */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
  flex-shrink: 0;
}
.m-list__row:hover .m-list__row-icon { color: var(--accent); }
.m-list__row--danger .m-list__row-icon { color: var(--alert); }
.m-list__row-label { flex: 1; }
.m-list__row-value {
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 600;
}
.m-list__row--danger { color: var(--alert); }
.m-list__row--danger:hover { background: rgba(185, 28, 28, 0.06); }

/* Horizontal chip row (e.g. training chips) */
.m-chip-row {
  display: flex;
  gap: var(--s-2);
  overflow-x: auto;
  padding-bottom: var(--s-2);
  scrollbar-width: thin;
  -webkit-overflow-scrolling: touch;
}
.m-chip-row .m-chip {
  flex-shrink: 0;
  white-space: nowrap;
}
.m-chip--faded {
  opacity: 0.65;
  border-style: dashed;
}


/* ============================================================
 * ONBOARDING — 3-card swipe tutorial
 * ============================================================ */

.m-onboarding {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  padding: var(--s-5) var(--s-4);
}
.m-onboarding__cards {
  flex: 1;
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  gap: var(--s-4);
}
.m-onboarding__cards::-webkit-scrollbar { display: none; }
.m-onboarding__card {
  flex: 0 0 100%;
  scroll-snap-align: center;
  /* `scroll-snap-stop: always` forces the browser to stop at EVERY
     snap point even on a high-velocity swipe. Without this, a fast
     flick could skip card 2 entirely and jump from card 1 to card 3
     — the "mandatory" snap type only guarantees we LAND on a snap
     point at gesture end, not that we visit each one. */
  scroll-snap-stop: always;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--s-5) var(--s-3);
}
.m-onboarding__card-emoji {
  /* Hero icon at the top of each onboarding card. Was a literal
     64px emoji; now a 112px inline SVG via the icons.* macro.
     Thicker stroke override (3 instead of the default 2) so the
     icon doesn't look anemic at this size. Theme-aware terracotta
     fill via currentColor. */
  color: var(--accent-hot);
  line-height: 1;
  margin-bottom: var(--s-5);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.m-onboarding__card-emoji svg {
  /* MUST override the global .m-icon { width: 20px; height: 20px }
     rule — CSS always wins over SVG width/height attributes, so the
     `size=112` parameter from the icon macro is otherwise ignored.
     Setting an explicit size here lets onboarding render the icons
     as proper hero illustrations. */
  width: 112px !important;
  height: 112px !important;
  stroke-width: 1.6;   /* relative to viewBox 24 — visually balanced at 112px */
}
.m-onboarding__skip {
  position: fixed;
  top: calc(var(--s-4) + env(safe-area-inset-top, 0px));
  right: var(--s-4);
  z-index: 10;
  color: var(--text-dim);
  font-size: 14px;
  text-decoration: none;
  padding: 6px 10px;
}


/* ============================================================
 * GENERAL-PURPOSE COMPONENTS — promoted from log_filament.html
 * (Phase M.5 polish, 2026-05-23)
 * ============================================================ */

/* Flash messages — used inline above forms/lists to surface
 * route-level notice/error params. Three variants. */
.m-flash {
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-card);
  margin-bottom: var(--s-4);
  border-left: 4px solid var(--text-dim);
  background: var(--bg-elev);
  color: var(--text);
}
.m-flash--good  { border-left-color: var(--good); }
.m-flash--alert { border-left-color: var(--alert); }

/* Section divider with optional inline text label. Pair of
 * pseudo-elements draws the lines on either side of any inner
 * text content. */
.m-divider {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  color: var(--text-dim);
  margin: var(--s-5) 0;
  font-size: 13px;
}
.m-divider::before,
.m-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* Cost / total preview pill — neutral readonly card used before
 * a confirm step to show pricing. (Filament log, future booking
 * confirm, etc.) */
.m-cost-preview {
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  padding: var(--s-3) var(--s-4);
  text-align: center;
  color: var(--text-dim);
}
.m-cost-preview strong {
  color: var(--text);
  font-size: 18px;
}

/* Heavier CTA-style button variant. Use for primary "complete
 * this important action" buttons. Slightly taller, accent fill,
 * bold weight. */
.m-btn--cta {
  background: var(--accent);
  color: #fff;
  min-height: var(--tap-cta);
  font-weight: 600;
  font-size: 16px;
}


/* ============================================================
 * WELCOME BANNER — home-screen hero card
 * ============================================================
 * Replaces the plain "<h1>Welcome, Shawn</h1>" sitting on the page
 * background. Now a proper card with:
 *   - G3 monogram avatar on the left (terracotta circle)
 *   - Time-aware greeting ("Good morning" / "Good afternoon" /
 *     "Good evening") above the member's first name
 *   - Membership chip pill below name (PRO / STANDARD / etc.)
 *   - Subtle terracotta accent strip on the top edge — signals
 *     "this is G3" without yelling */

.m-welcome {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  padding: var(--s-4);
  margin-bottom: var(--s-4);
  box-shadow: var(--shadow);
  overflow: hidden;
}
/* Top accent strip — pure terracotta in light mode, slightly lifted
   in dark so it pops against the dark card. Decorative only. */
.m-welcome::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: linear-gradient(90deg,
              var(--accent-hot) 0%,
              var(--accent-hot) 60%,
              transparent 100%);
  border-radius: var(--r-card) var(--r-card) 0 0;
}
.m-welcome__row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}
.m-welcome__avatar {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: var(--accent-hot);
  color: #FFFFFF;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  font-size: 22px;
  letter-spacing: 0.5px;
  flex-shrink: 0;
  /* Subtle inner ring to feel substantial vs flat */
  box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.15);
}
.m-welcome__body {
  flex: 1;
  min-width: 0;
}
.m-welcome__greeting {
  font-size: 13px;
  color: var(--text-dim);
  font-weight: 500;
  line-height: 1.2;
  margin: 0;
}
.m-welcome__name {
  font-size: 22px;
  font-weight: 700;
  color: var(--text);
  line-height: 1.15;
  margin: 2px 0 0;
  /* Wrap long names cleanly on narrow screens */
  word-break: break-word;
}
.m-welcome__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: var(--s-2);
}
.m-welcome__chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.6px;
}
.m-welcome__chip--level {
  /* Terracotta-tinted pill for the membership tier — uses the G3
     orange family. rgba alpha keeps the pill background subtle on
     both light and dark surfaces. */
  background: rgba(241, 107, 44, 0.12);
  color: var(--accent-hot);
}
.m-welcome__chip--expires {
  /* Neutral "expires in N days" pill */
  background: var(--bg-elev);
  color: var(--text-dim);
}
.m-welcome__chip--warn {
  background: rgba(217, 119, 6, 0.14);
  color: #B45309;
}
[data-theme="dark"] .m-welcome__chip--warn {
  color: #FCD34D;
}


/* ============================================================
 * ANNOUNCEMENT CARDS — admin-pushed announcements on Home
 * ============================================================
 * Two variants: standard (terracotta accent, dismissible) and
 * emergency (alert-red accent, undismissible). Both use mobile
 * theme tokens so dark mode adapts automatically. */

.m-announcement {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  margin-bottom: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-card);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent-hot);
  background: var(--bg-elev);
  color: var(--text);
}
.m-announcement--emergency {
  border-left-color: var(--alert);
  background: color-mix(in srgb, var(--alert) 8%, var(--bg-elev));
}
.m-announcement__icon {
  flex-shrink: 0;
  font-size: 22px;
  line-height: 1;
  color: var(--accent-hot);   /* terracotta for standard megaphone */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
}
.m-announcement--emergency .m-announcement__icon {
  color: var(--alert);        /* alert red for emergency octagon */
}
.m-announcement__body {
  flex: 1;
  min-width: 0;
}
.m-announcement__title {
  display: block;
  margin-bottom: 4px;
  font-weight: 600;
  color: var(--text);
}
.m-announcement__urgent {
  color: var(--alert);
  font-weight: 700;
}
.m-announcement__text {
  font-size: 14px;
  line-height: 1.55;
  color: var(--text);
}
.m-announcement__link {
  display: inline-block;
  margin-top: var(--s-2);
  color: var(--accent);
  font-size: 14px;
  text-decoration: none;
  font-weight: 600;
}
.m-announcement__dismiss {
  flex-shrink: 0;
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 22px;
  padding: 0 var(--s-2);
  line-height: 1;
}
.m-announcement__dismiss:hover { color: var(--text); }


/* ============================================================
 * TILE — tappable dashboard card on the Home tab
 * ============================================================
 * Used for the 4 main destinations (Bills / Reservations / Usage
 * / Profile). Clear tap affordance via: visible border + arrow
 * chevron + active state. Whole tile is the click target. */

.m-tile {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: var(--s-4);
  min-height: 130px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-card);
  text-decoration: none;
  color: inherit;
  position: relative;
  transition: border-color 120ms ease, transform 120ms ease;
}
.m-tile:hover,
.m-tile:focus-visible {
  border-color: var(--accent);
  text-decoration: none;
}
.m-tile:active {
  transform: scale(0.985);
}
.m-tile::after {
  content: "›";
  position: absolute;
  top: var(--s-3);
  right: var(--s-3);
  color: var(--text-dim);
  font-size: 22px;
  line-height: 1;
}
.m-tile__label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--text-dim);
  font-weight: 600;
}
.m-tile__primary {
  font-size: 28px;
  font-weight: 700;
  color: var(--text);
  margin: var(--s-2) 0 0;
}
.m-tile__primary--sm {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  margin: var(--s-2) 0 0;
}
.m-tile__secondary {
  font-size: 13px;
  color: var(--text-dim);
}
.m-tile__cta {
  font-size: 13px;
  color: var(--accent);
  font-weight: 600;
  margin-top: var(--s-1);
}
.m-tile__warn  { color: var(--alert); font-weight: 600; }
.m-tile__soon  { color: var(--accent-hot); font-weight: 600; }

/* Grid container for the dashboard tiles. */
.m-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--s-3);
  margin: 0 0 var(--s-5);
}


/* ============================================================
 * STATUS BANNER — pending / rejected applicant messages
 * ============================================================
 * Single-color-strip card used for application-status surfaces
 * (waiver-pending nudge, rejected message). Theme-aware. */

.m-status-banner {
  border-radius: var(--r-card);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent-hot);
  background: var(--bg-elev);
  color: var(--text);
  padding: var(--s-3) var(--s-4);
  margin-bottom: var(--s-5);
}
.m-status-banner--alert {
  border-left-color: var(--alert);
}
.m-status-banner__title {
  font-weight: 700;
  color: var(--text);
  display: block;
  margin-bottom: var(--s-2);
}
.m-status-banner__text {
  margin: 0;
  color: var(--text-dim);
  font-size: 14px;
  line-height: 1.55;
}


/* ============================================================
 * NOTIFICATION NUDGE — "you have X unread" link
 * ============================================================ */

.m-notif-nudge {
  display: block;
  margin-bottom: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-card);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  background: var(--bg-elev);
  color: var(--text);
  font-size: 13px;
  text-decoration: none;
}
.m-notif-nudge:hover {
  border-color: var(--accent);
  text-decoration: none;
}


/* ============================================================
 * INSTALL CARD — PWA install nudge as an inline card
 * ============================================================
 * Card-style nudge sitting at the top of <main> on member pages.
 * Replaced the original strip-banner pattern (which felt like a
 * cookie consent bar) with a proper card that fits the rest of
 * the design system. Theme-aware via mobile tokens. */

.m-install-card {
  display: none; /* JS flips to flex when shown */
  align-items: flex-start;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
  padding: var(--s-4);
  border-radius: var(--r-card);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  background: var(--bg-elev);
  color: var(--text);
}

.m-install-card__icon {
  flex-shrink: 0;
  font-size: 32px;
  line-height: 1;
  color: var(--accent-hot);   /* terracotta for the install hero icon */
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.m-install-card__body {
  flex: 1;
  min-width: 0;
}

.m-install-card__title {
  display: block;
  margin-bottom: 4px;
  font-weight: 700;
  color: var(--text);
  font-size: 16px;
}

.m-install-card__text {
  font-size: 14px;
  line-height: 1.55;
  color: var(--text-dim);
  margin-bottom: var(--s-3);
}

.m-install-card__actions {
  display: flex;
  gap: var(--s-3);
  align-items: center;
  flex-wrap: wrap;
}

.m-install-card__cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-cta);
  padding: 0 var(--s-4);
  background: var(--accent);
  color: #fff;
  border-radius: var(--r-btn);
  text-decoration: none;
  font-weight: 600;
  font-size: 15px;
}
.m-install-card__cta:hover {
  text-decoration: none;
  filter: brightness(1.08);
}

.m-install-card__dismiss {
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 14px;
  padding: var(--s-2) var(--s-3);
  text-decoration: underline;
}
.m-install-card__dismiss:hover { color: var(--text); }

/* Already installed (Android Chrome + desktop PWAs hit this; iOS
 * is caught by the JS navigator.standalone check). */
@media (display-mode: standalone) {
  .m-install-card { display: none !important; }
}

/* At desktop widths the install nudge is less useful — desktop
 * Chrome/Edge offer their own install icon in the address bar,
 * and members on desktop typically aren't installing PWAs. */
@media (min-width: 768px) {
  .m-install-card { display: none !important; }
}


/* ============================================================
 * PULL-TO-REFRESH — installed-PWA equivalent of the browser's
 *   native swipe-down-to-refresh. Phase M.5 polish.
 *   JS lives in ui/static/js/g3-ptr.js.
 * ============================================================ */

.m-ptr {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  pointer-events: none;
  z-index: 50;
  overflow: hidden;
  transition: height 180ms ease;
}

.m-ptr__spinner {
  width: 28px;
  height: 28px;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  margin-bottom: var(--s-3);
  opacity: 0;
  transform: translateY(-8px) rotate(0deg);
  transition: opacity 120ms ease, transform 120ms ease;
}

.m-ptr.is-pulling .m-ptr__spinner,
.m-ptr.is-refreshing .m-ptr__spinner {
  opacity: 1;
  transform: translateY(0) rotate(0deg);
}

.m-ptr.is-refreshing .m-ptr__spinner {
  animation: m-ptr-spin 900ms linear infinite;
}

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

/* Pull-to-refresh is mobile-only; the desktop sidebar/scroll model
 * doesn't expect it and the gesture is weird on a trackpad. */
@media (min-width: 768px) {
  .m-ptr { display: none; }
}


/* ============================================================
 * NOTIFICATION BELL + INBOX OVERLAY (Phase M.5)
 * ============================================================
 * Bell button lives in the top header (mobile chrome) and the
 * desktop chrome's header. Tap opens a slide-up sheet overlay
 * (.m-inbox) that renders the latest 20 notifications inline
 * via fetch to /notifications/inbox.json. Reuses .m-sheet and
 * .m-sheet-backdrop primitives. Theme-aware. */

.m-bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: transparent;
  border: none;
  color: var(--text);
  font-size: 20px;
  cursor: pointer;
  transition: background-color 120ms ease;
}
.m-bell:hover,
.m-bell:focus-visible { background: var(--bg-elev); outline: none; }
.m-bell:active { transform: scale(0.94); }

.m-bell__badge {
  position: absolute;
  top: 2px;
  right: 2px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 9px;
  background: var(--alert);
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
  font-family: Inter, system-ui, sans-serif;
  /* White ring so the badge stays legible over both light and
   * dark headers. */
  box-shadow: 0 0 0 2px var(--surface);
}

/* Desktop variant — same look, just placed in the desktop chrome
 * header. The .m-bell--desktop modifier is reserved for any
 * spacing/sizing tweaks if needed later; for now it inherits. */
.m-bell--desktop {
  margin-left: auto;  /* push to the right of the brand */
}


/* Inbox overlay — anchored popover that propagates from the bell.
 *
 * 2026-05-24 redesign: was inheriting .m-sheet's slide-up-from-
 * bottom transform. Members tapping the bell at the top-right
 * expected a popover that emerges FROM the bell, not a full-width
 * sheet sliding from the opposite edge. New behavior:
 *   - Fixed-positioned card pinned just under the header, anchored
 *     to the right edge (the bell's corner).
 *   - transform-origin: top right + scale animation makes it look
 *     like it grows out of the bell.
 *   - Smaller / popover-shaped (380px max-width) instead of full
 *     bottom-sheet width.
 * Overrides .m-sheet positioning entirely while keeping the
 * .is-open class hook the existing JS uses. */
.m-inbox {
  position: fixed;
  top: calc(var(--header-h) + env(safe-area-inset-top, 0px) + 6px);
  right: 8px;
  left: auto;
  bottom: auto;
  width: calc(100vw - 16px);
  max-width: 380px;
  max-height: calc(100vh - var(--header-h) - env(safe-area-inset-top, 0px) - 32px);
  border-radius: 14px;
  display: flex;
  flex-direction: column;
  padding: 0;
  background: var(--surface);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
  /* Override .m-sheet's outer overflow so only the inner __list
     scrolls. Without this, double scrollbars appear when the list
     is long enough to need them. */
  overflow: hidden;
  /* Closed state: collapsed near the bell. The scale + opacity
     transition makes it pop out from the top-right corner. */
  transform-origin: top right;
  transform: scale(0.85);
  opacity: 0;
  pointer-events: none;
  transition: transform var(--t-med) var(--ease),
              opacity var(--t-med) var(--ease);
}
.m-inbox.is-open {
  transform: scale(1);
  opacity: 1;
  pointer-events: auto;
}

.m-inbox__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--s-4) var(--s-4) var(--s-3);
  border-bottom: 1px solid var(--border);
}

.m-inbox__title {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
}

.m-inbox__close {
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 28px;
  line-height: 1;
  padding: 0 var(--s-2);
}
.m-inbox__close:hover { color: var(--text); }

.m-inbox__list {
  flex: 1;
  overflow-y: auto;
  padding: var(--s-2) 0;
}

.m-inbox__loading,
.m-inbox__empty {
  padding: var(--s-5) var(--s-4);
  color: var(--text-dim);
  text-align: center;
  font-size: 14px;
}

.m-inbox__item {
  display: block;
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--border);
  color: var(--text);
  text-decoration: none;
  transition: background-color 120ms ease;
}
.m-inbox__item:hover {
  background: var(--bg-elev);
  text-decoration: none;
}
.m-inbox__item:last-child { border-bottom: none; }

.m-inbox__item-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s-3);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-dim);
  margin-bottom: 2px;
}
.m-inbox__item-chan { font-weight: 600; }
.m-inbox__item-time { font-family: monospace; }

.m-inbox__item-title {
  font-weight: 600;
  color: var(--text);
  font-size: 14px;
  margin-bottom: 2px;
}

.m-inbox__item-body {
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.5;
}

.m-inbox__seeall {
  display: block;
  padding: var(--s-4);
  text-align: center;
  color: var(--accent);
  font-weight: 600;
  font-size: 14px;
  text-decoration: none;
  border-top: 1px solid var(--border);
  background: var(--bg-elev);
}
.m-inbox__seeall:hover {
  text-decoration: none;
  background: var(--surface);
}

/* Desktop: same popover-from-bell behavior, just anchored to the
   desktop header height (the m-desktop-header sits at the top of
   the viewport at >=768px). Slightly larger popover since we
   have more room. The scale animation already feels right at
   both viewport sizes since transform-origin: top right doesn't
   depend on viewport width. */
@media (min-width: 768px) {
  .m-inbox {
    /* The desktop header is roughly the same height as mobile's
       --header-h (56px); --header-h is still in scope. */
    top: calc(var(--header-h) + 12px);
    right: 16px;
    max-width: 420px;
  }
}

