/* Momentum — app overlay on rv-theme.css.
 *
 * House rules from the RV style guide (RV-style-exp-260908), because they
 * decide which Bootstrap class to reach for:
 *
 *   - `*primary` renders in the CUSTOMER's brand colour: rv-theme maps
 *     --bs-primary to --rv-accent, which _head.html overrides per gym. Use it
 *     for anything that should wear the gym's identity.
 *   - `*info` is always PV blue. Use it when an element must stay blue no
 *     matter whose page it is.
 *   - `*success` / `*warning` / `*danger` carry meaning only, and the guide
 *     asks us to avoid placing them beside brand-coloured elements.
 *   - Momentum is a branded satellite, so it inherits rv-theme's NEUTRAL gray
 *     ramp rather than PV's blue-tinted one. Don't reintroduce blue grays.
 *
 * Do NOT hardcode --rv-accent or --bs-primary in this file. An unbranded page
 * should keep rv-theme's defaults; the gym's colour arrives from PV.
 *
 * Surface-specific components (scratch card, OTP boxes, kiosk display, QR
 * sheet) land here as each surface is converted, mostly lifted from
 * referralboost.css so the two apps stay visually consistent.
 */

body {
  background: var(--bs-body-bg);
  -webkit-font-smoothing: antialiased;
}

/* Borderless white cards — the rv-theme / ReviewLoop / ReferralLoop default.
   Surfaces that need a visible frame (kiosk) opt back in locally. */
.card {
  --bs-card-border-width: 0;
  border-radius: 1rem;
}

/* The wordmark is `.logo` + a single `.logo-part-1-color` span: Momentum is
   one word, so unlike Review|Boost or Leads|Vault there is no second colour.
   The icon sits in the theme's muted gray, as it does in the sibling apps. */
.logo-icon {
  color: #bdc0c9;
}

/* The gym's logo on the nav workspace chip. The wordmark beside it stays
   "Momentum" — the chip is what tells the operator which team they're in. */
.mo-nav-business-logo {
  object-fit: contain;
  vertical-align: -0.2em;
  margin: -10px 4px;
}

/* Wide enough for the signed-in email, which is the longest thing in it. */
.mo-nav-dropdown {
  min-width: 260px;
}

footer.mo-footer {
  padding: 2.5rem 0;
  text-align: center;
  color: var(--bs-secondary-color);
  font-size: .9rem;
  background: var(--bs-tertiary-bg);
  border-top: 1px solid var(--bs-border-color);
}

/* Phone-first surfaces (staff, participant) cap their content width instead of
   using Bootstrap's responsive container: they're one-column handset layouts,
   not pages that should stretch across a desktop. */
.mo-narrow {
  max-width: 560px;
}

/* A short numeric code read aloud or typed across devices — the staff wave-in
   code, and the pending-participant code in the join flow. Tabular figures so
   the digits don't shift width as they change. */
.mo-code-display {
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: .3rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* Participant surface — the visitor's own phone.
 *
 * The gym's colour lives in the page wash (.mo-accent-wash) and in anything
 * `*primary`; the navbar above it stays plain white so the gym's own logo
 * reads whatever colour that logo is.
 */
.mo-narrow-sm {
  max-width: 480px;
}

/* The wash every public page sits on: white under the navbar, fading into the
   gym's colour. Byte-identical to referralboost's .kiosk-page gradient, which
   .kiosk-page here shares, so a visitor meets the same page in both apps.
   Deliberately NOT --rv-accent-subtle: that token mixes 80% white where both
   apps' gradients mix 90%, and matching the sibling matters more here. */
.mo-accent-wash,
.kiosk-page,
.mo-qr-stage {
  background: linear-gradient(
    180deg,
    #ffffff 3rem,
    color-mix(in srgb, var(--rv-accent), white 90%) 100%
  );
}

.mo-front-card {
  --bs-card-border-width: 0;
  max-width: 480px;
  margin: auto;
  width: 100%;
  box-shadow: var(--bs-box-shadow-lg);
  overflow: hidden;
}

.mo-border-dashed {
  border: 1px dashed var(--bs-gray-400);
}

/* The one step they can act on right now, ringed in the gym's accent — the
   same colour as the wayfinder heading above it, so "What's next" and the
   card it points at agree.

   outline rather than box-shadow: the 2px gap has to be see-through, and a
   transparent box-shadow layer does not mask the layer beneath it, so that
   route paints one solid 5px ring instead of a ring held off the edge.
   outline also stays out of layout, leaving the card's dashed border and its
   own spacing exactly as they are. */
.mo-step-current {
  outline: 4px solid var(--rv-accent);
  outline-offset: 2px;
}

/* A step the rules have not opened yet: the card and everything advertising
   it recede, so the only thing left at full strength is the line saying why
   it is shut.

   Per element rather than on a wrapper, because opacity groups: a child of
   an opacity:.5 parent cannot be brought back to 1, so dimming the card body
   would take the explanation down with it. .mo-step-dim marks what fades and
   does nothing outside a locked card. */
.mo-step-locked .mo-step-dim {
  opacity: .6;
}

/* Wayfinder: a section heading shaped like a direction sign — a rectangle
   whose right edge comes to a point, the way "this way" signage does.

   --rv-on-accent is rv-theme's paired foreground, derived from the accent's
   own lightness, so a gym with a pale brand colour gets dark text instead of
   unreadable white. Same pairing as .mo-kiosk-stage; do not swap it for a
   hardcoded white.

   Built as a rounded box plus a rotated square rather than one clip-path:
   clip-path cuts hard edges and cannot round a vertex, and all three points
   on the right have to carry the same radius as the rest of the badge. The
   square is rotated 45°, which sends its top-right corner out to the right
   as the tip and its top-left and bottom-right corners to the box's own
   right-hand corners — so those three get the radius and the fourth, buried
   inside the box, stays square. It inherits the background, so the two
   shapes read as one. */
.mo-wayfinder {
  /* Half the badge's own height: that is how far a 45° point reaches. */
  --mo-wayfinder-point: .95em;
  /* Deliberately much tighter than --bs-border-radius. Rounding a corner
     pulls the outline back from the vertex by ~0.41 x the radius, and on a
     square this small the badge radius would cut all three points back far
     enough to blunt the tip into a bump and lift the diamond off the box's
     top and bottom edges. */
  --mo-wayfinder-radius: .18em;
  position: relative;
  display: inline-block;
  max-width: 100%;
  padding: .25em calc(var(--mo-wayfinder-point) + 0.05em) .25em .7em;
  background: var(--rv-accent);
  color: var(--rv-on-accent);
  /* Square on the right: the point springs off a flat edge. Rounding there
     too would curve the box away from the diamond's straight sides and notch
     the join. */
  border-radius: var(--bs-border-radius) 0 0 var(--bs-border-radius);
}
.mo-wayfinder::after {
  content: "";
  position: absolute;
  top: 50%;
  right: 0;
  /* 1/√2 of the height puts the square's diagonal exactly across the box's
     right edge, so the point springs from the full height of it.

     Exactly 1/√2, never more: rounding pulls each corner back from its
     vertex, so at this size the diamond can only ever sit inside the box's
     top and bottom edges. Padding it out to close that hairline makes the
     rounded vertices cross the edges instead, and a crossing reads as a
     horn on the shoulder of the arrow the moment anyone zooms in. */
  height: 70.7%;
  aspect-ratio: 1;
  background: inherit;
  /* The tip only (top-right, which the rotation swings out to the right).
     The other two outer corners land on the box's own square corners, and
     rounding pulls an outline ~0.41 x the radius back from its vertex — so
     rounding them steps the join, and enlarging the square to close the step
     pushes them past the edge as horns instead. Left square, they meet the
     box exactly. They are 135° shoulders rather than real corners, so there
     is nothing there that reads as sharp. */
  border-radius: 0 var(--mo-wayfinder-radius) 0 0;
  transform: translate(50%, -50%) rotate(45deg);
}

/* The gym's logo at the top of a participant card — referralboost's
   .kiosk-logo in the same position, given a little more room here (80px tall
   against its 60px, and a fixed 220px rather than its 80% of the card).

   PV serves whatever aspect ratio the gym uploaded, so all three properties
   are load-bearing: the fixed height reserves the row before a remote image
   lands (this is read on expo wifi, and a head that grows on load is the jump
   you'd see); max-width keeps a very wide wordmark inside the card; and
   contain keeps that clamp from squashing the mark rather than letterboxing
   it. */
.mo-header-logo {
  display: block;
  width: 100%;
  height: auto;
  max-height: 80px;
  max-width: 220px;
  object-fit: contain;
}

/* The logo-and-title block the cards open with (common/_brand_head.html).
   The gap belongs to the head, not to whatever follows it — except when the
   head IS the card, as on What's next, where the card's own padding is the
   only spacing wanted. */
.mo-brand-head {
  margin-bottom: 1.5rem;
}
.mo-brand-head:last-child {
  margin-bottom: 0;
}

/* Full-width accent bar over rv-theme's auto-contrasting foreground. Only the
   simulator console still wears one; the participant surface carries its
   branding inside the card instead (common/_brand_head.html). */
.mo-accent-header {
  background: var(--rv-accent);
  color: var(--rv-on-accent);
}

/* ── Shared six-box OTP input ─────────────────────────────────────────────
   Pairs with templates/common/_otp_input.html. Keep this block identical in
   leadsvault.css, referralboost.css and momentum.css so every satellite's
   code entry looks the same: six separate soft-grey rounded boxes, one digit
   each, in a single centred row that never wraps (fits a 320px phone). */
.otp-boxes {
  display: flex;
  flex-wrap: nowrap;
  justify-content: center;
  gap: .5rem;
  margin: 1.25rem 0 .5rem;
}
.otp-box {
  flex: 0 0 auto;
  width: 3rem;
  height: 3.5rem;
  padding: 0;
  border: 1px solid var(--bs-border-color);
  border-radius: .5rem;
  background: var(--bs-tertiary-bg);
  text-align: center;
  font-size: 1.5rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--bs-emphasis-color);
  caret-color: var(--rv-accent, var(--bs-primary));
  transition: border-color .15s, box-shadow .15s, background-color .15s;
}
.otp-box:focus {
  outline: none;
  background: var(--bs-body-bg);
  border-color: var(--rv-accent, var(--bs-primary));
  box-shadow: 0 0 0 .2rem color-mix(in srgb, var(--rv-accent, var(--bs-primary)) 20%, transparent);
}
.otp-box.is-filled {
  background: var(--bs-body-bg);
  border-color: var(--rv-accent, var(--bs-primary));
}
.otp-boxes.is-invalid .otp-box:not(.is-filled) {
  border-color: var(--bs-danger);
}
@media (max-width: 380px) {
  .otp-boxes { gap: .35rem; }
  .otp-box { width: 2.5rem; height: 3rem; font-size: 1.25rem; }
}

/* Operator-authored copy — terms_text and friends — comes out of a textarea,
   so the line breaks the operator typed are the only structure it has. */
.mo-prewrap {
  white-space: pre-line;
}

/* Terms panels on the join form. Fixed height with an inner scroll so the
   gym's terms and PV's read as two boxes of the same size however long either
   one runs — referralboost's treatment on its friend pages.

   Tighter than Bootstrap's p-3, and asymmetric: at this size the padding is
   competing with the fixed height for how many lines of legalese are visible
   before the reader has to scroll. */
.mo-terms-box {
  height: 6rem;
  overflow-y: auto;
  padding: .6rem .8rem;
}

/* The completion page's check (participant/landing.html). A fixed size, not
   .display-*: those scale with the viewport and come out small on exactly
   the phones this page is for. */
.mo-landing-check {
  font-size: 4.5rem;
}

/* Scratch card — lifted from referralboost.css so the reward reveal feels the
 * same in both apps, including the gold "prize" panel underneath the foil.
 * Element ids are suffixed with the issuance id (several cards can share a
 * page); the classes below are what the stylesheet hangs on.
 */
.scratch-wrap {
  position: relative;
  width: 100%;
  height: 220px;
  margin: 1.5rem 0 .5rem;
  border-radius: 1rem;
  overflow: hidden;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}

.scratch-prize {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, #ffd76a 0%, #ff9844 100%);
  color: #2b1700;
  text-align: center;
}

.scratch-prize .prize-face {
  font-size: 3rem;
  font-weight: 800;
  line-height: 1;
}

.scratch-prize .prize-desc {
  font-weight: 600;
  margin-top: .25rem;
  padding: 0 1rem;
}

.scratch-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  cursor: grab;
}

.scratch-canvas:active {
  cursor: grabbing;
}

.scratch-hint {
  position: absolute;
  bottom: .5rem;
  left: 0;
  right: 0;
  text-align: center;
  color: #fff;
  font-weight: 600;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .4);
  pointer-events: none;
}

/* Full-viewport confetti overlay fired on reveal. */
.scratch-confetti {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 1080;
  display: none;
}

/* Simulator console — the operator's guided walkthrough of their own promotion.
 *
 * The trainer is a CSS-drawn character (no image asset) with four looping
 * animations: the avatar bobs, the eyes blink, the hand waves, and .mo-pulse
 * marks anything the page is waiting on. Drawn geometry, so the magic numbers
 * are load-bearing — nudging one moves an eye off the face.
 *
 * Only two colours here follow the gym's brand: the collar under the face and
 * the name tag. The skin gradient and the facial features stay fixed, because
 * an arbitrary accent would produce a green face.
 */
.mo-trainer {
  display: flex;
  gap: .9rem;
  align-items: flex-start;
}

.mo-trainer-avatar {
  flex-shrink: 0;
  width: 76px;
  text-align: center;
  animation: mo-bob 2.6s ease-in-out infinite;
}

.mo-trainer-face {
  width: 64px;
  height: 64px;
  margin: 0 auto;
  border-radius: 50%;
  background: linear-gradient(135deg, #ffd39b, #f4b06c);
  position: relative;
  box-shadow: inset 0 -8px 0 var(--rv-accent);
}

/* Scoped to the face, so these very generic names can't leak. */
.mo-trainer-face .eye {
  position: absolute;
  top: 24px;
  width: 8px;
  height: 10px;
  border-radius: 50%;
  background: #20222a;
  animation: mo-blink 4s infinite;
}

.mo-trainer-face .eye.l { left: 17px; }
.mo-trainer-face .eye.r { right: 17px; }

.mo-trainer-face .smile {
  position: absolute;
  left: 50%;
  top: 40px;
  width: 22px;
  height: 10px;
  transform: translateX(-50%);
  border: 3px solid #20222a;
  border-top: 0;
  border-radius: 0 0 22px 22px;
  border-left-color: transparent;
  border-right-color: transparent;
}

.mo-trainer-face .hand {
  position: absolute;
  right: -12px;
  top: 30px;
  font-size: 1.3rem;
  transform-origin: 70% 80%;
  animation: mo-wave 1.8s ease-in-out infinite;
}

.mo-trainer-tag {
  font-size: .72rem;
  font-weight: 700;
  color: var(--rv-accent-shade);
  background: var(--rv-accent-subtle);
  border-radius: 99px;
  padding: .15rem .5rem;
  margin-top: .3rem;
  display: inline-block;
}

/* Speech bubble, with the little tail poking at the avatar. */
.mo-trainer-bubble {
  position: relative;
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: 14px;
  padding: .9rem 1.1rem;
  flex: 1;
  box-shadow: var(--bs-box-shadow-sm);
}

.mo-trainer-bubble:before {
  content: "";
  position: absolute;
  left: -8px;
  top: 22px;
  width: 14px;
  height: 14px;
  background: var(--bs-body-bg);
  border-left: 1px solid var(--bs-border-color);
  border-bottom: 1px solid var(--bs-border-color);
  transform: rotate(45deg);
}

/* Marks a step the page is waiting on — a scan, a text, a staff confirmation. */
.mo-pulse {
  animation: mo-pulse 1.6s ease-in-out infinite;
}

@keyframes mo-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-5px); }
}

@keyframes mo-blink {
  0%, 94%, 100% { transform: scaleY(1); }
  96% { transform: scaleY(.1); }
}

@keyframes mo-wave {
  0%, 100% { transform: rotate(0); }
  20% { transform: rotate(24deg); }
  40% { transform: rotate(-8deg); }
  60% { transform: rotate(20deg); }
}

@keyframes mo-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: .45; }
}

/* Someone who set "reduce motion" gets a still trainer. Four infinite loops on
   one screen is exactly what that preference exists for. */
@media (prefers-reduced-motion: reduce) {
  .mo-trainer-avatar,
  .mo-trainer-face .eye,
  .mo-trainer-face .hand,
  .mo-pulse {
    animation: none;
  }
}

/* Kiosk — two unrelated layouts that happen to share the word.
 *
 * .kiosk-page / .kiosk-card is the centred-card treatment lifted from
 * referralboost.css, used here for the expired-code notice a participant's
 * phone lands on. Kept byte-compatible with ReferralLoop so the two apps
 * show visitors the same furniture. Its accent gradient lives with
 * .mo-accent-wash above, which the participant surface shares.
 *
 * .mo-kiosk-stage is the tablet mounted at the front desk. It has no
 * sibling-app equivalent: full-bleed accent with the QR floated on top.
 */
.kiosk-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
}

.kiosk-card {
  --bs-card-border-width: 0;
  max-width: 480px;
  margin: auto;
  width: 100%;
  box-shadow: var(--bs-box-shadow-lg);
  overflow: hidden;
}

.mo-kiosk-stage {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  text-align: center;
  background: var(--rv-accent);
  /* rv-theme derives this from the accent's own lightness, so a gym with a
     pale brand colour gets dark text rather than unreadable white. */
  color: var(--rv-on-accent);
}

.mo-kiosk-stage h1 {
  font-size: clamp(1.6rem, 5vw, 2.6rem);
}

/* The quiet zone around a QR code has to stay white for cameras to read it,
   whatever the surrounding accent or theme is. */
.mo-kiosk-qr {
  display: inline-block;
  background: #fff;
  border-radius: 1.125rem;
  padding: 18px;
}

.mo-kiosk-qr img {
  display: block;
  width: min(62vmin, 460px);
  height: auto;
}

.mo-kiosk-back {
  position: fixed;
  top: .9rem;
  left: 1rem;
  font-size: .9rem;
  color: inherit;
  text-decoration: none;
}

/* Fenced curl examples on the operator API guide. */
.mo-code {
  background: var(--bs-tertiary-bg);
  border-radius: .5rem;
  padding: .9rem 1rem;
  overflow-x: auto;
  font-size: .82rem;
  line-height: 1.5;
}

/* QR codes. .mo-qr is any rendered code; .mo-qr-tile is one cell of the
   printable sheet, which must not be split across a page break. */
/* Deliberately no `image-rendering: pixelated` here. qrcode.make() renders at
   box_size 8-10, so these PNGs arrive 264-370px wide and every size below
   DOWNSCALES them (0.24x on the roster). Nearest-neighbour sampling on the way
   down drops whole QR modules, which is a scannability bug, not a sharpness
   win. Let the browser interpolate. */
.mo-qr {
  width: 100%;
  max-width: 200px;
}

/* Size ramp for contexts that need a smaller code than the 200px default: a
   table cell, a card, a phone screen. These cap the width rather than fixing
   it, so a code in a narrow column still shrinks to fit. */
/* Tapping a code on the QR sheet blows it up to fill the screen — the same
   wash and white card the customer sees, so the desk display matches what
   they are about to scan. Its own layout rather than .kiosk-page's, whose
   display:flex would fight the open/closed toggle. */
.mo-qr-zoom {
  cursor: zoom-in;
}
.mo-qr-stage {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 1080;
  overflow: auto;
  padding: 1.5rem;
  cursor: zoom-out;
}
.mo-qr-stage.is-open {
  display: grid;
  place-items: center;
}
/* Bigger than the 200px sheet tile, but never taller than the screen it is
   being scanned off. */
.mo-qr-stage .mo-qr {
  max-width: min(340px, 55vh);
}

.mo-qr-sm { max-width: 72px; }
.mo-qr-md { max-width: 110px; }
.mo-qr-lg { max-width: 160px; }

/* One code on the sheet: a .card for the white box, like every other surface
   in the app, with no card wrapped around the set.

   align-items, because .card is a flex column: .mo-qr is an inline image
   that text-align centres in a block, but as a flex item it takes its own
   width (200px) and text-align no longer reaches it, so it would sit against
   the left edge of a wider tile.

   The fill is pinned to #fff rather than left to --bs-card-bg: a QR needs a
   white quiet zone to scan, and this is the one surface that would have to
   stay white if the app ever stopped hardcoding data-bs-theme="light". */
.mo-qr-tile {
  align-items: center;
  background: #fff;
  padding: 1rem;
  text-align: center;
  page-break-inside: avoid;
  break-inside: avoid;
}

.mo-qr-tile-link {
  display: inline-block;
  font-size: .75rem;
  word-break: break-all;
  text-decoration: none;
}

/* Numeric field widths. A currency, percentage or count input should be about
   as wide as the number it holds — left to stretch, a two-digit quantity ends
   up as wide as its table column. Like .mo-qr-*, these CAP the width rather
   than fixing it, so a field in a narrow column still shrinks to fit rather
   than overflowing it.

   Apply to the .input-group when the field carries a $ or % addon, since the
   addon and the input share the width; otherwise apply to the input itself. */
/* Placeholder chips in an editor field's help line: a badge that types the
   mustache for you. Sized down from .badge so a row of them sits inside the
   .form-text line without shouting over the copy around it. */
.mo-placeholder {
  font-size: .72rem;
  font-weight: 500;
  font-family: var(--bs-font-monospace);
  padding: .3em .5em;
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius-pill);
  background-color: var(--bs-tertiary-bg);
  color: var(--bs-secondary-color);
  cursor: pointer;
}
.mo-placeholder:hover,
.mo-placeholder:focus-visible {
  background-color: var(--bs-primary);
  border-color: var(--bs-primary);
  color: #fff;
}

.mo-field-sm { max-width: 5.5rem; }
.mo-field-md { max-width: 7rem; }
.mo-field-lg { max-width: 12rem; }

/* Printing. The QR sheet is the one page meant for paper: chrome drops away
   and everything left prints black on white — the codes, their labels and
   their URLs, nothing else.

   There is deliberately no print-color-adjust: exact anywhere. That property
   forces a browser to paint backgrounds it would otherwise drop, which is
   the opposite of what a printed code sheet wants — it would put a tile fill
   on the paper and eat the quiet zone the scanner needs. */
@media print {
  .no-print,
  .navbar {
    display: none !important;
  }

  .card {
    border: 0;
    box-shadow: none !important;
  }

  /* Tiles are boxes on screen only: on paper the white fill is the page. */
  .mo-qr-tile {
    background: transparent;
    padding: 0.5rem 0;
  }

  /* The on-screen header sits above the sheet with a gap; it's hidden in
     print (.no-print), so the gap would otherwise print as a blank margin. */
  .mo-qr-sheet {
    margin-top: 0 !important;
  }
}

/* =============================================================================
 * Expandable setup cards (operator/promotion_detail.html): the opt-in reward
 * and every action as one row each, opening in place into their own editor.
 *
 * A collapsed row states what the thing does and how it is performing, so the
 * list answers most questions without opening anything. The fixed pixel
 * columns are what make the rows line up like a table while staying separate
 * cards — a real table can't hold a full form in a row.
 *
 * Only ONE card carries a body at a time and the server decides which
 * (?open=…), so the page never holds two reward panels or two rule builders;
 * both of those includes own fixed element ids.
 * ========================================================================== */
.ecard {
  border-radius: 1.125rem;
  margin-bottom: 0.5rem;
}
.ehead2 {
  display: grid;
  /* index · title · reward · completions · preview · status · chevron. The
     grip and the move buttons have no track at rest — they are display:none
     and claim none — so reorder mode adds two at the front (see
     .is-reordering). The two stat columns gave up width for the preview:
     they hold two short lines and never needed 128px. */
  grid-template-columns: 22px minmax(0, 1fr) 104px 104px 300px 74px 14px;
  align-items: center;
  gap: 14px;
  padding: 0.7rem 1.25rem;
  cursor: pointer;
  /* Anchors the stretched link that makes the whole row one click target. */
  position: relative;
}
/* The open card is marked by an outline alone — no shadow, no fill change,
   nothing that would make the row jump as it opens. */
.ecard-open {
  outline: 2px solid color-mix(in srgb, var(--rv-accent) 40%, transparent);
}
.ecard-open > .ehead2 {
  border-bottom: 1px solid var(--rv-gray-100);
}
.ecard-title {
  min-width: 0;  /* long names truncate instead of pushing the grid apart */
}
.ecard-title a {
  color: inherit;
  text-decoration: none;
}
.enum {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: var(--rv-gray-100);
  color: var(--rv-gray-700);
  font-size: 0.6875rem;
  font-weight: 600;
  display: grid;
  place-items: center;
}
.ecode {
  font-family: var(--bs-font-monospace);
  font-size: 0.75rem;
  color: var(--rv-gray-700);
}
/* gray-700 (#4d4e55), not gray-500: 12px on white needs it to clear 4.5:1. */
.emeta {
  font-size: 0.75rem;
  color: var(--rv-gray-700);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.15rem;
}
.edot {
  color: var(--rv-gray-400);
}
/* The recipient's step card, shown in the row. Scaled down rather than
   restyled: it is the member's own markup (participant/_step_card.html), so
   changing how it LOOKS here would stop it being a preview. Everything in
   the card is sized in em, so one font-size brings the whole thing down. */
.epreview {
  font-size: 0.75rem;
}
.epreview .card {
  margin-bottom: 0 !important;
  padding: 0.6rem !important;
  cursor: pointer;  /* the row's stretched link covers it */
}
/* The member's QR scanner (participant/_qr_scanner.html). Black like a
   camera app, whatever the gym's colours. The stage is portrait in the
   dialog and fills whatever the header and footer leave in full screen
   (modal-fullscreen-sm-down: phones). The video covers the stage rather than
   letterboxing: the edges it crops are the least useful part of the picture.
   The frame's huge spread shadow is what dims everything outside it. */
.mo-scan-content {
  background: #000;
}
/* In the dialog: 3:4 of its 500px width when the screen has the room, and
   otherwise whatever is left. modal-dialog-scrollable caps the dialog at the
   viewport (margins included), and flex-shrink hands the shortfall to the
   stage — so nothing here guesses how tall the header and footer are.
   A fixed height, NOT aspect-ratio + max-height: with both, some browsers
   (Safari) keep the ratio by narrowing the box, which leaves it hugging the
   dialog's left edge with the frame off-centre. The video covers whatever
   box results. The selector out-ranks .modal-dialog-scrollable .modal-body,
   whose overflow: auto would otherwise apply. */
.mo-scan-content .mo-scan-stage {
  position: relative;
  overflow: hidden;
  flex: 0 1 auto;
  height: 41.75rem;
  min-height: 0;
  background: #000;
  /* Lets the frame size itself against the stage's height as well as its
     width (cqw/cqh below). */
  container-type: size;
}
@media (max-width: 575.98px) {
  .mo-scan-content .mo-scan-stage {
    flex: 1 1 auto;
    height: auto;
  }
}
.mo-scan-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.mo-scan-frame {
  position: absolute;
  top: 50%;
  left: 50%;
  /* The smaller of the stage's two sides decides, so the square never
     overruns a stage that has been squeezed short. */
  width: min(70cqw, 80cqh, 320px);
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border: 3px solid rgba(255, 255, 255, 0.9);
  border-radius: 1.25rem;
  box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.45);
  pointer-events: none;
}

/* The instructions and scan buttons are the member's real controls. In the
   row they're a picture: a click opens the row, not the operator's sign-up
   form or a camera. */
.epreview .card a,
.epreview .card button {
  pointer-events: none;
}
/* The reward badge is an h5 — a heading size inside a 300px column, where
   it would wrap the name onto its own line. */
.epreview .h5 {
  font-size: 1em;
  margin-bottom: 0;
}
.epreview .fs-4 {
  font-size: 1.15em !important;
}
.estat {
  font-size: 0.75rem;
  color: var(--rv-gray-700);
  line-height: 1.4;
}
.estat strong {
  font-size: 0.8125rem;
  color: var(--rv-gray-900);
  display: block;
}
.ebody {
  padding: 1.15rem 1.25rem 1.25rem;
  animation: mo-card-open 0.3s ease-in-out;
}
/* Opening a card is a navigation (?open=…), so the body arrives with the page
   already at its full height — there is no height to grow from. This fades and
   settles it instead, which marks which card opened without reflowing
   everything below it mid-read, as animating the height on load would.

   Bootstrap's Collapse is the tool for a client-side toggle, and it doesn't
   fit here: it would have to hide the body after paint and then grow it, and
   a slow or blocked CDN script would leave the editor hidden altogether. */
@keyframes mo-card-open {
  from {
    opacity: 0;
    transform: translateY(-0.35rem);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
/* Here rather than in the reduce block up by the trainer: a media query adds
   no specificity, so an override written earlier in the file would lose to
   the .ebody rule above it. */
@media (prefers-reduced-motion: reduce) {
  .ebody {
    animation: none;
  }
}
/* Textareas fill the card like every other control in it. They used to be
   capped at 560px on the theory that a very long line is hard to read —
   true of prose, but these hold an SMS-length sentence or two, and the cap
   only made them look truncated beside the full-width rows above. */
.efoot {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1.1rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--rv-gray-100);
}
.addcard {
  border: 1px dashed var(--rv-gray-300);
  border-radius: 1.125rem;
  padding: 0.9rem 1.25rem;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  color: var(--rv-gray-700);
  background: transparent;
  text-decoration: none;
  cursor: pointer;
}
.addcard:hover,
.addcard:focus-visible {
  border-color: var(--rv-gray-400);
  color: var(--rv-gray-900);
}

/* Reorder mode. The grip and the move buttons appear together in a leading
   column that simply isn't there the rest of the time — a display:none grid
   item takes no track, so the resting row keeps its seven columns. While
   reordering, the row's stretched link is switched off: dragging a card must
   not navigate to it. */
.ecard-move {
  display: none;
}
.is-reordering .ehead2 {
  grid-template-columns: auto 16px 22px minmax(0, 1fr) 104px 104px 300px 74px 14px;
}
/* From xxl the container stops growing at 1320px, so the row has width to
   spare: the preview takes the 390px the real page gives it (.mo-preview-phone
   is capped at the same), which is where its copy stops wrapping. Placed
   AFTER both track lists above — a media query adds no specificity, so source
   order is what decides. */
@media (min-width: 1400px) {
  .ehead2 {
    grid-template-columns: 22px minmax(0, 1fr) 104px 104px 390px 74px 14px;
  }
  .is-reordering .ehead2 {
    grid-template-columns: auto 16px 22px minmax(0, 1fr) 104px 104px 390px 74px 14px;
  }
  /* At full width it is the member's card at its real size, so it no longer
     needs shrinking to fit. */
  .epreview {
    font-size: 0.8125rem;
  }
}
.is-reordering .ecard-move {
  display: flex;
  gap: 0.25rem;
}
.is-reordering .ecard {
  cursor: grab;
}
.is-reordering .ecard.is-dragging {
  opacity: 0.4;
}
.is-reordering .stretched-link::after {
  display: none;
}
/* The grip only means anything in reorder mode, so that is the only time it
   exists — the "Change order of actions" link is the way in, not a grip on
   every row inviting a drag that does nothing. display, not visibility: a
   hidden-but-present grip would hold its 16px track open and indent every
   row for nothing. */
.mo-drag-handle {
  display: none;
  cursor: grab;
  color: var(--rv-gray-400);
}
.is-reordering .mo-drag-handle {
  display: block;
}

/* Narrow screens: the fixed reward and performance columns drop out and the
   meta line carries the summary alone. The status badge stays — "Paused" is
   the one thing on the row you cannot afford to miss. */
/* Bootstrap's lg step, not a round number: .container is 720px from 768px
   up and only grows again at 992px, so between 901px and 991px the wide
   row's fixed tracks (702px with gaps) no longer fit inside the card and
   the badge and chevron spilled past its edge. */
@media (max-width: 991.98px) {
  .ehead2 {
    grid-template-columns: 22px minmax(0, 1fr) 74px 14px;
    gap: 10px;
  }
  .is-reordering .ehead2 {
    grid-template-columns: auto 16px 22px minmax(0, 1fr) 74px 14px;
  }
  .estat,
  .epreview {
    display: none;
  }
}

/* =============================================================================
 * Dashboard stat cards (operator promotion page). Meaning-only Bootstrap
 * subtle colours; the big number leads, a one-line caption explains it. No
 * accent hardcoding — the cards sit on the neutral surface so the branded
 * primary buttons around them stay the only accent on the page.
 * ========================================================================== */
.mo-stat-card {
  height: 100%;
}
.mo-stat-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 0.75rem;
  font-size: 1.25rem;
}
.mo-stat-value {
  font-size: 1.75rem;
  font-weight: 700;
  line-height: 1.1;
}
.mo-stat-label {
  font-weight: 600;
}

/* Journey funnel: a labelled bar per stage, width ∝ share of the first stage. */
.mo-funnel-row + .mo-funnel-row {
  margin-top: 0.5rem;
}
.mo-funnel-track {
  background: var(--bs-tertiary-bg);
  border-radius: 0.375rem;
  overflow: hidden;
  height: 1.5rem;
}
.mo-funnel-fill {
  background: var(--rv-accent);
  color: var(--rv-on-accent);
  height: 100%;
  min-width: 2.25rem;
  display: flex;
  align-items: center;
  padding: 0 0.5rem;
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
}

/* =============================================================================
 * Customer journey timeline (common/_journey.html). A connected vertical
 * timeline: a success node for completed steps, an accent "you're here" node
 * for the current step, muted dots for upcoming ones. Accent comes from the
 * gym's brand (rv-theme --rv-accent / --rv-on-accent) so the current step and
 * reward pill always meet contrast; neutrals use Bootstrap vars.
 * ========================================================================== */
.mo-journey-progress { height: 0.5rem; }
.mo-journey-progress .progress-bar { background: var(--rv-accent); }

.mo-journey-steps {
  list-style: none;
  margin: 0;
  padding: 0;
}
.mo-journey-step {
  position: relative;
  display: flex;
  gap: 0.75rem;
  padding-bottom: 1.25rem;
}
.mo-journey-step:last-child { padding-bottom: 0; }
/* connector line between nodes */
.mo-journey-step:not(:last-child)::before {
  content: "";
  position: absolute;
  left: 15px;
  top: 30px;
  bottom: -2px;
  width: 2px;
  background: var(--bs-border-color);
}
.mo-journey-done:not(:last-child)::before { background: var(--bs-success); }

.mo-journey-node {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bs-body-bg);
  color: var(--bs-secondary-color);
  border: 2px solid var(--bs-border-color);
  font-size: 0.95rem;
}
.mo-journey-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--bs-secondary-color);
}
.mo-journey-done .mo-journey-node {
  background: var(--bs-success);
  border-color: var(--bs-success);
  color: #fff;
}
.mo-journey-current .mo-journey-node {
  background: var(--rv-accent);
  border-color: var(--rv-accent);
  color: var(--rv-on-accent);
}

.mo-journey-body { padding-top: 2px; min-width: 0; }
.mo-journey-name { font-weight: 600; }
.mo-journey-upcoming .mo-journey-name { color: var(--bs-secondary-color); }
.mo-journey-current .mo-journey-name { font-size: 1.05rem; }
/* The current step is the visually prominent one. */
.mo-journey-current .mo-journey-body {
  border: 1px solid var(--rv-accent);
  border-radius: 0.5rem;
  padding: 0.4rem 0.65rem;
  margin-top: -2px;
}
.mo-journey-here {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--rv-accent);
  margin-bottom: 0.1rem;
}
.mo-journey-reward {
  display: inline-block;
  background: var(--rv-accent);
  color: var(--rv-on-accent);
  border-radius: 0.375rem;
  padding: 0.15rem 0.5rem;
  font-size: 0.8rem;
  font-weight: 600;
}

/* =============================================================================
 * Operator Preview (operator/preview.html): the customer pages shown inside a
 * phone frame via a sandboxed iframe. Purely presentational — the safety comes
 * from the iframe sandbox and preview_mode, not this CSS.
 * ========================================================================== */
.mo-preview-phone {
  width: 100%;
  max-width: 390px;
  border: 10px solid #1f2733;
  border-radius: 2rem;
  overflow: hidden;
  box-shadow: 0 1rem 2.5rem rgba(0, 0, 0, 0.18);
  background: #1f2733;
}
.mo-preview-frame {
  display: block;
  width: 100%;
  height: 720px;
  border: 0;
  background: #fff;
}

/* =============================================================================
 * Promotion cards (home.html) — the operator's landing list, in the shape the
 * sibling apps use for their campaign lists.
 *
 * The whole card is one click target: a .stretched-link on the title covers
 * it. Bootstrap gives that overlay z-index 1, so anything else clickable in
 * the card has to be lifted above it or the card's link swallows the click.
 * ========================================================================== */
.mo-promo-card {
  transition: box-shadow 0.15s ease-in-out;
}
/* focus-within as well as hover: the stretched link's own focus ring is
   invisible (the anchor is just the title), so keyboard tabbing needs the
   whole card to answer. */
.mo-promo-card:hover,
.mo-promo-card:focus-within {
  box-shadow: var(--bs-box-shadow);
}
.mo-promo-link {
  color: inherit;
  text-decoration: none;
}
.mo-promo-card:hover .mo-promo-link,
.mo-promo-card:focus-within .mo-promo-link {
  text-decoration: underline;
}
/* Above the stretched link's overlay — see the note at the top of this block. */
.mo-promo-action {
  position: relative;
  z-index: 2;
}
.mo-promo-budget {
  height: 0.375rem;
}
.mo-promo-budget .progress-bar {
  background: var(--rv-accent);
}

/* =============================================================================
 * Boxed radio choices (common/_field.html, radio branch). An option that
 * carries a sentence of its own needs an edge to say where it stops; a bare
 * list of radios runs them together.
 * ========================================================================== */
.mo-choice {
  border: 1px solid var(--bs-border-color);
  border-radius: 0.625rem;
  /* Bootstrap's .form-check pads 1.5em for the input, which floats back into
     it on a -1.5em margin. Adding to that figure — rather than replacing it —
     is what keeps the radio inside the box instead of on its border. */
  padding: 0.75rem 1rem;
  padding-left: calc(1.5em + 0.75rem);
  margin-bottom: 0;
}
/* Stacked, so the gap goes between them and not after the last one. */
.mo-choice + .mo-choice {
  margin-top: 0.5rem;
}
/* The whole width of the box is the click target, not just the words. */
.mo-choice .form-check-label {
  display: block;
  cursor: pointer;
}
/* Selected: filled in the page's own background, a shade off the white card
   it sits on, with the border stepping up to match. :has() is doing the work
   — unsupported, it simply doesn't highlight, and the radio dot still says
   which one is chosen. */
.mo-choice:has(.form-check-input:checked) {
  background: var(--bs-body-bg);
  border-color: var(--rv-gray-400);
}


/* A step just done (journey page, Damian 2026-10-01): the card lights up
   and the check pops in, once per device, for a completion this device has
   not seen before. The celebration overlay is the same moment at full
   screen, for a step that completes while the page is open. */
.mo-just-done {
  animation: mo-done-glow 1.8s ease-out 1;
}
.mo-just-done .mo-done-check {
  animation: mo-done-pop .9s cubic-bezier(.2, 1.4, .4, 1) 1;
  transform-origin: center;
}
@keyframes mo-done-glow {
  0%   { box-shadow: 0 0 0 0 rgba(25, 135, 84, 0); background-color: rgba(25, 135, 84, 0); }
  25%  { box-shadow: 0 0 0 6px rgba(25, 135, 84, .35); background-color: rgba(25, 135, 84, .12); }
  100% { box-shadow: 0 0 0 0 rgba(25, 135, 84, 0); background-color: rgba(255, 255, 255, .25); }
}
@keyframes mo-done-pop {
  0%   { transform: scale(.2); opacity: 0; }
  60%  { transform: scale(1.35); opacity: 1; }
  100% { transform: scale(1); }
}
.mo-celebrate {
  position: fixed;
  inset: 0;
  z-index: 1080;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  background: rgba(255, 255, 255, .92);
  backdrop-filter: blur(4px);
}
.mo-celebrate-card {
  width: 100%;
  max-width: 26rem;
  animation: mo-card-open .4s ease-out 1;
}
.mo-celebrate-check {
  font-size: 5rem;
  line-height: 1;
  animation: mo-done-pop 1s cubic-bezier(.2, 1.4, .4, 1) 1;
}
body.mo-celebrating { overflow: hidden; }
@media (prefers-reduced-motion: reduce) {
  .mo-just-done,
  .mo-just-done .mo-done-check,
  .mo-celebrate-card,
  .mo-celebrate-check {
    animation: none;
  }
}


/* The action editor's Repeat section (operator/_action_fields.html).
   With the switch on, a subtle rule above and below sets the group apart
   from the rules before it and the reward after. */
.mo-repeat-on {
  border-top: 1px solid var(--bs-border-color);
  border-bottom: 1px solid var(--bs-border-color);
  padding-bottom: 1rem;
  margin: 1.5rem 0;
}
.mo-repeat-on > h3 { margin-top: 1rem !important; }
/* Each row of repeat fields is a grid: labels on one row, controls on the
   next, help text under those — so a label that wraps to two lines never
   pushes its input below the inputs beside it. The field partial's wrappers
   are dissolved (display: contents) so label, control and help are the grid
   items. Narrow screens keep the stacked layout. */
@media (min-width: 768px) {
  .mo-repeat-grid {
    display: grid;
    grid-template-columns: repeat(var(--mo-cols, 3), minmax(0, 13rem));
    column-gap: 1.5rem;
    row-gap: .25rem;
    margin-bottom: 1rem;
  }
  .mo-repeat-grid > .mo-repeat-cell,
  .mo-repeat-grid > .mo-repeat-cell > .mb-3 { display: contents; }
  .mo-repeat-grid[hidden],
  .mo-repeat-grid > .mo-repeat-cell[hidden] { display: none; }
  .mo-repeat-grid .form-label { grid-row: 1; align-self: end; margin-bottom: 0; }
  .mo-repeat-grid .form-control,
  .mo-repeat-grid .form-select,
  .mo-repeat-grid .input-group { grid-row: 2; align-self: start; }
  .mo-repeat-grid .form-text { grid-row: 3; }
  .mo-repeat-grid .text-danger { grid-row: 4; }
}
@media (min-width: 1200px) {
  .mo-repeat-grid { grid-template-columns: repeat(var(--mo-cols, 3), minmax(0, 15rem)); }
}
