/* ─── Reset & base ─── */
*, *::before, *::after { box-sizing: border-box; }

:root {
    --ve-navy: #0F1F1C;
    --ve-navy-dark: #2A3439;
    --ve-accent: #005E4F;
    --ve-accent-hover: #0F1F1C;
    --ve-bg: #F2F3F1;
    --ve-surface: #ffffff;
    --ve-border: #A7ADA9;
    --ve-control-border: #2A3439;
    --ve-text: #0F1F1C;
    --ve-muted: #2A3439;
    --ve-rail: #008868;
    --ve-status-info-bg: #DBEAFE;
    --ve-status-info-text: #1E3A5F;
    --ve-status-warning-bg: #FDE68A;
    --ve-status-warning-text: #4A2C00;
    --ve-status-error-bg: #B91C1C;
    --ve-status-error-text: #FFFFFF;
    /* ⚠️ "It went well" and "it went wrong", as TEXT. Added 2026-08-15 because these two
       shades were hard-coded into a dozen inline styles as #15803d and #b91c1c/#dc2626 —
       values chosen for a white card, which are unreadable on the dark redesign. Anything
       saying a thing succeeded or failed uses these, never a literal. The redesign overrides
       them with its own green and red. */
    --ve-success: #15803d;
    --ve-danger: #b91c1c;
    /* ─── The scrim: everything below an open form is darkened (issue #178) ───
       "Since I am still able to see the Dashboard behind the form, the screen feels
       overwhelming and with the dark colors it is hard to differentiate between what is on
       the form and what is part of the background." The form and the cards behind it are the
       SAME colour on the redesign (both --ve-card), so the only thing separating them is
       this. Measured on the real Buy a Package screen at 390px: the studio photograph behind
       it read at a relative luminance of 0.0875 against the form's own 0.0119 — seven times
       brighter than the thing the reader was supposed to be looking at. At 0.78 the loudest
       remaining element behind is about twice the form's ground, which reads as background.
       One token, used by every form's backdrop — the date picker's included. */
    --ve-scrim: rgba(0, 0, 0, 0.78);
    --ve-nav-width: 220px;
    /* Single knob for the minimal margin that pins every section to the device
       L/R edges (mobile). Combined with env(safe-area-inset-*) at the edges so
       the layout is anchored to the real screen, not to content width. */
    --ve-gutter: 8px;
}

/* Hard horizontal lockdown: the document can never be wider than the visible
   viewport. Without this, a single stray-wide descendant (or an iOS WebKit
   width quirk that Chrome doesn't show) lets the layout exceed the screen and
   the right edge gets clipped. overflow-x:hidden here clips it at the root;
   overflow-y stays scrollable so the auth compact-fit fallback still works. */
html, body {
    width: 100%;
    max-width: 100%;
    overflow-x: hidden;
    /* Kill the browser's pull-to-refresh (issue #141). A downward drag that
       reaches the VIEWPORT — i.e. one that started somewhere without its own
       scroller (topbar, modal backdrop, a short page, an auth form) or on a
       scroller already at the top — makes Chrome/Android and iOS Safari reload
       the page, which throws away whatever the user was typing. `contain` stops
       the overscroll at the root instead of handing it to the browser chrome;
       normal scrolling and the local rubber-band are untouched. The value must
       be set on BOTH html and body: the UA only propagates body's value to the
       viewport while html's is `auto`, so whichever one the engine reads sees
       `contain`. Individual scrollers (.ve-content, the drawer, modals) contain
       their own overscroll too, so it never chains up here in the first place.
       CSS-only covers Chrome/Android and iOS 16+; js/pullrefresh.js is the
       fallback for engines that ignore root overscroll-behavior. */
    overscroll-behavior-y: contain;
}

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

/* ─── The design's typefaces (issue #175) ───
   Inter everywhere, Heebo for the one display headline: the two faces the Figma design
   system names (node 146:351). Served from this site, never a font host — the app must
   reach a usable screen with no network at all, and a face fetched from somewhere else is
   a whole screen of substitute letterforms on the first offline start. Both ride the
   critical-asset manifest (CriticalAssetManifestService, the fonts/ sweep) so they are
   staged into the offline shell alongside the stylesheet that asks for them.

   ⚠️ Inter is ONE variable file spanning 100-900. Google serves the same 48 KB woff2 for
   every weight it advertises, so four weight-specific @font-face blocks would download the
   identical file four times. Verified by checksum, not assumed.

   Latin subset only: 60 KB for both faces. Nothing in this app is written in Cyrillic,
   Greek or Vietnamese, and the fallback stack still covers anything that arrives in one. */
@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url('/fonts/inter-var.woff2') format('woff2');
}

@font-face {
    font-family: 'Heebo';
    font-style: normal;
    font-weight: 700;
    font-display: swap;
    src: url('/fonts/heebo-700.woff2') format('woff2');
}

/* ─── Shell layout ─── */
.ve-shell {
    display: flex;
    width: 100%;
    max-width: 100%;
    height: 100vh;
    height: 100dvh; /* lock the frame to the *visible* viewport (handles mobile browser chrome) */
    overflow: hidden; /* only .ve-content scrolls — header + nav stay fixed */
}

.ve-main {
    flex: 1;
    margin-left: var(--ve-nav-width);
    min-width: 0; /* prevent flex child from overflowing */
    display: flex;
    flex-direction: column;
    height: 100vh;
    height: 100dvh;
    min-height: 0;
}

.ve-content {
    flex: 1 1 auto;
    min-width: 0;     /* a flex child must be allowed to shrink below its content width */
    min-height: 0;
    overflow-y: auto; /* the single scrolling "window"; the header above it does not scroll */
    overflow-x: hidden; /* last-resort net; the grid track below is the real fix */
    overscroll-behavior: contain; /* don't chain/rubber-band the content scroll out to the page (iOS) */
    padding: 1rem 1.25rem;
    /* WIDTH AUTHORITY (the method change). A single-column grid track of
       minmax(0, 1fr) clamps EVERY child to the track width: the `0` minimum
       defeats the flex/grid/min-content "blowout" that lets a stubborn
       descendant force the page wider than the screen — the real cause of the
       iOS right-edge truncation. This makes content REFLOW to fit instead of
       relying on overflow:hidden to CLIP it (clipping hid the symptom in Chrome
       but still truncated on WebKit). `100%` (not `100vw`) ties the cap to the
       true parent width; `100vw` is a foot-gun under viewport-fit=cover and
       sub-pixel rounding. See docs/responsive-layout-cheatsheet.md. */
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-content: start;
    max-width: min(1200px, 100%);
    width: 100%;
}

/* Belt-and-suspenders: no direct child of the content track may impose a
   min-content floor wider than the track, and none may exceed it. */
.ve-content > * { min-width: 0; max-width: 100%; }

/* Issue #154 follow-up — a real, in-flow condition banner. It is deliberately
   independent of .ve-topbar-title: it wraps, accepts pointer input and is visible
   to every role. Healthy idle renders no element, so it consumes no space. */
.ve-status-banner {
    flex: 0 0 auto;
    align-self: center;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    column-gap: 0.65rem;
    row-gap: 0.45rem;
    width: 100%;
    padding: 0.65rem calc(0.75rem + env(safe-area-inset-right, 0px))
             0.65rem calc(0.75rem + env(safe-area-inset-left, 0px));
    border: 0;
    font-size: 0.9rem;
    font-weight: 650;
    line-height: 1.35;
    white-space: normal;
    overflow: visible;
    overflow-wrap: anywhere;
    z-index: 140;
}

.ve-status-banner--info {
    background: var(--ve-status-info-bg);
    color: var(--ve-status-info-text);
}

.ve-status-banner--warning {
    background: var(--ve-status-warning-bg);
    color: var(--ve-status-warning-text);
}

.ve-status-banner--error {
    background: var(--ve-status-error-bg);
    color: var(--ve-status-error-text);
}

.ve-status-banner__icon { flex: 0 0 auto; }
.ve-status-banner__text { min-width: 0; }

.ve-status-banner__retry {
    grid-column: 2;
    justify-self: start;
    min-height: 40px;
    padding: 0.45rem 0.8rem;
    border: 2px solid var(--ve-status-error-text);
    border-radius: 7px;
    background: var(--ve-status-error-text);
    color: var(--ve-status-error-bg);
    font: inherit;
    font-weight: 750;
    line-height: 1;
    cursor: pointer;
}

.ve-status-banner__retry:hover:not(:disabled) { filter: brightness(0.94); }
.ve-status-banner__retry:focus-visible { outline: 3px solid currentColor; outline-offset: 2px; }
.ve-status-banner__retry:disabled { opacity: 0.75; cursor: wait; }

.ve-offline-empty {
    padding: .75rem .9rem;
    border: 1px solid var(--ve-border);
    border-radius: 8px;
    background: var(--ve-surface);
    color: var(--ve-muted);
    font-size: .88rem;
    line-height: 1.4;
}

@media (min-width: 480px) {
    .ve-status-banner { grid-template-columns: auto minmax(0, 1fr) auto; }
    .ve-status-banner__retry { grid-column: 3; }
}

/* ─── Navigation ─── */
.ve-nav {
    position: fixed;
    top: 0;
    left: 0;
    width: var(--ve-nav-width);
    height: 100vh;
    height: 100dvh; /* fit the visible viewport exactly, not the chrome-inflated 100vh */
    background: var(--ve-navy);
    display: flex;
    flex-direction: column;
    padding: 0;
    overflow: hidden; /* the nav itself never scrolls — only .ve-nav-links can */
    z-index: 100;
}

.ve-nav-brand {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    padding: 1.25rem 1.25rem 1rem;
    border-bottom: 1px solid rgba(255,255,255,0.08);
    flex-shrink: 0;
}

.ve-nav-phone {
    color: rgba(255,255,255,0.85);
    font-size: 0.95rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-decoration: none;
    white-space: nowrap;
}
.ve-nav-phone:hover { color: #fff; }

.ve-nav-logo {
    background: var(--ve-accent);
    color: white;
    font-weight: 800;
    font-size: 0.9rem;
    width: 36px;
    height: 36px;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    letter-spacing: -0.5px;
}

.ve-nav-title {
    color: white;
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: 0.01em;
}

.ve-nav-links {
    list-style: none;
    margin: 0.75rem 0 0;
    padding: 0 0.75rem;
    flex: 1 1 auto;   /* take the slack so the footer pins to the bottom */
    min-height: 0;
    overflow-y: auto; /* only if the links can't all fit does THIS list scroll */
    overscroll-behavior: contain; /* the drawer's own scroll doesn't chain to the page behind */
}

.ve-nav-links li { margin-bottom: 0.25rem; }

.ve-nav-links a {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.75rem 0.85rem;
    color: rgba(255,255,255,0.72);
    text-decoration: none;
    border-radius: 7px;
    font-size: 0.9rem;
    font-weight: 500;
    transition: background 0.15s, color 0.15s;
    min-height: 44px;
}

.ve-nav-links a:hover {
    background: rgba(255,255,255,0.08);
    color: white;
}

.ve-nav-links a.active {
    background: var(--ve-accent);
    color: white;
}

.nav-icon { font-size: 1rem; }

.ve-nav-footer {
    flex-shrink: 0; /* always visible — never scrolls off the bottom */
    padding: 0.85rem 1rem calc(0.85rem + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid rgba(255,255,255,0.08);
}

.ve-nav-logout {
    display: flex;
    align-items: center;
    min-height: 44px;
    padding: 0.6rem 1rem;
    border-radius: 6px;
    color: rgba(255,255,255,0.55);
    text-decoration: none;
    font-size: 0.9rem;
    transition: background 0.15s, color 0.15s;
}

.ve-nav-logout:hover {
    background: rgba(255,255,255,0.08);
    color: white;
}

/* Ghost button inside the navy nav footer needs light text/border so it
   doesn't disappear into the background. */
.ve-nav-footer .ve-btn-ghost {
    color: #fff;
    background: transparent;
    border: 1px solid rgba(255, 255, 255, 0.45);
}

.ve-nav-footer .ve-btn-ghost:hover,
.ve-nav-footer .ve-btn-ghost:focus-visible {
    background: rgba(255, 255, 255, 0.12);
    border-color: #fff;
    color: #fff;
}

/* ─── Modals ─── */
.ve-modal-backdrop {
    position: fixed;
    inset: 0;
    background: var(--ve-scrim);
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
}

.ve-modal {
    background: white;
    border-radius: 12px;
    padding: 1.5rem;
    width: 100%;
    max-width: 520px;
    max-height: 90vh;
    overflow-y: auto;
    overscroll-behavior: contain; /* a drag past the top of a modal must not reach the page/browser (#141) */
    box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}

/* `.ve-modal-overlay` is an alias for `.ve-modal-backdrop` — some pages
   (PromoCodes, Transactions) reference it; without this they fell back to
   document flow (uncentered, unsized). Keep both names in sync. */
.ve-modal-overlay {
    position: fixed;
    inset: 0;
    background: var(--ve-scrim);
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
}

/* ─── One scrim, and it sits directly under the topmost form (issue #178) ───
   Forms mostly REPLACE one another rather than stack, but two deliberately stack — the
   Stripe add-card pop-up over Buy a Package, and any ConfirmDialog over the form that asked
   — and a second backdrop over the first multiplies the dimming (0.78 twice over is 0.95,
   which is black). js/modalchrome.js marks every backdrop but the topmost one, and only the
   top one paints. The result is what the instruction asks for either way: everything below
   the form in front is darkened exactly once, the form behind included.
   ⚠️ Fails safe. If the script never runs, no element carries the class and every backdrop
   paints its own scrim, which is the behaviour that shipped before this. */
.ve-modal-backdrop.ve-scrim-under,
.ve-modal-overlay.ve-scrim-under,
.ve-dp-backdrop.ve-scrim-under { background: transparent; }

/* ⚠️ The bottom tab bar is OUTSIDE .ve-shell, which is `position: relative; z-index: 1` and
   therefore its own stacking context — so a backdrop's z-index of 1000 counts only INSIDE
   the shell, and the bar's 40 in the root context wins. Measured: with Buy a Package open,
   the bar was fully lit over the scrim and a tap on CLASSES navigated away and took the form
   with it. While a form is open the bar drops below the shell, so the scrim covers it and
   the backdrop takes the tap.
   This is a paint order, NOT a visibility gate: `.ve-tabbar` stays `display: flex`, which is
   the rule issue #155 set and this must not become an exception to. body.ve-modal-open is a
   fresh DOM query in modalchrome.js and cannot become unbalanced, so the bar cannot be left
   behind when the last form closes. */
body.ve-modal-open .ve-tabbar { z-index: 0; }

/* ─── Background scroll lock (docs/modal-background-scroll-lock-handoff.md) ───
   modalchrome.js sets body.ve-modal-open while any .ve-modal-backdrop/-overlay is in
   the DOM. The page behind an open form must not move when the reader grabs it: the
   shell is a fixed 100dvh frame whose ONE scroller is .ve-content, so freeze it (and
   the body, the fallback scroller) at every width — the same proven pattern as
   body.nav-open below. scrollTop is left alone, so closing the form returns the
   reader to the exact place they left; the modal's own scrollers (.ve-modal,
   .ve-modal-body) keep their overflow rules and stay scrollable throughout. */
body.ve-modal-open             { overflow-y: hidden; }
body.ve-modal-open .ve-content { overflow-y: hidden; }

/* ─── Framed modal: pinned header (title + actions) and footer that never
   scroll; only the body scrolls (G4). Structure:
     <div class="ve-modal-backdrop"><div class="ve-modal ve-modal-framed">
        <div class="ve-modal-header"><h2>Title</h2>
            <div class="ve-modal-actions"> …small top buttons… </div></div>
        <div class="ve-modal-body"> …scrolling fields… </div>
        <div class="ve-modal-footer"> …primary / cancel buttons… </div>
     </div></div>
   Use ve-modal-framed when a form is tall enough that Save/Cancel would
   otherwise scroll off. The header/footer stay put at every screen size. */
.ve-modal-framed { padding: 0; display: flex; flex-direction: column; }
.ve-modal-header {
    position: sticky; top: 0; z-index: 2;
    display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
    padding: 0.85rem 1.25rem;
    background: var(--ve-navy);                 /* distinct, non-scrolling header bar — matches the nav */
    border-radius: 12px 12px 0 0;
}
.ve-modal-header h1, .ve-modal-header h2, .ve-modal-header h3 {
    margin: 0; font-size: 1.05rem; line-height: 1.2; min-width: 0; color: #fff;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Buttons sit on the navy bar — invert them so they stay legible. (A button with its
   own inline background, e.g. the destructive Deactivate, keeps that inline colour.) */
.ve-modal-header .ve-btn-ghost { color: #fff; border-color: rgba(255,255,255,0.45); }
.ve-modal-header .ve-btn-ghost:hover { background: rgba(255,255,255,0.14); }
.ve-modal-header .ve-btn-primary { background: #fff; color: var(--ve-navy); }
.ve-modal-header .ve-btn-primary:hover { background: #eef2ff; }
.ve-modal-header .ve-btn-accent { background: #fff; color: var(--ve-accent); }
.ve-modal-header .ve-btn-accent:hover { background: #fff5f5; }
.ve-modal-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; flex-shrink: 0; }
.ve-modal-actions .ve-btn { padding: 0.35rem 0.7rem; font-size: 0.82rem; }
.ve-modal-body { padding: 1.25rem; overflow-y: auto; overscroll-behavior: contain; }
/* Package name moved out of the (now generic "Purchase") modal header into the body. */
.ve-purchase-pkg-name { margin: 0 0 1rem; font-size: 1.15rem; line-height: 1.25; color: var(--ve-navy); }
/* The bottom button bar (.ve-modal-footer) was removed — the sticky navy
   .ve-modal-header above is the single, non-scrolling button bar. */

/* Save gating (modalchrome.js sets data-dirty): the Save button — mark it
   class="… ve-modal-save" — is disabled until the user enters/changes data. New
   forms start empty → disabled until something is typed; edit forms start clean →
   disabled until a field changes. Absent attribute = enabled (safe default). */
.ve-modal-framed[data-dirty="false"] .ve-modal-save {
    opacity: 0.5;
    pointer-events: none;
}

/* ─── Definition list (label/value pairs) ─── */
.ve-dl { display: grid; grid-template-columns: auto 1fr; gap: 0.4rem 1rem; font-size: 0.9rem; }
.ve-dl dt { font-weight: 600; color: var(--ve-muted); white-space: nowrap; }
.ve-dl dd { margin: 0; }

/* ─── Shared form helper ─── */
.ve-label { display: block; font-size: 0.82rem; font-weight: 600; color: var(--ve-muted); margin-bottom: 0.3rem; }

/* ─── Cards ─── */
.ve-card {
    background: var(--ve-surface);
    border: 1px solid var(--ve-border);
    border-radius: 12px;
    padding: 1.5rem;
    box-shadow: 0 1px 4px rgba(0,0,0,0.05);
}

/* ─── Stat tiles ─── */
.ve-stats {
    display: flex;
    gap: 0.3rem;
    margin-bottom: 0;
    flex-wrap: nowrap;
}

/* Banner stat tiles: number LEFT, label RIGHT, minimal vertical height (global
   directive 2026-07-09). Matches the client dashboard's .ve-dash-stat layout. */
.ve-stat {
    flex: 1;
    min-width: 0;
    background: var(--ve-surface);
    border: 1px solid var(--ve-border);
    border-radius: 10px;
    padding: 0.4rem 0.7rem;
    box-shadow: 0 1px 4px rgba(0,0,0,0.05);
    display: flex;
    align-items: center;
    gap: 0.45rem;
}

.ve-stat-value {
    flex: 0 0 auto;
    font-size: 1.3rem;
    font-weight: 700;
    color: var(--ve-navy);
    line-height: 1;
}

.ve-stat-label {
    min-width: 0;
    font-size: 0.66rem;
    color: var(--ve-muted);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ─── Table ─── */
.ve-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9rem;
}

.ve-table th {
    text-align: left;
    padding: 0.65rem 1rem;
    border-bottom: 2px solid var(--ve-border);
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--ve-muted);
    white-space: nowrap;
}

.ve-table td {
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--ve-border);
    vertical-align: middle;
}

.ve-table tbody tr:hover { background: #f8f9fc; }
.ve-table tbody tr:last-child td { border-bottom: none; }

.ve-table th.ve-sortable {
    cursor: pointer;
    user-select: none;
    white-space: nowrap;
}
.ve-table th.ve-sortable:hover { color: var(--ve-navy); }

.ve-filter-row td {
    padding: 0.3rem 0.5rem;
    border-bottom: 2px solid var(--ve-border);
    background: #f8f9fc;
}
.ve-filter-row input,
.ve-filter-row select {
    width: 100%;
    padding: 0.28rem 0.5rem;
    border: 1px solid var(--ve-control-border);
    border-radius: 6px;
    font-size: 0.8rem;
    background: white;
    outline: none;
    transition: border-color 0.15s;
}
.ve-filter-row input:focus,
.ve-filter-row select:focus { border-color: var(--ve-navy); outline: 2px solid var(--ve-rail); outline-offset: 2px; }

/* ─── Pinned list header + filters (G6) ──────────────────────────────────────
   Wrap any .ve-table in <div class="ve-table-wrap"> so the rows scroll inside a
   bounded box while the column headers AND the .ve-filter-row stay pinned at the
   top. Replaces the ad-hoc inline `overflow-x:auto` wrappers. On phones the
   bound is removed (see mobile @media) so the page scrolls normally — the
   filter row is already kept visible there by the table reflow. */
.ve-table-wrap {
    overflow: auto;
    overscroll-behavior: contain; /* #141 — a list that hits its top doesn't hand the drag to the browser */
    max-height: calc(100vh - 14rem);
    border: 1px solid var(--ve-border);
    border-radius: 10px;
    background: var(--ve-surface);
}
/* separate borders so the sticky header keeps a visible divider while scrolling
   (border-collapse:collapse drops shared borders out of the sticky layer) */
.ve-table-wrap > .ve-table { border-collapse: separate; border-spacing: 0; }
.ve-table-wrap .ve-table thead { position: sticky; top: 0; z-index: 2; }
.ve-table-wrap .ve-table thead th {
    background: var(--ve-surface);
    box-shadow: inset 0 -2px 0 var(--ve-border);
    border-bottom: none;
}
.ve-table-wrap .ve-filter-row td {
    box-shadow: inset 0 -2px 0 var(--ve-border);
    border-bottom: none;
}
.ve-table-wrap .ve-table tbody td { border-bottom: 1px solid var(--ve-border); }

/* ─── Date-picker filter cell (D1) — a trigger button that opens DatePickerPopup,
   plus a clear (✕). Add class="ve-filter-datecell" to the filter <td>. ─── */
.ve-filter-row td.ve-filter-datecell { display: flex; align-items: center; gap: 0.25rem; }
.ve-filter-trigger {
    flex: 1; min-width: 0; width: 100%; text-align: left;
    padding: 0.28rem 0.5rem;
    border: 1px solid var(--ve-control-border);
    border-radius: 6px;
    font-size: 0.8rem;
    background: white;
    color: var(--ve-text);
    cursor: pointer;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ve-filter-trigger:hover { border-color: var(--ve-navy); }
.ve-filter-clear {
    flex: 0 0 auto;
    border: none; background: transparent; cursor: pointer;
    color: var(--ve-muted); font-size: 0.95rem; line-height: 1; padding: 0 0.25rem;
}

/* ─── Sticky filter panel (D2) — a toolbar/card of filter controls above a
   page-scrolling list (Schedule filter card, Reports tab cards, the client
   dashboard calendar header). Pins to the top of the scroll area instead of
   scrolling off. Add class="ve-filter-sticky" to the panel. ─── */
.ve-filter-sticky { position: sticky; top: 0; z-index: 5; background: var(--ve-surface); }

/* ─── Single-line list search (issue #37) — one box, literal contains-search across
   every displayed column, rendered by the <ListSearchBox> component. Placed directly
   above a .ve-table-wrap (which scrolls the rows internally) so the search box stays
   put while the data scrolls beneath it, on phone and desktop alike. Replaces the
   per-column .ve-filter-row everywhere. ─── */
.ve-searchbar {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
}
.ve-search-input {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 44px;                 /* min touch target */
    padding: 0.5rem 0.85rem;
    border: 1px solid var(--ve-control-border);
    border-radius: 8px;
    font-size: 0.95rem;
    background: white;
    outline: none;
    transition: border-color 0.15s;
}
.ve-search-input:focus { border-color: var(--ve-navy); outline: 2px solid var(--ve-rail); outline-offset: 2px; }
.ve-search-count {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    line-height: 1.15;
    font-size: 0.82rem;
    color: var(--ve-muted);
    white-space: nowrap;
    text-align: right;
}
.ve-search-count-noun { font-size: 0.75rem; }

/* Multi-state ("cycle") filter button that sits at the left edge of a .ve-searchbar,
   above the column it filters (issue #159). One tap advances to the next phase; the
   current phase is the label, and the fill borrows the matching .badge-* palette so the
   button reads the same colour as the rows it is showing. It ANDs with the text box —
   it is not a second search. */
.ve-cycle-btn {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
    gap: 0.05rem;
    min-height: 44px;                 /* min touch target */
    padding: 0.3rem 0.7rem;
    border: 1px solid var(--ve-control-border);
    border-radius: 8px;
    background: white;
    cursor: pointer;
    line-height: 1.1;
    text-align: left;
    transition: border-color 0.15s, background 0.15s;
}
.ve-cycle-btn:hover  { border-color: var(--ve-navy); }
.ve-cycle-btn:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: 2px; }
.ve-cycle-btn-cap {
    font-size: 0.62rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ve-muted);
}
.ve-cycle-btn-val { font-size: 0.9rem; font-weight: 600; }
/* Phase colours — same palette as the Status badges in the rows below. */
.ve-cycle-btn.is-all         { background: white; }
.ve-cycle-btn.badge-active   { background: #d1fae5; border-color: #6ee7b7; color: #065f46; }
.ve-cycle-btn.badge-inactive { background: #fee2e2; border-color: #fca5a5; color: #991b1b; }
.ve-cycle-btn.badge-pending  { background: #fef3c7; border-color: #fcd34d; color: #92400e; }
.ve-cycle-btn.badge-active .ve-cycle-btn-cap,
.ve-cycle-btn.badge-inactive .ve-cycle-btn-cap,
.ve-cycle-btn.badge-pending .ve-cycle-btn-cap { color: inherit; opacity: 0.75; }
@media (max-width: 767px) {
    .ve-search-count { font-size: 0.75rem; }
}

/* ─── Client type-ahead picker (ClientSelect) — filterable client menu ─── */
.ve-clientselect { position: relative; }
.ve-clientselect-control { position: relative; display: flex; align-items: center; }
.ve-clientselect-input { width: 100%; padding-right: 1.8rem; }
/* Multi-email field (#136): an auto-growing textarea so several addresses stay visible.
   Height is driven by JS (veEmailMulti.autogrow); keep it a comfy touch target and wrap
   long addresses instead of scrolling them out of view. */
.ve-emailmulti {
    min-height: 44px; line-height: 1.45; resize: none; overflow: hidden;
    white-space: pre-wrap; overflow-wrap: anywhere; font-family: inherit;
}
.ve-clientselect-clear {
    position: absolute; right: 0.4rem; top: 50%; transform: translateY(-50%);
    border: none; background: transparent; cursor: pointer;
    color: var(--ve-muted); font-size: 0.95rem; line-height: 1; padding: 0 0.2rem;
}
/* In-flow (not absolute) so a scrolling ancestor like .ve-modal-body can't clip it. */
.ve-clientselect-menu {
    margin-top: 3px;
    max-height: 240px; overflow-y: auto;
    background: white; border: 1px solid var(--ve-border); border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
.ve-clientselect-item {
    display: block; width: 100%; text-align: left;
    padding: 0.55rem 0.7rem; min-height: 40px; border: none; background: transparent;
    cursor: pointer; font-size: 0.9rem; color: var(--ve-text);
}
.ve-clientselect-item:hover { background: #f1f5f9; }
.ve-clientselect-item.is-selected { background: #eef2ff; font-weight: 600; }
.ve-clientselect-none { color: var(--ve-muted); font-style: italic; }
.ve-clientselect-empty { padding: 0.55rem 0.7rem; color: var(--ve-muted); font-size: 0.85rem; }

/* ─── Package cards (issue #20) — replaces .ve-table for member package views.
   Responsive by construction: multiple cards per row on wide screens, a single
   column on phones, never a horizontal scrollbar. ─── */
.ve-pkg-list {
    display: grid;
    /* min(220px,100%) keeps a column from ever being wider than its container,
       so the grid can't overflow and get clipped on narrow screens (issue #20). */
    grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr));
    gap: 0.6rem;
}
/* min-width:0 lets the grid item shrink below its content's intrinsic width;
   overflow-wrap stops a long package name from forcing the card wider. */
.ve-pkg-card   { border: 1px solid var(--ve-border); border-radius: 10px; padding: 0.7rem 0.8rem; min-width: 0; overflow-wrap: anywhere; }
.ve-pkg-name   { font-weight: 600; color: var(--ve-text); }
.ve-pkg-action { margin-top: 0.55rem; }          /* "Request a Booking", left-aligned under the name */
.ve-pkg-meta   {
    margin: 0.6rem 0 0; padding-top: 0.55rem;
    border-top: 1px solid var(--ve-border);
    display: flex; flex-direction: column; gap: 0.3rem;
}
.ve-pkg-row    { display: flex; justify-content: space-between; gap: 1rem; font-size: 0.88rem; }
.ve-pkg-row dt { color: var(--ve-muted); margin: 0; }
.ve-pkg-row dd { margin: 0; font-weight: 600; color: var(--ve-text); }

/* ─── Package banners and the Buy More type accordion ───────────────────────────
   Lived in Packages/Index.razor until issue #142 split that page in two: buying is
   <BuyPackageModal/> now and the staff catalogue stayed behind. Both draw banners, so
   the rules belong here — the same reason .ve-pkg-card above is here rather than in
   PackageList.razor. ─── */

/* The #135 package-type ACCORDION was here and is gone (issue #184): the purchase form
   picks its kind with <PackageTypeSwitch/> now, the same control the Packages tab shows,
   so the styling it needs is .ve-pkg-typeseg further down and nothing of its own. */

/* Package banners (issue #125) — one full-width, tappable row per package. */
.ve-pkg-banners { display:flex; flex-direction:column; gap:0.6rem; }
.ve-pkg-banner {
    display:flex; align-items:center; justify-content:space-between; gap:0.6rem;
    width:100%; min-height:44px; text-align:left; cursor:pointer;
    background:var(--ve-surface); border:1px solid var(--ve-border); border-radius:12px;
    padding:0.9rem 1rem; font:inherit; color:inherit;
    transition:transform 0.1s, box-shadow 0.15s, border-color 0.15s;
}
.ve-pkg-banner:hover { transform:translateY(-1px); box-shadow:0 4px 14px rgba(0,0,0,0.08); border-color:var(--ve-navy); }
.ve-pkg-banner:active { transform:translateY(0); }
.ve-pkg-banner-inactive { opacity:0.6; }
.ve-pkg-banner-main { display:flex; flex-direction:column; gap:0.15rem; min-width:0; }
/* Un-bold titles, kept to a single line (no word wrap). The font steps down on
   narrower phones so the full name still fits; ellipsis is only the last-resort
   guard on an unusually narrow screen (mobile-first, 320px min). */
.ve-pkg-banner-name { font-weight:400; font-size:0.95rem; color:var(--ve-navy);
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ve-pkg-banner-sub { font-size:0.74rem; color:var(--ve-muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* Price stacked over a very small "per class" — right-aligned, so it adds no
   horizontal width (the name keeps its room). */
.ve-pkg-banner-price { display:flex; flex-direction:column; align-items:flex-end; white-space:nowrap; line-height:1.05; }
.ve-pkg-banner-price strong { font-size:1.05rem; color:var(--ve-navy); }
.ve-pkg-banner-perclass { font-style:normal; font-size:0.6rem; color:var(--ve-muted); text-transform:uppercase; letter-spacing:0.03em; margin-top:1px; }
@media (max-width: 430px) { .ve-pkg-banner-name { font-size:0.88rem; } }
@media (max-width: 360px) { .ve-pkg-banner-name { font-size:0.82rem; } }
@media (max-width: 335px) { .ve-pkg-banner-name { font-size:0.72rem; } }

/* #137 — promo input + Apply/Remove row. min-width:0 lets the input shrink
   below its intrinsic width instead of widening the modal; below 430px the
   button wraps to a full-width second row. Font-size is owned by the global
   input-zoom invariant above — do NOT set one here. */
.ve-promo-entry { display:flex; gap:0.5rem; min-width:0; max-width:100%; }
.ve-promo-entry .ve-input { flex:1 1 auto; min-width:0; }
.ve-promo-entry .ve-btn { flex:0 0 auto; }
@media (max-width: 430px) {
    .ve-promo-entry { flex-wrap:wrap; }
    .ve-promo-entry .ve-btn { width:100%; }
}

/* New-version prompt (issue #55) — non-disruptive replacement for the forced reload.
   Fixed thin bar at the bottom; never covers the nav, never steals focus/input. */
.ve-update-banner {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 2000;
    display: flex; align-items: center; justify-content: center;
    gap: 0.75rem; flex-wrap: wrap;
    padding: 0.55rem 1rem;
    background: var(--ve-navy); color: #fff;
    box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.18);
    font-size: 0.9rem;
}
.ve-update-banner-text    { font-weight: 600; }
.ve-update-banner-actions { display: flex; align-items: center; gap: 0.5rem; }
.ve-update-banner-refresh {
    background: #fff; color: var(--ve-navy); border: none; border-radius: 8px;
    padding: 0.4rem 1rem; font-weight: 700; cursor: pointer;
    min-height: 44px; min-width: 44px;
}
.ve-update-banner-refresh:hover { background: #eef1f8; }

/* Tappable package card — drill into the classes that used its credits (issue #49). */
.ve-pkg-clickable { cursor: pointer; transition: transform 0.1s, box-shadow 0.15s; }
.ve-pkg-clickable:hover { transform: translateY(-2px); box-shadow: 0 6px 18px rgba(0,0,0,0.1); }
.ve-pkg-history-hint { margin-top: 0.5rem; font-size: 0.8rem; font-weight: 600; color: var(--ve-navy); text-align: right; }

/* Issue #116 — "show expired / used-up" filter toggle + dimmed inactive cards.
   Expired & used-up packages are hidden by default; the toggle reveals them, and each
   revealed card is dimmed with a neutral "Expired"/"Used up" pill so its state is clear. */
.ve-pkg-filter {
    display: inline-flex; align-items: center; gap: 0.5rem;
    margin: 0 0 0.8rem; min-height: 44px;
    font-size: 0.9rem; color: var(--ve-muted);
    cursor: pointer; user-select: none;
}
.ve-pkg-filter input { width: 18px; height: 18px; cursor: pointer; }
.ve-pkg-inactive { opacity: 0.6; }
.ve-pkg-inactive:hover { opacity: 0.85; }
.ve-pkg-badge { margin-left: 0.45rem; vertical-align: middle; background: #eef1f5; color: #64748b; }

/* ─── Private (Personal Training) session badge — issue #23 ───
   Marks a class as a private session wherever a client sees it (calendar events
   + class detail). Private session = ClassDto.IsPublic == false. */
.ve-private-tag {
    display: inline-flex;
    align-items: center;
    gap: 0.25em;
    font-size: 0.68rem;
    font-weight: 700;
    line-height: 1;
    padding: 0.2em 0.55em;
    border-radius: 999px;
    background: var(--ve-navy);
    color: #fff;
    letter-spacing: 0.02em;
    white-space: nowrap;
    vertical-align: middle;
}

/* ─── Coach Check-In / Attendance / Confirm panel (issue #2) ─── */
.ve-coach-head {
    border: 1px solid var(--ve-border);
    border-radius: 10px;
    padding: 0.85rem 1rem;
    margin-bottom: 0.85rem;
}
.ve-checkin-row,
.ve-confirm-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
}
.ve-checkin-row { margin-bottom: 0.85rem; }
.ve-confirm-row { margin-top: 0.85rem; }
.ve-stamp { font-size: 0.82rem; color: var(--ve-text); }
.ve-stamp-muted { color: var(--ve-muted); font-style: italic; }

/* Inline text-button + collapsible section header (issue #26; also used by the
   Dashboard class modal). Global so every surface styles them consistently. */
.ve-link-btn { background:none;border:0;padding:0;color:var(--ve-navy);text-decoration:underline;cursor:pointer;font:inherit; }
.ve-structure-toggle { display:flex;justify-content:space-between;align-items:center;width:100%;background:none;border:0;padding:0 0 0.4rem;cursor:pointer; }

/* Issue #38 — class-level ("main") video. A centred "Watch Video" link sits
   directly under the class-detail title bar on BOTH class-detail surfaces
   (the /schedule/{id} page and the Dashboard modal). */
.ve-watch-video-row { display: flex; justify-content: center; margin: 0 0 1.1rem; }
.ve-watch-video {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 44px;                       /* touch target */
    padding: 0.4rem 1.2rem;
    background: none;
    border: 1px solid var(--ve-control-border);
    border-radius: 999px;
    color: var(--ve-navy);
    font: inherit;
    font-weight: 600;
    line-height: 1;
    cursor: pointer;
}
.ve-watch-video:hover { background: var(--ve-bg); border-color: var(--ve-navy); }
.ve-watch-video svg { flex: none; }

/* The player itself. Controls are hidden during playback (js/videoplayer.js), so
   they can't obscure the picture (#38); while paused they overlay the bottom edge,
   which is fine because that's when the user wants them. No reserved strip needed.

   SIZE FROM THE CLIP, NOT FROM THE MODAL. This was `width:100%` with a max-height,
   which pins the element to the modal's shape and lets object-fit letterbox the
   video inside a box that has nothing to do with it. Coaches shoot on a phone, so
   the library is full of PORTRAIT clips (the sample is 2160x3840): in a landscape
   box that renders as a thin strip in a sea of black — measured at 672x294 in an
   installed PWA window, which is the "1 inch tall, 4 inches wide, sliver of video"
   report.

   width/height auto with BOTH caps is the replaced-element sizing rule: the browser
   keeps the intrinsic ratio and shrinks until it satisfies max-width and max-height
   together. Portrait and landscape both come out whole and as large as they fit. */
.ve-video-player {
    display: block;
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 70vh;
    margin: 0 auto;          /* a portrait clip is narrower than the modal — centre it */
    background: #000;
    border-radius: 8px;
    object-fit: contain;
}

/* Let the player modal hug the clip instead of holding a fixed 720px frame around a
   narrow portrait video. min-width keeps the header/title usable when the clip is
   very narrow. */
.ve-video-modal {
    width: fit-content;
    min-width: min(320px, 100%);
}

/* Admin video-editor item (VideoEditorRow), #38. Line 1 = title + (green file name OR a
   right-aligned "Add Video" button, so no second row is wasted when there's no video);
   line 2 = the actions when a video exists. The green file name replaces the old
   "Has video" badge — it's self-explanatory. The name wraps in full, never truncates. */
.ve-video-card { margin-bottom: 0.6rem; }
.ve-video-item { padding: 0.15rem 0; }
.ve-video-head { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.ve-video-title { font-size: 0.95rem; font-weight: 600; color: var(--ve-navy); }
.ve-video-detail { font-size: 0.82rem; color: var(--ve-muted); }
.ve-video-name {
    font-size: 0.85rem; font-weight: 600; color: #065f46;   /* same green as the badge-active banner */
    word-break: break-all; min-width: 0;
}
.ve-video-add { margin-left: auto; }   /* push "Add Video" to the right of the title line */
.ve-video-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; margin-top: 0.4rem; }
.ve-file-btn { position: relative; cursor: pointer; }
.ve-file-btn input[type="file"] { display: none; }
.ve-file-btn.is-busy { opacity: 0.6; pointer-events: none; }
.ve-video-msg { font-size: 0.82rem; margin: 0.35rem 0 0; }

/* Chunked upload progress + resume (#38). The bar is driven by ACKNOWLEDGED bytes —
   what the server confirms it has committed — so it can never reach 100% and then
   fail. Cancel and Resume are 44px targets like every other control. */
.ve-upload, .ve-upload-resume {
    display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.4rem;
}
.ve-upload-bar {
    flex: 1 1 120px; min-width: 120px; height: 6px;
    background: var(--ve-border); border-radius: 3px; overflow: hidden;
}
.ve-upload-fill {
    height: 100%; background: #065f46; border-radius: 3px;
    transition: width 0.2s ease-out;
}
.ve-upload-text { font-size: 0.82rem; color: var(--ve-muted); }
.ve-upload-cancel { color: var(--ve-accent); }

/* Player failure message (#38 P3). Shown ONLY for a real fault — a blocked autoplay
   on iOS is routine and gets no panel, or the panel stops meaning anything. */
.ve-video-error {
    display: flex; align-items: center; justify-content: space-between; gap: 0.6rem;
    flex-wrap: wrap; margin-top: 0.6rem; padding: 0.6rem 0.75rem;
    background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px;
    font-size: 0.85rem; color: #991b1b;
}

/* Session-structure disclosure — collapsed by default so a class card stays ~1–2 lines
   and many classes fit on one page; the badge flags any videos hidden inside (#38). */
.ve-disclosure {
    display: inline-flex; align-items: center; gap: 0.4rem;
    background: none; border: none; cursor: pointer;
    padding: 0.4rem 0 0.1rem; margin-top: 0.15rem;
    font-size: 0.82rem; font-weight: 600; color: var(--ve-navy);
}
.ve-disclosure-caret { font-size: 0.7rem; color: var(--ve-muted); }
.ve-disclosure-none { font-size: 0.78rem; font-weight: 400; color: var(--ve-muted); }
.ve-video-blocks { padding-left: 0.75rem; border-left: 2px solid var(--ve-border); margin-top: 0.25rem; }

/* Compact roster that fits a phone width without horizontal scroll —
   deliberately NOT .ve-table (which forces min-width:480px on mobile). */
.ve-coach-roster {
    width: 100%;
    border-collapse: collapse;
    border: 1px solid var(--ve-border);
    border-radius: 8px;
    font-size: 0.8rem;
    overflow: hidden;
}
.ve-coach-roster th {
    text-align: left;
    padding: 0.4rem 0.55rem;
    font-size: 0.66rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--ve-muted);
    border-bottom: 1px solid var(--ve-border);
    white-space: nowrap;
}
.ve-coach-roster td {
    padding: 0.4rem 0.55rem;
    border-bottom: 1px solid var(--ve-border);
    vertical-align: middle;
    white-space: nowrap;
}
.ve-coach-roster tbody tr:last-child td { border-bottom: none; }
.ve-roster-name    { font-weight: 500; }
.ve-roster-credits { text-align: center; width: 1%; }
.ve-roster-att     { text-align: right; width: 1%; white-space: nowrap; }

/* Narrow Attended / No Show toggle buttons — no wrap, compact, fit a phone row. */
.ve-att-btn {
    display: inline-block;
    min-height: 38px;
    padding: 0.25rem 0.5rem;
    margin-left: 0.3rem;
    font-size: 0.72rem;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    border: 1px solid var(--ve-control-border);
    border-radius: 6px;
    background: #eef1f7;
    color: var(--ve-navy);
    cursor: pointer;
    transition: background 0.12s, color 0.12s;
}
.ve-att-btn:first-child { margin-left: 0; }
.ve-att-btn.is-on {
    background: var(--ve-navy);
    color: #fff;
    border-color: var(--ve-navy);
}
.ve-att-btn:disabled { opacity: 0.55; cursor: default; }

/* ─── Unsaved attendance state (issue #153 A-1) ───
   Issue #153: a local Attended/No Show selection looked exactly like a saved one, so a
   change that was never written read as saved and silently reverted on re-entry. The
   amber edge and the "Unsaved" tag exist to make that difference visible; the note above
   Confirm names the only control that writes. Amber, not red — nothing is wrong yet. */
.ve-coach-roster tr.ve-roster-dirty td {
    background: #fffaf0;
}
.ve-coach-roster tr.ve-roster-dirty td:first-child {
    box-shadow: inset 3px 0 0 0 #d97706;
}
.ve-unsaved-tag {
    display: inline-block;
    margin-left: 0.45rem;
    padding: 0.08rem 0.36rem;
    border: 1px solid #d97706;
    border-radius: 999px;
    background: #fef3c7;
    color: #92400e;
    font-size: 0.64rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    vertical-align: middle;
    white-space: nowrap;
}
.ve-unsaved-note {
    margin: 0.75rem 0 0;
    padding: 0.45rem 0.6rem;
    border-left: 3px solid #d97706;
    border-radius: 4px;
    background: #fffaf0;
    color: #92400e;
    font-size: 0.82rem;
    font-weight: 600;
}
.ve-lock-note {
    margin: 0.75rem 0 0;
    font-size: 0.82rem;
    color: var(--ve-muted);
}

/* Staged push-notification choice (issue #153 D9e) — selected here, written by Save. */
.ve-push-toggle {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    min-height: 44px;
    font-size: 0.9rem;
    cursor: pointer;
}
.ve-push-toggle input { width: 20px; height: 20px; flex: 0 0 auto; }
.ve-push-pending {
    margin: 0.15rem 0 0;
    font-size: 0.82rem;
    font-weight: 600;
    color: #92400e;
}

/* ─── Roster contact popup (issue #57) ─── */
.ve-roster-namelink {
    background: none; border: none; cursor: pointer;
    padding: 0.55rem 0; margin: -0.55rem 0;      /* ~44px tap height without inflating the row */
    font: inherit; font-weight: 500; text-align: left;
    color: var(--ve-navy); text-decoration: underline; text-underline-offset: 2px;
}
.ve-contact-modal { max-width: 420px; }
.ve-contact-rows { display: flex; flex-direction: column; gap: 0.55rem; }
.ve-contact-row {
    display: flex; align-items: center; gap: 0.8rem;
    min-height: 44px; padding: 0.6rem 0.85rem;
    border: 1px solid #dbe2ec; border-radius: 10px; background: #fff;
    font: inherit; color: inherit; text-decoration: none; text-align: left;
    cursor: pointer; width: 100%;
}
.ve-contact-row:hover { background: #f5f7fb; }
.ve-contact-row.is-empty { color: var(--ve-muted); cursor: default; background: #fafbfd; }
.ve-contact-row.is-empty:hover { background: #fafbfd; }
.ve-contact-ico { font-size: 1.2rem; flex-shrink: 0; }
.ve-contact-label {
    display: block; font-size: 0.7rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.04em; color: var(--ve-muted);
}
.ve-contact-meta { font-size: 0.85rem; margin: 0 0 0.5rem; overflow-wrap: anywhere; }
.ve-contact-compose textarea {
    width: 100%; min-height: 8.5rem; padding: 0.6rem 0.7rem;
    border: 1px solid #dbe2ec; border-radius: 8px; font: inherit; resize: vertical;
}
.ve-contact-note { font-size: 0.78rem; color: var(--ve-muted); margin: 0.35rem 0 0; }
.ve-contact-sent { font-size: 0.92rem; font-weight: 600; color: green; margin: 0.25rem 0 0.75rem; }
.ve-contact-btns { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 0.9rem; }
.ve-contact-btns .ve-btn { min-height: 44px; }

.ve-row-link { cursor: pointer; }

/* ─── Badges ─── */
.badge {
    display: inline-block;
    padding: 0.2em 0.65em;
    border-radius: 20px;
    font-size: 0.75rem;
    font-weight: 600;
}

.badge-active   { background: #d1fae5; color: #065f46; }
.badge-inactive { background: #fee2e2; color: #991b1b; }
.badge-pending  { background: #fef3c7; color: #92400e; }
.badge-booked   { background: #dbeafe; color: #1e40af; }
.badge-waitlist { background: #f3e8ff; color: #6b21a8; }
.badge-attended { background: #d1fae5; color: #065f46; }

/* ─── Page header ─── */
.ve-page-header {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1.5rem;
}

.ve-page-header h1 {
    margin: 0;
    font-size: 1.5rem;
    font-weight: 700;
}

/* ─── Search bar ─── */
.ve-search {
    display: flex;
    gap: 0.5rem;
    margin-bottom: 1rem;
}

.ve-search input {
    flex: 1;
    padding: 0.5rem 0.85rem;
    border: 1px solid var(--ve-control-border);
    border-radius: 8px;
    font-size: 0.9rem;
    outline: none;
    transition: border-color 0.15s;
}

.ve-search input:focus { border-color: var(--ve-navy); outline: 2px solid var(--ve-rail); outline-offset: 2px; }

/* ─── Buttons ─── */
.ve-btn {
    padding: 0.5rem 1.1rem;
    border-radius: 8px;
    border: none;
    cursor: pointer;
    font-size: 0.88rem;
    font-weight: 600;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    transition: background 0.15s, transform 0.1s, box-shadow 0.15s;
}

.ve-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}

.ve-btn:active {
    transform: translateY(0);
    box-shadow: none;
}

/* Disabled buttons must read as inactive — e.g. "Save Changes" until the form
   is actually edited. Without this a disabled .ve-btn looked fully enabled. */
.ve-btn:disabled,
.ve-btn:disabled:hover {
    opacity: 0.45;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
}

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

.ve-btn-primary:hover { background: var(--ve-navy-dark); }

.ve-btn-accent {
    background: var(--ve-accent);
    color: white;
}

.ve-btn-accent:hover { background: var(--ve-accent-hover); }

.ve-btn-ghost {
    background: transparent;
    color: var(--ve-navy);
    border: 1px solid var(--ve-control-border);
}

.ve-btn-ghost:hover { background: #f0f2f8; }

.ve-btn-signup {
    background: #dc2626;
    color: #fff;
    border: 1px solid #dc2626;
}

.ve-btn-signup:hover { background: #b91c1c; border-color: #b91c1c; color: #fff; }

/* Compact variant for placement inside the topbar */
.ve-btn-compact {
    padding: 0.3rem 0.75rem;
    font-size: 0.82rem;
    min-height: 0;
}

/* ─── Loading ─── */
.ve-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 3rem;
    color: var(--ve-muted);
    font-size: 0.95rem;
}

/* ─── Empty state ─── */
.ve-empty {
    text-align: center;
    padding: 3rem;
    color: var(--ve-muted);
}

/* ─── Stat tile interactions ─── */
.ve-stat-link { cursor: pointer; transition: transform 0.1s, box-shadow 0.15s; }
.ve-stat-link:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.ve-stat-link.ve-stat-active { border-color: var(--ve-navy); box-shadow: 0 0 0 2px var(--ve-navy) inset; }

/* ─── Admin stats bar ─── */
.ve-statsbar { margin-bottom: 0.3rem; }
.ve-statsbar-panel {
    background: var(--ve-surface);
    border: 1px solid var(--ve-border);
    border-radius: 10px;
    padding: 0.75rem 1rem;
    margin-top: 0.5rem;
    box-shadow: 0 1px 4px rgba(0,0,0,0.05);
}

/* ─── Grid helper ─── */
.ve-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
.ve-grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.5rem; }

/* ─── Mobile nav controls (hidden on desktop) ─── */
.ve-hamburger {
    display: none;
    background: none;
    border: none;
    font-size: 1.5rem;
    cursor: pointer;
    color: var(--ve-navy);
    padding: 0;
    min-height: 36px;   /* compact top bar (user directive) — below the 44px guideline */
    min-width: 36px;
    align-items: center;
    justify-content: center;
}

.ve-nav-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.4);
    z-index: 99;
    touch-action: none; /* a drag on the dimmer must not scroll-through to the page (iOS) */
}

.ve-nav-close-btn {
    display: none;
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    background: none;
    border: none;
    color: rgba(255,255,255,0.65);
    font-size: 1.1rem;
    cursor: pointer;
    padding: 0;
    min-height: 44px;
    min-width: 44px;
    align-items: center;
    justify-content: center;
    border-radius: 4px;
}

.ve-nav-close-btn:hover { color: white; background: rgba(255,255,255,0.15); }

@media (max-width: 768px) {
    /* ── Nav overlay ── */
    .ve-hamburger     { display: inline-flex; }
    .ve-nav-close-btn { display: inline-flex; }

    .ve-nav {
        transform: translateX(-100%);
        transition: transform 0.25s ease;
        z-index: 200;
    }

    body.nav-open .ve-nav          { transform: translateX(0); }
    body.nav-open .ve-nav-backdrop { display: block; }

    /* Freeze the page behind the open drawer. iOS Safari lets a touch-drag
       scroll-through a fixed overlay (and rubber-band the fixed header) unless
       the content scroller itself is locked while the drawer is open. Desktop
       Chrome's device emulator does not reproduce this, so it must be tested on
       a real iPhone. */
    body.nav-open .ve-content { overflow: hidden; }

    /* Main takes full width; overflow-x hidden prevents body-level horizontal scroll */
    .ve-main { margin-left: 0; overflow-x: hidden; }

    /* ── Grids ── */
    .ve-grid-2, .ve-grid-3 { grid-template-columns: 1fr; }
    .ve-field-row           { grid-template-columns: 1fr; }

    /* Topbar mobile overrides live at the end of the file so they fall
       AFTER the base .ve-topbar* desktop rules in source order. */

    /* ── Page structure ── */
    /* Pin the scroll area to the real screen edges: minimal gutter + the
       device safe-area insets (notch in landscape, home indicator at bottom). */
    .ve-content {
        padding-top: 0.65rem;
        padding-bottom: calc(0.65rem + env(safe-area-inset-bottom, 0px));
        padding-left:  calc(var(--ve-gutter) + env(safe-area-inset-left, 0px));
        padding-right: calc(var(--ve-gutter) + env(safe-area-inset-right, 0px));
    }
    .ve-page-header h1 { font-size: 1.2rem; }

    /* ── Stats — wrap to 2-column ── */
    .ve-stats { flex-wrap: wrap; margin-bottom: 0; }
    .ve-stat  { min-width: calc(50% - 0.15rem); }

    /* Tighter card padding on phones. NO overflow here on purpose: overflow-x:auto
       forces overflow-y to compute to `auto`, turning EVERY card into a scroll
       container — and on WebKit a scroll-container grid item gets clipped to its
       track, which lopped the lower rows off the My Packages card (bottom packages
       cut off). Inherently-wide content scrolls in its OWN wrapper instead
       (.ve-week-scroll for the 770px week grid; the inline overflow-x:auto divs),
       and .ve-content's minmax(0,1fr) grid track + overflow-x:hidden is the
       page-level net — so a blanket card overflow is unnecessary and harmful. */
    .ve-card     { padding: 0.7rem 0.75rem; }

    /* ── Package cards — one column on phones; never wider than the viewport ── */
    .ve-pkg-list { grid-template-columns: 1fr; }

    /* ── Tables reflow to stacked cards on phones (no horizontal scroll) ──
       Every row becomes a bordered card and every cell shows its column name
       from data-label="Col" — add that attribute to each <td>. Cells with no
       data-label (or data-label="") render label-less and left-aligned, which
       suits action/button cells. */
    .ve-table, .ve-table tbody, .ve-table tr, .ve-table td { display: block; }
    .ve-table        { min-width: 0; }

    /* Issue #37 — keep per-column filters usable on phones. Hide only the
       header label row (its labels move to each cell's data-label) and keep
       the .ve-filter-row, reflowed into a stacked, labelled block. Filter
       cells carry their own data-label so each control is captioned; empty
       filter cells (e.g. an actions column) collapse out. */
    .ve-table thead tr:not(.ve-filter-row) { display: none; }
    .ve-filter-row {
        display: block;
        border: 1px solid var(--ve-border);
        border-radius: 10px;
        padding: 0.35rem 0.6rem;
        margin-bottom: 0.6rem;
        background: var(--ve-surface);
    }
    /* G3 — label LEFT, input RIGHT (side by side), not stacked. They fit on one
       line at phone widths, so don't waste vertical space stacking them. */
    .ve-filter-row td {
        display: flex;
        flex-direction: row;
        align-items: center;
        gap: 0.5rem;
        padding: 0.1rem 0;
        border: none;
    }
    .ve-filter-row td:empty { display: none; }
    .ve-filter-row td::before {
        content: attr(data-label);
        flex: 0 0 40%;
        font-weight: 600;
        font-size: 0.82rem;
        color: var(--ve-muted);
    }
    /* cells with no caption (e.g. an actions column) let the control fill the row */
    .ve-filter-row td[data-label=""]::before,
    .ve-filter-row td:not([data-label])::before { content: none; }
    .ve-filter-row input,
    .ve-filter-row select,
    .ve-filter-row .ve-filter-trigger {
        flex: 1; min-width: 0; width: 100%; box-sizing: border-box;
        /* Minimum height = font line box + a hair of padding (no 44px tap target).
           0.8rem font × 1.3 line-height + 2×0.15rem padding ≈ 1.34rem ≈ ~21px. */
        min-height: 0;
        line-height: 1.3;
        padding: 0.15rem 0.45rem;
    }
    .ve-table tbody tr {
        border: 1px solid var(--ve-border);
        border-radius: 10px;
        padding: 0.4rem 0.7rem;
        margin-bottom: 0.5rem;
    }
    .ve-table tbody tr:last-child { margin-bottom: 0; }
    .ve-table tbody tr:hover      { background: transparent; }
    .ve-table td {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
        gap: 1rem;
        padding: 0.25rem 0;
        border: none;
        white-space: normal;
        text-align: right;
    }
    .ve-table td::before {
        content: attr(data-label);
        font-weight: 600;
        color: var(--ve-muted);
        text-align: left;
        white-space: nowrap;
    }
    .ve-table td[data-label=""],
    .ve-table td:not([data-label])          { justify-content: flex-start; text-align: left; }
    .ve-table td[data-label=""]::before,
    .ve-table td:not([data-label])::before  { content: none; }

    /* ── Touch targets ── */
    .ve-btn        { min-height: 44px; }
    .ve-btn-sm     { min-height: 44px; min-width: 44px; }
    .ve-input,
    .ve-field input { min-height: 44px; }
    /* Filter controls are deliberately EXCLUDED from the 44px tap target: a filter
       row stacks many controls into a compact block, so their height is minimized to
       the font's line box (see the .ve-filter-row input rule above). */

    /* Auth-page/card phone overrides live in the TRAILING @media block (end of
       file). Their base rules are defined later, so an override here loses on
       source order. See the SOURCE-ORDER RULE in the layout cheat-sheet. */

    /* ── Definition lists — stack label above value ── */
    .ve-dl    { grid-template-columns: 1fr; }
    .ve-dl dt { white-space: normal; }

    /* ── …except .ve-dl-inline: keep label left / value right at every size ── */
    .ve-dl-inline    { grid-template-columns: auto 1fr; }
    .ve-dl-inline dt { white-space: nowrap; }

    /* ── Modals — reduce viewport bleed ── */
    .ve-modal-backdrop,
    .ve-modal-overlay  { padding: 0.5rem; }
    .ve-modal          { max-width: 100%; }
    .ve-modal-header   { padding: 0.7rem 0.9rem; }
    .ve-modal-body     { padding: 0.9rem; }
    .ve-modal-footer   { padding: 0.7rem 0.9rem; }

    /* ── List wrapper on phones (G10) — pin the filter row at the top and let the
       data scroll in a WINDOW directly below it: a vertical (y-axis) split, NOT a
       z-index sticky overlay (rows never slide behind a floating filter). The wrap
       is a flex column bounded to the viewport; the reflowed <thead> (the filter
       card) is the fixed top, <tbody> is the scroll region. The 11rem allows for
       the topbar + page header above the list — tune if a page stacks more above. */
    .ve-table-wrap {
        display: flex;
        flex-direction: column;
        max-height: calc(100vh - 11rem);
        max-height: calc(100dvh - 11rem);
        overflow: hidden;          /* the inner tbody scrolls, not the wrap */
        border: none;
        border-radius: 0;
        background: transparent;
    }
    .ve-table-wrap > .ve-table { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
    .ve-table-wrap > .ve-table > thead { flex: 0 0 auto; }              /* filter card: pinned */
    .ve-table-wrap > .ve-table > tbody {
        flex: 1 1 auto; min-height: 0;
        overflow-y: auto; overflow-x: hidden;       /* the data window */
        overscroll-behavior: contain;               /* #141 — no pull-to-refresh out of the row list */
        -webkit-overflow-scrolling: touch;
    }
}

@media (max-width: 360px) {
    /* Only stack stats to single column on very small phones (< 360px) */
    .ve-stat { min-width: 100%; }
    .ve-page-header h1 { font-size: 1.05rem; }
}

h1:focus { outline: none; }

/* ─── Onboarding header bar (AuthLayout) ─── */
.ve-onboarding-bar {
    flex-shrink: 0;           /* pinned header: never compresses */
    position: sticky;         /* and stays put if the document ever has to scroll */
    top: 0;
    z-index: 50;
    background: var(--ve-navy);
    padding: calc(0.3rem + env(safe-area-inset-top, 0px)) 0.75rem 0.3rem;
    display: flex;
    align-items: center;
    justify-content: flex-end;
}

/* Sign Out rendered as a button on the navy bar — light outline so it reads on dark. */
.ve-onboarding-bar .ve-btn-ghost {
    color: #fff;
    background: transparent;
    border: 1px solid rgba(255,255,255,0.45);
    padding: 0.35rem 0.95rem;
    font-size: 0.82rem;
}

.ve-onboarding-bar .ve-btn-ghost:hover,
.ve-onboarding-bar .ve-btn-ghost:focus-visible {
    background: rgba(255,255,255,0.12);
    border-color: #fff;
    color: #fff;
}

/* ─── Top bar (MainLayout) ─── */
.ve-topbar {
    background: var(--ve-surface);
    border-bottom: 1px solid var(--ve-border);
    padding: 0.1rem 2rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    font-size: 0.85rem;
    min-height: 0;   /* hug the 36px hamburger/account controls — barely taller */
    flex-shrink: 0; /* fixed header: keep its height; never compress under tall content */
}

.ve-topbar-left {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    align-items: center;
}

.ve-topbar-right {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5rem;
}

/* Topbar-native buttons — sized to sit inside the header without crowding each other. */
.ve-topbar-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.4rem 0.85rem;
    border-radius: 6px;
    font-size: 0.82rem;
    font-weight: 600;
    line-height: 1.15;
    white-space: nowrap;
    flex-shrink: 0;
    cursor: pointer;
    text-decoration: none;
    transition: background 0.15s, border-color 0.15s, color 0.15s, box-shadow 0.15s;
}

.ve-topbar-btn-signup {
    background: #dc2626;
    color: #fff;
    border: 1px solid #dc2626;
    box-shadow: 0 1px 2px rgba(220, 38, 38, 0.25);
}
.ve-topbar-btn-signup:hover,
.ve-topbar-btn-signup:focus-visible {
    background: #b91c1c;
    border-color: #b91c1c;
    color: #fff;
}

.ve-topbar-btn-ghost {
    background: transparent;
    color: var(--ve-navy);
    border: 1px solid var(--ve-control-border);
}
.ve-topbar-btn-ghost:hover,
.ve-topbar-btn-ghost:focus-visible {
    background: #f0f2f8;
    border-color: var(--ve-navy);
}
/* Focus needs an indicator of its own. With the resting boundary now Graphite, the
   border-colour move to Night ink above is a 1.34:1 change nobody can see (#155 §4.3).
   Split out of the :hover rule so the rail appears on focus only, never on hover. */
.ve-topbar-btn-ghost:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: 2px; }

/* Open Door — client door unlock (issue #24). Filled navy so it reads as the primary
   member action next to the kebab; the transient outcome recolors it green/red. */
.ve-topbar-btn-door {
    gap: 0.35rem;
    background: var(--ve-navy);
    color: #fff;
    border: 1px solid var(--ve-navy);
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.25);
}
.ve-topbar-btn-door:hover:not(:disabled),
.ve-topbar-btn-door:focus-visible {
    filter: brightness(1.1);
    color: #fff;
}
.ve-topbar-btn-door:disabled {
    opacity: 0.7;
    cursor: default;
}
.ve-topbar-btn-door.is-ok {
    background: #16a34a;
    border-color: #16a34a;
}
.ve-topbar-btn-door.is-err {
    background: #dc2626;
    border-color: #dc2626;
}
.ve-door-icon { flex-shrink: 0; }

/* ─── Top-bar account menu (triple-dot / kebab) ─── */
.ve-account {
    position: relative;
    z-index: 150;          /* keep the toggle + menu above the dismiss backdrop */
    flex-shrink: 0;
}

.ve-account-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;           /* compact top bar (user directive) — below the 44px guideline */
    height: 36px;
    padding: 0;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 8px;
    color: var(--ve-navy);
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}
.ve-account-toggle:hover,
.ve-account-toggle:focus-visible,
.ve-account-toggle[aria-expanded="true"] {
    background: #f0f2f8;
    border-color: var(--ve-control-border);
}

/* Transparent full-viewport catcher: any outside click closes the menu. */
.ve-account-backdrop {
    position: fixed;
    inset: 0;
    z-index: 140;
    background: transparent;
}

.ve-account-menu {
    position: absolute;
    top: calc(100% + 0.4rem);
    right: 0;
    min-width: 230px;
    max-width: calc(100vw - 1.5rem);
    background: var(--ve-surface);
    border: 1px solid var(--ve-border);
    border-radius: 10px;
    box-shadow: 0 8px 28px rgba(17, 27, 54, 0.18);
    padding: 0.35rem;
    z-index: 150;
}

.ve-account-head {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    padding: 0.6rem 0.7rem;
    border-bottom: 1px solid var(--ve-border);
    margin-bottom: 0.3rem;
}
.ve-account-name {
    font-weight: 700;
    color: var(--ve-navy);
    font-size: 0.95rem;
    word-break: break-word;
}
.ve-account-email {
    color: var(--ve-muted);
    font-size: 0.82rem;
    word-break: break-all;
}

.ve-account-item {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    min-height: 44px;      /* min touch target */
    padding: 0.5rem 0.7rem;
    border-radius: 7px;
    color: var(--ve-text);
    font-size: 0.92rem;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
}
.ve-account-item:hover,
.ve-account-item:focus-visible {
    background: #f0f2f8;
    color: var(--ve-navy);
}
.ve-account-item-icon {
    flex-shrink: 0;
    color: var(--ve-muted);
}
.ve-account-item:hover .ve-account-item-icon,
.ve-account-item:focus-visible .ve-account-item-icon {
    color: var(--ve-navy);
}

/* ─── Auth pages ─── */
/* Auth/onboarding frame — compact-fit, NON-scrolling viewport column. The
   Sign-out bar is pinned at top; .ve-auth-page fills the rest. There is NO
   scroll region: the forms are sized to fit, so there are zero scrollbars.
   min-height (not height + overflow:hidden) means a viewport genuinely too short
   for the form grows the document — a single, last-resort scroll, never nested. */
.ve-auth-shell {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    min-height: 100dvh;
}

.ve-auth-page {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--ve-bg);
    padding: 2rem 1rem;
}

.ve-auth-card {
    background: var(--ve-surface);
    border: 1px solid var(--ve-border);
    border-radius: 16px;
    padding: 2.5rem;
    width: 100%;
    max-width: 440px;
    box-shadow: 0 4px 24px rgba(0,0,0,0.08);
}

.ve-auth-page-wide  { align-items: flex-start; padding-top: 3rem; }
.ve-auth-card-wide  { max-width: 720px; }

/* #87 — auth card with a modal-style header bar (title + actions) instead of the
   centered brand. Card padding moves onto the body so the navy header bleeds to the
   rounded edges; overflow:hidden clips it to the card radius. Header is static here
   (not sticky) so it never overlaps the top Sign-out bar. */
.ve-auth-card.ve-auth-card-framed { padding: 0; overflow: hidden; }  /* compound selector so it beats the mobile .ve-auth-card padding */
.ve-auth-card-framed .ve-modal-header { position: static; }
.ve-auth-card-body { padding: 2.5rem; }

.ve-auth-brand {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.75rem;
    margin-bottom: 2rem;
    text-align: center;
}

.ve-auth-brand h1 { margin: 0; font-size: 1.4rem; }

.ve-auth-phone {
    color: var(--ve-accent);
    font-size: 1rem;
    font-weight: 600;
    text-decoration: none;
    white-space: nowrap;
}
.ve-auth-phone:hover { text-decoration: underline; }

.ve-field { margin-bottom: 1rem; }
.ve-field label {
    display: block;
    font-size: 0.85rem;
    font-weight: 600;
    margin-bottom: 0.35rem;
    color: var(--ve-text);
}

.ve-field input, .ve-input {
    width: 100%;
    padding: 0.55rem 0.85rem;
    border: 1px solid var(--ve-control-border);
    border-radius: 8px;
    font-size: 1rem;   /* 16px — under 16px iOS auto-zooms on focus (see trailing @media note) */
    outline: none;
    transition: border-color 0.15s;
    box-sizing: border-box;
}

.ve-field input:focus, .ve-input:focus { border-color: var(--ve-navy); outline: 2px solid var(--ve-rail); outline-offset: 2px; }
/* Checkboxes / radios must NOT take the full-width or 44px-tall sizing meant for text
   inputs — otherwise e.g. the "Active (visible to clients)" toggle balloons on phones. */
.ve-field input[type="checkbox"],
.ve-field input[type="radio"] { width: auto; min-height: 0; flex: 0 0 auto; }

.ve-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }

/* Password show/hide toggle */
.ve-password-wrap { position: relative; display: block; }
.ve-password-wrap .ve-input,
.ve-password-wrap input { padding-right: 2.75rem; }
.ve-password-toggle {
    position: absolute;
    top: 50%;
    right: 4px;
    transform: translateY(-50%);
    min-width: 44px;
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 0;
    padding: 0;
    cursor: pointer;
    color: var(--ve-muted);
    border-radius: 6px;
}
/* #74: the eye/eye-off icon shape already signals state, so keep one constant color across
   hover/focus/active and drop the focus outline (the button is tabindex=-1, mouse-only). */
.ve-password-toggle:hover,
.ve-password-toggle:focus,
.ve-password-toggle:focus-visible,
.ve-password-toggle:active { color: var(--ve-muted); outline: none; }
.ve-password-toggle:disabled,
.ve-password-toggle:disabled:hover { color: #cfd4dc; cursor: not-allowed; }
.ve-password-toggle svg    { display: block; }

.ve-alert {
    padding: 0.75rem 1rem;
    border-radius: 8px;
    font-size: 0.9rem;
    margin-bottom: 1rem;
}

.ve-alert-error   { background: #fee2e2; color: #991b1b; border: 1px solid #fca5a5; }
.ve-alert-success { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
.ve-alert-info    { background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd; }
/* Issue #179 — the fourth alert tone. Used where an action is allowed but has a consequence
   the reader must see first (a signed agreement version whose wording is fixed). */
.ve-alert-warn    { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }

/* Issue #179 — the signed-agreement actions on the client detail page's Agreement row.
   ⚠️ They go on their OWN line beneath the badge, not beside it. The row lives in a
   `grid-template-columns: auto 1fr` definition list, so its value column is only as wide as the
   card allows: measured inline at 1400px, Open and Download each wrapped onto a line of their
   own and stepped outside the column. A block that flows and wraps as one keeps them together
   at every width. */
.ve-agr-actions {
    display: flex;
    align-items: center;
    gap: 0.4rem 0.55rem;
    flex-wrap: wrap;
    margin-top: 0.45rem;
}
.ve-agr-when,
.ve-agr-none { font-size: 0.8rem; color: var(--ve-muted); }
.ve-agr-when { flex-basis: 100%; }        /* the date and version caption its own buttons */

/* Issue #179 — the restricted rich-text editor (Components/HtmlEditor.razor).
   ⚠️ `.ve-hidden` is how the three views are switched. They all stay in the DOM: tearing the
   editor down and rebuilding it would re-run its one-and-only write, and the reader would lose
   their place and their undo history every time they looked at the source. */
.ve-hidden { display: none; }

.ve-htmled { border: 1px solid var(--ve-border); border-radius: 8px; overflow: hidden; }
.ve-htmled-bar {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    flex-wrap: wrap;
    padding: 0.4rem 0.5rem;
    background: var(--ve-surface-2, #f3f4f6);
    border-bottom: 1px solid var(--ve-border);
}
.ve-htmled-btn {
    min-width: 44px;
    min-height: 44px;
    padding: 0 0.6rem;
    font-size: 0.82rem;
    border: 1px solid var(--ve-border);
    border-radius: 6px;
    background: var(--ve-surface);
    color: var(--ve-text);
    cursor: pointer;
}
.ve-htmled-btn:hover { background: var(--ve-surface-2, #e5e7eb); }
.ve-htmled-sep { width: 1px; align-self: stretch; margin: 0.2rem 0.35rem; background: var(--ve-border); }

/* The editable area reuses the reading styles, so what is typed looks like what is signed. */
.ve-htmled-area {
    border: 0;
    border-radius: 0;
    min-height: 40vh;
    max-height: 46vh;
    outline: none;
}
.ve-htmled-area:focus { box-shadow: inset 0 0 0 2px var(--ve-accent, #0f766e); }

/* Issue #179 — the agreement wording, which is HTML, in the admin editor's preview and on the
   signed record. Bounded and scrolled: it is ~15,000 characters and would otherwise push every
   control in the form off the screen. */
.ve-agreement-read {
    max-height: 46vh;
    overflow-y: auto;
    padding: 0.9rem 1rem;
    border: 1px solid var(--ve-line, #d1d5db);
    border-radius: 6px;
    background: var(--ve-surface, #fff);
    font-size: 0.9rem;
    line-height: 1.55;
}
.ve-agreement-read h1,
.ve-agreement-read h2,
.ve-agreement-read h3 { font-size: 1rem; margin: 1rem 0 0.35rem; }
.ve-agreement-read p  { margin: 0 0 0.7rem; }

/* The same wording as source. Monospace so an unclosed tag is findable by eye. */
.ve-agreement-edit {
    font-family: Consolas, Menlo, "Courier New", monospace;
    font-size: 0.82rem;
    line-height: 1.5;
    white-space: pre;
    overflow-wrap: normal;
    overflow-x: auto;
}

.ve-agreement-text {
    background: #f8f9fc;
    border: 1px solid var(--ve-border);
    border-radius: 8px;
    padding: 1.25rem;
    max-height: 400px;
    overflow-y: auto;
    overscroll-behavior: contain; /* #141 */
    font-size: 0.88rem;
    line-height: 1.5;
    color: var(--ve-text);
}
.ve-agreement-text p                  { margin: 0.25rem 0; }
.ve-agreement-text p:empty            { display: none; }
.ve-agreement-text br + br            { display: none; }
.ve-agreement-text h1,
.ve-agreement-text h2,
.ve-agreement-text h3                 { margin: 0.75rem 0 0.25rem; font-size: 1rem; font-weight: 700; }
.ve-agreement-text ul,
.ve-agreement-text ol                 { margin: 0.25rem 0 0.25rem 1.25rem; padding: 0; }
.ve-agreement-text li                 { margin: 0.1rem 0; }

/* ─── Nav logo ─── */
.ve-nav-logo-img {
    display: block;
    width: 100%;
    max-width: 148px;
    height: auto;
    margin: 0 auto;
    border-radius: 6px;
    background: white;
    padding: 5px 8px;
}

/* ─── Shared calendar styles ─── */
.ve-view-toggle { display:flex;justify-content:center;gap:0.3rem; }
.ve-day-header { font-size:0.78rem;font-weight:700;text-transform:uppercase;letter-spacing:0.06em;color:var(--ve-muted);margin-bottom:0.3rem; }
.ve-agenda-booked   { border-left:3px solid #15803d;padding-left:0.5rem; }
.ve-agenda-full     { border-left:3px solid #b91c1c;padding-left:0.5rem; }
.ve-agenda-waitlist { border-left:3px solid #6b21a8;padding-left:0.5rem; }
.ve-agenda-past     { border-left:3px solid #9ca3af;padding-left:0.5rem;opacity:0.6;color:var(--ve-muted); }
.ve-agenda-cancelled { border-left:3px solid #b91c1c;padding-left:0.5rem;color:#b91c1c; }
.ve-public-row.past { opacity:0.6;color:var(--ve-muted); }
.ve-cal-nav { display:flex;align-items:center;justify-content:center;gap:0.75rem;margin-bottom:0.75rem;font-size:0.88rem;font-weight:600; }
.ve-cal-label-btn { background:none;border:0;padding:0.25rem 0.4rem;font:inherit;font-weight:600;color:var(--ve-navy);cursor:pointer;display:inline-flex;align-items:center;gap:0.3rem;border-radius:6px;min-height:44px; }
.ve-cal-label-btn:hover { background:var(--ve-bg); }

/* Two date-label formats inside the date picker button. Long shows by
   default; short kicks in at narrow widths via the mobile media query. */
.ve-cal-label-long  { display: inline; }
.ve-cal-label-short { display: none; }
.ve-dp-backdrop { position:fixed;inset:0;background:var(--ve-scrim);z-index:1200;display:flex;align-items:center;justify-content:center;padding:1rem; }
.ve-dp-card { background:var(--ve-surface,#fff);border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.25);padding:1rem;width:100%;max-width:340px;box-sizing:border-box; }
.ve-dp-head { display:flex;align-items:center;justify-content:space-between;margin-bottom:0.5rem; }
.ve-dp-title { font-weight:700;font-size:1rem;color:var(--ve-navy); }
.ve-dp-grid { display:grid;grid-template-columns:repeat(7,1fr);gap:2px; }
.ve-dp-dow { text-align:center;font-size:0.7rem;font-weight:700;color:var(--ve-muted);text-transform:uppercase;padding:0.25rem 0; }
.ve-dp-day { position:relative;aspect-ratio:1;border:0;background:none;border-radius:8px;font:inherit;font-size:0.9rem;color:var(--ve-text);cursor:pointer;display:flex;align-items:center;justify-content:center;min-height:40px; }
.ve-dp-day:hover { background:var(--ve-bg); }
.ve-dp-day.other { color:var(--ve-muted);opacity:0.55; }
/* Two marks, two meanings (issue #155):
     the BOX — the studio has classes available that day;
     the DOT — THIS viewer has a booking on it.
   They are deliberately different shapes, not two shades of the same one, because a
   day can be either, both or neither. The wash is --ve-rail (#008868) at 14% — pale
   enough that the day number keeps its full contrast against it — and must stay
   BEFORE .today/.sel below so the selected day's fill still wins. */
.ve-dp-day.has-classes { background:rgba(0,136,104,0.14);font-weight:600; }
.ve-dp-day.has-classes.other { background:rgba(0,136,104,0.07); }
.ve-dp-day.has-classes:hover { background:rgba(0,136,104,0.24); }
.ve-dp-day.has-booking::after { content:'';position:absolute;bottom:4px;left:50%;transform:translateX(-50%);width:5px;height:5px;border-radius:50%;background:var(--ve-rail); }
.ve-dp-day.sel.has-booking::after { background:#fff; }
.ve-dp-day.today { font-weight:700;box-shadow:inset 0 0 0 1px var(--ve-navy); }
.ve-dp-day.sel { background:var(--ve-navy);color:#fff;font-weight:700; }
.ve-dp-foot { display:flex;align-items:center;justify-content:space-between;margin-top:0.75rem; }
.ve-btn-sm { padding:0.25rem 0.7rem;font-size:1.1rem;line-height:1;min-height:44px;min-width:44px;display:inline-flex;align-items:center;justify-content:center; }

/* min-width:0 + max-width:100% keep this scroller from imposing the 770px grid's
   width on its parent — the 7-day grid scrolls INSIDE here, never widening the page. */
.ve-week-scroll { overflow-x:auto; min-width:0; max-width:100%; }
.ve-week-grid { display:grid;grid-template-columns:repeat(7,minmax(110px,1fr));min-width:770px;border:1px solid var(--ve-border);border-radius:8px;overflow:hidden; }
.ve-week-col { border-right:1px solid var(--ve-border); }
.ve-week-col:last-child { border-right:none; }
.ve-week-col-today { background:#f8f9ff; }
.ve-week-day-hdr { padding:0.4rem 0.5rem;background:var(--ve-bg);border-bottom:1px solid var(--ve-border);text-align:center;font-size:0.7rem;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:var(--ve-muted); }
.ve-week-day-hdr.today { color:var(--ve-navy); }
.ve-week-day-num { font-size:1rem;font-weight:700;color:var(--ve-muted);line-height:1.2; }
.ve-week-day-num.today { color:var(--ve-accent); }
.ve-week-events { padding:0.4rem;min-height:60px; }
.ve-week-event { background:var(--ve-navy);color:white;border-radius:5px;padding:0.3rem 0.45rem;margin-bottom:0.3rem;font-size:0.72rem;cursor:pointer; }
.ve-week-event.waitlist { background:#6b21a8; }
.ve-week-event.avail { background:#1e40af; }
.ve-week-event.avail.full { background:#b91c1c; }
.ve-week-event.booked { background:#15803d; }
.ve-week-event.past  { background:#9ca3af;color:#f3f4f6;opacity:0.75; }
.ve-week-event.cancelled { background:#9ca3af;color:#f3f4f6;text-decoration:line-through; }
.ve-week-event-time { font-size:0.65rem;opacity:0.8;margin-bottom:0.1rem; }
.ve-week-event-name { font-weight:600;line-height:1.2; }

.ve-month-grid { display:grid;grid-template-columns:repeat(7,minmax(50px,1fr));border:1px solid var(--ve-border);border-radius:8px;overflow:hidden; }
.ve-month-dow { padding:0.3rem;text-align:center;font-size:0.7rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:var(--ve-muted);background:var(--ve-bg);border-bottom:1px solid var(--ve-border); }
.ve-month-cell { min-height:72px;border-right:1px solid var(--ve-border);border-bottom:1px solid var(--ve-border);padding:0.25rem 0.3rem;background:var(--ve-surface); }
.ve-month-cell.other { background:#f9fafc; }
.ve-month-cell.today { background:#f0f4ff; }
.ve-month-date { font-size:0.78rem;font-weight:600;color:var(--ve-muted);margin-bottom:0.15rem; }
.ve-month-date.today { color:var(--ve-accent);font-weight:800; }
.ve-month-event { font-size:0.67rem;background:var(--ve-navy);color:white;border-radius:3px;padding:0.1rem 0.3rem;margin-bottom:0.1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer; }
.ve-month-event.waitlist { background:#6b21a8; }
.ve-month-event.avail { background:#1e40af; }
.ve-month-event.avail.full { background:#b91c1c; }
.ve-month-event.booked { background:#15803d; }
.ve-month-event.past  { background:#9ca3af;color:#f3f4f6;opacity:0.75; }
.ve-month-event.cancelled { background:#9ca3af;color:#f3f4f6;text-decoration:line-through; }

/* validation messages */
.validation-message { color: #dc2626; font-size: 0.8rem; margin-top: 0.2rem; }

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

    .blazor-error-boundary::after {
        content: "An error has occurred."
    }

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

/* ─── Mobile topbar overrides — placed at end of file so they win against
       the base .ve-topbar* rules earlier in source order. ─── */
@media (max-width: 768px) {
    .ve-topbar {
        /* Clear the status bar / Dynamic Island, and the L/R notch in landscape. */
        padding: calc(0.1rem + env(safe-area-inset-top, 0px))
                 calc(0.75rem + env(safe-area-inset-right, 0px))
                 0.1rem
                 calc(0.75rem + env(safe-area-inset-left, 0px));
        min-height: 0;
        gap: 0.5rem;
    }
    .ve-topbar-left  { flex: 0 0 auto; }
    /* Hamburger left, action buttons right — same row (no date to crowd them). */
    .ve-topbar-right {
        flex: 1 1 auto;
        justify-content: flex-end;
        gap: 0.5rem;
        font-size: 0.82rem;
    }
    .ve-topbar-btn   { padding: 0.45rem 0.85rem; font-size: 0.82rem; }
}

/* Calendar toolbar (Prev / Date / Next / Today  +  Day / Week / Month / Agenda):
   on phones, let each group wrap and shrink the per-button horizontal padding so
   nothing gets clipped at narrow widths. Vertical touch target stays at 44px. */
@media (max-width: 768px) {
    .ve-cal-nav,
    .ve-view-toggle {
        flex-wrap: wrap;
        justify-content: center;
        gap: 0.3rem;
    }
    .ve-cal-nav { gap: 0.35rem; }

    .ve-cal-nav .ve-btn,
    .ve-view-toggle .ve-btn {
        padding: 0.5rem 0.55rem;
        font-size: 0.82rem;
    }
    .ve-cal-nav .ve-btn-sm {
        padding: 0.25rem 0.45rem;
        min-width: 36px;
        font-size: 1rem;
    }
    .ve-cal-label-btn {
        padding: 0.25rem 0.3rem;
        font-size: 0.85rem;
    }

    /* Swap long date label for the compact form so Today fits on the same row */
    .ve-cal-label-long  { display: none; }
    .ve-cal-label-short { display: inline; }
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}

/* ─── PWA install banner (InstallPrompt) ─── */
/* Mobile-first: full-width bar pinned to the bottom (points toward Safari's
   Share button, which lives at the bottom of the screen on iPhone). */
.ve-install-banner {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1200;
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.75rem 1rem;
    padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
    background: var(--ve-surface);
    border-top: 1px solid var(--ve-border);
    box-shadow: 0 -4px 16px rgba(17, 27, 54, 0.12);
    animation: ve-install-rise 0.25s ease-out;
}

@keyframes ve-install-rise {
    from { transform: translateY(100%); }
    to   { transform: translateY(0); }
}

.ve-install-icon {
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    border-radius: 10px;
    overflow: hidden;
    box-shadow: 0 1px 4px rgba(17, 27, 54, 0.2);
}

.ve-install-icon img { width: 100%; height: 100%; display: block; }

.ve-install-body {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding-top: 0.1rem;
}

.ve-install-text {
    font-size: 0.9rem;
    line-height: 1.35;
    color: var(--ve-text);
}

.ve-install-text strong { color: var(--ve-navy); }

.ve-install-action {
    align-self: flex-start;
    min-height: 44px;
    padding: 0 1.25rem;
    border: none;
    border-radius: 8px;
    background: var(--ve-accent);
    color: #fff;
    font-size: 0.92rem;
    font-weight: 600;
    cursor: pointer;
}

.ve-install-action:hover { background: var(--ve-accent-hover); }

.ve-install-close {
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    margin: -0.35rem -0.5rem 0 0;
    border: none;
    background: transparent;
    color: var(--ve-muted);
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
    border-radius: 8px;
}

.ve-install-close:hover { color: var(--ve-text); background: var(--ve-bg); }

/* ─── Issue #175 — the install banners take the new palette ───
   Unconditionally, and not through the theme tokens. These float over whatever is on screen,
   and the screen they appear over most is the sign-in page, which is now dark for everybody
   including staff — a banner reading its colours from the signed-in role was white-on-black
   there, because nobody is signed in yet for a role to be read from. Fixed values, so the
   banner looks the same wherever it surfaces. */
.ve-install-banner {
    background: #11161d;
    border-top: 1px solid #303947;
    box-shadow: 0 -10px 30px rgba(0, 0, 0, .5);
}
.ve-install-text { color: #f7f9fc; }
.ve-install-text strong { color: #fff; }
.ve-install-action {
    border-radius: 10px;
    background: #005de8;
    font-size: .85rem;
    font-weight: 800;
    box-shadow: 0 8px 22px rgba(0, 93, 232, .28);
}
.ve-install-action:hover { background: #1769e8; }
.ve-install-close { color: #8793a2; }
.ve-install-close:hover { color: #f7f9fc; background: rgba(255, 255, 255, .08); }

.ios-pwa-prompt {
    background: rgba(17, 22, 29, .94);
    border: 1px solid #303947;
    color: #f7f9fc;
}
.ios-pwa-close { color: #8793a2; }
.ios-pwa-desc { color: #b7c0cc; }
.ios-pwa-steps-label { color: #8793a2; }

/* Opt-in install diagnostics chip (?installdebug=1) — a fixed, high-contrast readout of
   what detect() resolved, so on-device install state can be verified without a debugger. */
.ve-install-debug {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 3000;
    background: #111; color: #6f6;
    font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
    padding: 6px 8px; word-break: break-all; white-space: pre-wrap;
}

/* Desktop: a compact card bottom-right instead of a full-width bar. */
@media (min-width: 768px) {
    .ve-install-banner {
        left: auto;
        right: 1.5rem;
        bottom: 1.5rem;
        max-width: 380px;
        border: 1px solid var(--ve-border);
        border-radius: 12px;
    }
}

/* ─── iOS install prompt — react-ios-pwa-prompt style (iOS Safari only) ─── */
.ios-pwa-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1199;
    background: rgba(0, 0, 0, 0.4);
    animation: ios-pwa-fade 0.3s ease both;
}

.ios-pwa-prompt {
    position: fixed;
    z-index: 1200;
    left: 8px;
    right: 8px;
    bottom: 8px;
    max-width: 420px;
    margin: 0 auto;
    padding: 1rem 1.1rem 0.6rem;
    padding-bottom: calc(0.6rem + env(safe-area-inset-bottom, 0px));
    background: rgba(250, 250, 252, 0.92);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    backdrop-filter: saturate(180%) blur(20px);
    border-radius: 16px;
    box-shadow: 0 10px 40px rgba(0, 0, 0, 0.28);
    color: #1c1c1e;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
    animation: ios-pwa-rise 0.4s cubic-bezier(0.22, 1, 0.36, 1) both;
}

@keyframes ios-pwa-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes ios-pwa-rise { from { transform: translateY(115%); } to { transform: translateY(0); } }

.ios-pwa-close {
    position: absolute;
    top: 0.35rem;
    right: 0.35rem;
    width: 44px;
    height: 44px;
    border: none;
    background: transparent;
    color: #8e8e93;
    font-size: 1.7rem;
    line-height: 1;
    cursor: pointer;
}

.ios-pwa-head { text-align: center; padding: 0.25rem 1.75rem 0.5rem; }

.ios-pwa-app-icon {
    width: 56px;
    height: 56px;
    border-radius: 13px;
    box-shadow: 0 1px 6px rgba(0, 0, 0, 0.18);
    margin-bottom: 0.5rem;
}

.ios-pwa-title { font-size: 1.12rem; font-weight: 700; }
.ios-pwa-desc { font-size: 0.86rem; color: #6b6b70; margin-top: 0.2rem; line-height: 1.35; }

/* Small section label so the list reads as "do these elsewhere", not as a
   group of tappable controls. */
.ios-pwa-steps-label {
    margin: 0.6rem 0.15rem 0.1rem;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: #8e8e93;
}

.ios-pwa-steps {
    list-style: none;
    margin: 0;
    padding: 0;
    counter-reset: ios-pwa-step;
}

/* No row dividers and no accent badges: those made each step look like a
   tappable iOS settings row / button. Testers tapped them expecting action.
   The step text is the emphasis; the icon + number are quiet references. */
.ios-pwa-steps li {
    display: flex;
    align-items: flex-start;
    gap: 0.65rem;
    padding: 0.4rem 0.15rem;
    counter-increment: ios-pwa-step;
}

.ios-pwa-steps li::before {
    content: counter(ios-pwa-step);
    flex: 0 0 auto;
    width: 22px;
    height: 22px;
    margin-top: 0.15rem;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.06);
    color: #6b6b70;
    font-size: 0.78rem;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
}

.ios-pwa-step-icon {
    flex: 0 0 auto;
    width: 26px;
    height: 26px;
    margin-top: 0.15rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: #9a9aa0;          /* muted: a reference glyph, not a button to press */
}

.ios-pwa-step-text { flex: 1 1 auto; font-size: 1rem; line-height: 1.38; color: #1c1c1e; }
.ios-pwa-step-text strong { font-weight: 700; }
.ios-pwa-step-text em { font-style: normal; color: #007aff; font-weight: 600; }

/* The ONE genuinely tappable control on the redirect sheet — styled like a
   button precisely so it stands apart from the (quiet) instruction steps. */
.ios-pwa-actions { padding: 0.6rem 0.15rem 0.2rem; }
.ios-pwa-copy {
    width: 100%;
    min-height: 44px;
    border: none;
    border-radius: 10px;
    background: #007aff;
    color: #fff;
    font-size: 0.95rem;
    font-weight: 600;
    cursor: pointer;
}
.ios-pwa-copy:active { background: #006fe0; }

.ios-pwa-pointer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.3rem;
    padding: 0.45rem 0.4rem 0 0;
    color: rgba(0, 0, 0, 0.45);
    animation: ios-pwa-bounce 1.4s ease-in-out infinite;
}

.ios-pwa-pointer-text { font-size: 0.78rem; font-weight: 600; }

@keyframes ios-pwa-bounce {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(4px); }
}

/* Opt-in diagnostics line (?installdebug=1) — never shown to normal users. */
.ios-pwa-debug {
    margin-top: 0.5rem;
    padding: 0.35rem 0.4rem;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.62rem;
    line-height: 1.35;
    color: rgba(0, 0, 0, 0.45);
    word-break: break-all;
    user-select: text;
    -webkit-user-select: text;
}

/* Dark mode — the prompt follows the system appearance, like the repo. */
@media (prefers-color-scheme: dark) {
    .ios-pwa-prompt { background: rgba(28, 28, 30, 0.92); color: #f2f2f7; }
    .ios-pwa-desc { color: #a1a1a6; }
    .ios-pwa-steps-label { color: #98989d; }
    .ios-pwa-steps li::before { background: rgba(255, 255, 255, 0.14); color: #d1d1d6; }
    .ios-pwa-step-icon { color: #8e8e93; }
    .ios-pwa-step-text { color: #f2f2f7; }
    .ios-pwa-pointer { color: rgba(255, 255, 255, 0.5); }
    .ios-pwa-debug { color: rgba(255, 255, 255, 0.45); }
}
/* ─── Phone overrides that MUST fall after their base rules (source order) ───
   Media queries add no specificity, so an override in the early @media block is
   beaten by any same-selector base rule defined later in this file. The .ve-auth-*
   components are defined late, so their phone overrides live here at the very end,
   giving My Profile / login minimal gutters and tight spacing that actually fit.
   (See SOURCE-ORDER RULE in docs/responsive-layout-cheatsheet.md) */
@media (max-width: 768px) {
    .ve-auth-page     { padding: 0.5rem var(--ve-gutter) calc(0.5rem + env(safe-area-inset-bottom, 0px)); align-items: flex-start; }
    .ve-auth-card     { padding: 0.85rem 0.75rem; border-radius: 12px; }
    .ve-auth-card-body { padding: 0.85rem 0.75rem; }   /* #87 framed card: body keeps the tight mobile gutters */
    .ve-auth-brand    { margin-bottom: 0.9rem; gap: 0.4rem; }
    .ve-auth-brand h1 { font-size: 1.2rem; }
    .ve-field         { margin-bottom: 0.6rem; }
}

/* ============================================================================
   GLOBAL INPUT-ZOOM INVARIANT (#137, replaces the old in-block guard).
   Every focusable text-entry control on a phone/tablet must compute >=16px or
   iOS Safari auto-zooms the viewport on focus (right-edge truncation:
   #137/#27/#95/#115). Categorical: input minus the non-text types (which have
   no text caret and CANNOT trigger focus-zoom at any size), plus all
   select/textarea/contenteditable — covers every current and future field,
   whatever its class, type (or no type), or page.

   - (max-width:768px) covers narrow windows and portrait phones.
   - (pointer:coarse) covers landscape phones/tablets wider than 768px.
     PRIMARY pointer only — a touchscreen laptop driven by mouse/trackpad
     stays on desktop compact styling. Do NOT broaden it to also match a
     device's non-primary pointers (that would restyle touch desktops).
   - !important: a device invariant, not typography — page-local rules and
     inline styles must never win. Do not remove it.
   - This block must remain TOP-LEVEL. Do NOT nest it inside another @media
     (nesting ANDs the conditions and silently kills the pointer branch).
   Enforced by tests/smoke/zoom-invariant.mjs. */
@media (max-width: 768px), (pointer: coarse) {
    input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):not([type="range"]):not([type="color"]):not([type="file"]):not([type="submit"]):not([type="button"]):not([type="reset"]):not([type="image"]),
    select,
    textarea,
    [contenteditable]:not([contenteditable="false"]) {
        font-size: 16px !important;
    }
}

/* ============================================================================
   MEMBER SHELL — Client and Coach (issue #155)

   Two shells now share one MainLayout. Administrator keeps everything above:
   left drawer, white top bar, Porcelain page background. Client and Coach get
   this one — a fixed studio photograph, a transparent top bar carrying the logo
   and a hamburger, white cards floating over the photo, and a five-tab bar
   pinned to the bottom. There is no drawer at all for those two roles.

   The switch is `html.ve-member`, set by the pre-boot script in App.razor (from
   the stored token, so it beats the first paint) and re-asserted at runtime by
   MemberTabBar via veShell.member(). Every rule below is scoped to that class,
   so nothing here can reach the admin interface.

   Specificity, not source order, is what makes these win: `html.ve-member .x`
   is (0,2,1) against the (0,1,0) base rules — including the ones inside the
   trailing mobile @media blocks. Do not "simplify" the html prefix away.
   ============================================================================ */

:root {
    --ve-tabbar-h: 58px;
    /* The floating top bar's own height (34px logo + its padding). Pages other
       than the landing must reserve it, because the bar is out of flow. */
    --ve-member-topbar-h: 50px;
    /* The room a member page must leave at each end, in ONE place: .ve-content
       spends it as padding, and a page that manages its own scrolling (the
       Classes tab) subtracts the same two values from the viewport. Two copies
       of this arithmetic is how a list ends up under the tab bar. */
    --ve-member-pad-top: calc(var(--ve-member-topbar-h) + env(safe-area-inset-top, 0px));
    --ve-member-pad-bottom: calc(var(--ve-tabbar-h) + 0.75rem + env(safe-area-inset-bottom, 0px));
    /* The member interface is a phone layout. On a DESKTOP WINDOW it stays a
       centred column rather than stretching a hero photo across 1900px — the
       studio photograph fills the rest of the viewport behind it.
       ⚠️ It fills every handheld first. A flat 560px cap drew the line in the
       wrong place: a foldable is ~690 CSS px and was given the desktop
       treatment — 65px of bare photograph down each side of the cards — while
       being, in every sense that matters here, a phone (reported 2026-08-10).
       ⚠️ ONE number, and it is BOTH the fill limit and the cap. A cap smaller
       than the widest filling screen makes the column SHRINK as the screen
       grows (560px behind an 820px breakpoint jumped 804 → 560 at 821px).
       min() keeps it continuous and needs no media query, so the two halves
       cannot drift apart.
       The one variable drives all five consumers — the top bar's padding, the
       connection banner, .ve-member-page, .ve-tabbar-inner and the top bar's
       centre slot — so they can never disagree about where the column is.
       Its `100%` resolves per consumer, which is what keeps the header controls
       flush with the cards: centring within .ve-content's padded box and
       centring on the viewport give the same edge for a symmetric gutter. */
    --ve-member-col: min(100%, 820px);
}

/* ─── The fixed backdrop ───
   Rendered by MainLayout for every user and revealed only for members. It is a
   real element, not a pseudo-element on html/body: those sit underneath body's
   own background and the paint order gets fragile once .ve-shell, .ve-main and
   .ve-content each carry their own background. `fixed` is what makes it ignore
   the scroll (background-attachment:fixed is unreliable on iOS Safari). */
.ve-member-bg { display: none; }

html.ve-member .ve-member-bg {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 0;
    /* `100% auto`, NOT `cover` — and that is the whole point of this photograph
       (stakeholder's choice, 2026-08-05). Under `cover` a portrait phone is HEIGHT-driven:
       the full image height is on screen and only a narrow ~27% of its width, which is why
       the previous asset had to be a wide pre-cropped strip panned to 26%. The frame chosen
       here is the opposite — 54% of the room's width, so the racks, the benches and the
       windows read as one space — and that can only be had by letting the photograph span
       the width and stop where it stops. It occupies the top ~1.06 x viewport-width and the
       navy carries on beneath, behind the cards.
       The join is invisible because the bottom 18% of the ASSET dissolves into this exact
       navy (#0F1F1C), baked in at crop time — a CSS gradient would be sized to the viewport
       instead of to the image and would darken the picture on a short screen.
       The asset is member-bg-weight-room.jpg: the approved rectangle cut out of the 4032 x
       2688 original and compressed to 204 KB. member-bg.jpg is the previous photograph,
       kept unused in case the studio reverts. */
    background: var(--ve-navy) url('/img/member-bg-weight-room.jpg') center top / 100% auto no-repeat;
}

/* On a landscape screen the same photograph is WIDER than the window is tall, so anchoring
   it to the top fills the browser with ceiling tiles. Nothing about the size changes — only
   where the window looks — and 55% puts the racks and the windows on screen instead. The
   phone framing above is the one that was approved and is left exactly alone. */
@media (min-aspect-ratio: 1/1) {
    html.ve-member .ve-member-bg { background-position: center 55%; }
}

html.ve-member body,
html.ve-member .ve-shell,
html.ve-member .ve-main { background: transparent; }

/* Only the rebuilt landing page is transparent. Every other member page still
   carries its old markup, which assumes a Porcelain page behind it — dark text
   and unfilled panels straight over the photograph is unreadable (My Packages
   proved it). Those pages keep their surface until each is redesigned, at which
   point dropping ve-member-home from the rule below hands them the photograph.
   The class is route-driven (MemberTabBar + the pre-boot script), not :has(),
   so it does not depend on selector support on an older iOS. */
html.ve-member .ve-content { background: var(--ve-bg); }
html.ve-member-home .ve-content { background: transparent; }

html.ve-member .ve-shell { position: relative; z-index: 1; }

/* ─── No drawer ───
   Not merely translated off-screen: for a member the drawer does not exist, so
   the backdrop and the body.nav-open path have nothing to act on either. */
html.ve-member .ve-nav,
html.ve-member .ve-nav-backdrop { display: none; }
html.ve-member .ve-main { margin-left: 0; position: relative; }

html.ve-member .ve-status-banner {
    max-width: var(--ve-member-col);
    margin-top: var(--ve-member-pad-top);
}

/* The banner's margin has already cleared the absolute member top bar. Avoid
   spending the normal top-bar clearance a second time in the content scroller. */
html.ve-member .ve-status-banner + .ve-content { padding-top: 0.75rem; }
html.ve-member-home .ve-status-banner + .ve-content { padding-top: 0; }

/* ─── Top bar over the photograph ───
   Absolute (not fixed) so it belongs to .ve-main and inherits the centred
   column; taking it out of flow is what lets the hero start at y=0 underneath. */
/* ⚠️ z-index 150, NOT 30 — and it is load-bearing, not cosmetic.
   `position:absolute` + a z-index makes this bar its OWN STACKING CONTEXT, so every
   z-index inside it is resolved against this number, not against the page. The account
   menu and its dismiss backdrop are siblings OUTSIDE this bar: `.ve-account-menu` is 150
   and `.ve-account-backdrop` is 140, deliberately, so the menu outranks the backdrop.
   At 30 the whole bar — menu included — was painted UNDER the 140 backdrop, so every
   click on "My Profile" or "Sign-out" hit the backdrop and merely closed the menu. It
   looked exactly like "the hamburger menu doesn't allow clicking", and it affected the
   member shell ONLY: the Administrator's bar is `position:static` with no z-index, makes
   no stacking context, and its menu has always worked.
   Keep this at or above .ve-account-backdrop (140). Modals are 1000 and still win. */
/* The BAR spans the viewport; only its CONTENTS are held to the member column —
   the same shape as .ve-tabbar / .ve-tabbar-inner at the bottom of the screen.
   Capping the element itself (max-width + margin:auto, as this rule did until
   2026-08-10) left the gradient band floating with a gutter either side on any
   screen wider than --ve-member-col, while the page under it ran edge to edge.
   Measured on a Z-Fold: ~690px of CSS viewport against a 560px bar, so 65px of
   bare photograph down each side of the header. It cannot show on a phone
   narrower than the column, which is why it survived every phone-width probe.
   The column arithmetic lives in the horizontal padding instead, so the logo and
   the hamburger stay exactly where they were — nothing moves but the band. */
html.ve-member .ve-topbar {
    position: absolute;
    top: 0; left: 0; right: 0;
    z-index: 150;
    background: transparent;
    border-bottom: 0;
    min-height: 0;
    padding-top: calc(0.5rem + env(safe-area-inset-top, 0px));
    padding-bottom: 0.5rem;
    padding-left: max(calc(0.9rem + env(safe-area-inset-left, 0px)),
                      calc((100% - var(--ve-member-col)) / 2));
    padding-right: max(calc(0.9rem + env(safe-area-inset-right, 0px)),
                       calc((100% - var(--ve-member-col)) / 2));
}

html.ve-member .ve-topbar-left { flex: 0 0 auto; }

/* The bar is pinned, so once the page scrolls the logo and hamburger end up over
   a white card and a Porcelain logo on white is invisible. This fade gives them
   something to sit on at every scroll position. It disappears into the hero,
   which is already dark, so it costs nothing where the design wanted nothing.
   (The reference app lets its header scroll away with the hero instead; doing
   that here means moving the header inside the scroller, which would change the
   Administrator chrome too — deliberately out of scope for this round.) */
html.ve-member .ve-topbar::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background: linear-gradient(180deg,
                rgba(15, 31, 28, 0.80) 0%,
                rgba(15, 31, 28, 0.45) 58%,
                rgba(15, 31, 28, 0) 100%);
}

/* ─── The top bar's centre slot (issue #155) ───
   Styling for a line of text between the logo and the hamburger. NOTHING publishes to it
   today: page titles were dropped because the bottom tab bar already says which section
   the reader is in. Kept for the next thing that must be said across every member page.

   Absolute, not a flex item: centring between a ~100px logo and a 44px hamburger would
   put it visibly off-centre. `left:50%` + translate centres it on the BAR. It sits on the
   same optical line as the logo by matching its height, and pointer-events:none keeps it
   from ever swallowing a tap meant for either side. Never shown to an administrator. */
.ve-topbar-title { display: none; }

html.ve-member .ve-topbar-title {
    display: flex;
    align-items: center;
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    top: calc(0.5rem + env(safe-area-inset-top, 0px));
    height: 34px;                        /* the logo's height */
    /* Clears the logo and the hamburger on a phone. The second term holds it to the
       member column now that the bar itself spans the viewport (2026-08-10) — without
       it a wide screen would let the centre slot run wider than the page under it. */
    max-width: min(calc(100% - 11rem), calc(var(--ve-member-col) - 11rem));
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    pointer-events: none;
    font-size: 1.05rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    color: var(--ve-bg);
    text-shadow: 0 2px 6px rgba(0, 0, 0, 0.55);
}

.ve-member-logo-link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    text-decoration: none;
}

/* The approved logo is blue-and-red on transparent and disappears into a dark
   photograph. VisionEliteLogo-porcelain.png is the same artwork with the alpha
   channel untouched and the colour replaced by Porcelain — a recolour of the
   approved mark, not a new one. The shadow lifts it off the busier frames. */
.ve-member-logo {
    display: block;
    height: 34px;
    width: auto;
    filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.55));
}

.ve-member-hamburger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    background: none;
    border: none;
    border-radius: 8px;
    color: var(--ve-bg);
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
    filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.55));
}

.ve-member-hamburger:hover,
.ve-member-hamburger[aria-expanded="true"] { background: rgba(255, 255, 255, 0.16); }
.ve-member-hamburger:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: 2px; }

/* ─── The scrolling column ───
   The bottom pad clears the tab bar plus the home indicator, or the last card
   hides behind the bar. The top pad reserves the floating top bar, which is out
   of flow — every member page except the landing needs that room, or its first
   heading sits under the logo. */
html.ve-member .ve-content {
    padding: var(--ve-member-pad-top)
             calc(var(--ve-gutter) + env(safe-area-inset-right, 0px))
             var(--ve-member-pad-bottom)
             calc(var(--ve-gutter) + env(safe-area-inset-left, 0px));
    max-width: 100%;
}

/* ⚠️ A FORM INSIDE THE MEMBER SHELL IS NOT VERTICALLY CENTRED (owner, 2026-08-15 — "the
   Sign-out button can't be reached in the profile form").

   `.ve-auth-page` is `align-items: center`, which is right for a short sign-in card and
   wrong for anything taller than the window: a flex item taller than its container overflows
   it EQUALLY at the top and the bottom, and that overflow is outside the scroller's own
   scrollHeight — so scrolling to the very end still leaves the foot of the card below the
   bottom of the box. Measured on the profile form at 390x844: with everything scrolled to
   its end, Sign Out sat 36.7px BEHIND the tab bar and no amount of scrolling reached it.

   ⚠️ Two separate faults, and fixing either alone changes nothing — both were measured:
     1. `align-items: center` centred a card taller than the box, hanging its foot below.
     2. `.ve-content` is a GRID and stretched this item to the track height, so the card
        overflowed the item; a grid item's overflow does not extend the scroller, and neither
        does padding on a scroll container past overflowing content. `align-self: start` lets
        the item take its content's real height, and the clearance then rides inside the
        scrolled content where it counts.
   With both: Sign Out sits 49px clear of the tab bar, and the page has ONE scroller instead
   of two. The extra room below the bar's height is for the install banner, which is fixed to
   the bottom of the window and would otherwise sit on the last control. Sign-in and the
   screens beside it keep their centring: they are not in this shell, and they fit. */
html.ve-member .ve-auth-page {
    align-items: flex-start;
    align-self: start;
    padding-bottom: calc(var(--ve-member-pad-bottom) + 1rem);
}

/* The landing is the exception on both counts: its hero runs edge to edge and
   deliberately starts underneath the transparent bar. */
html.ve-member-home .ve-content {
    padding-top: 0;
    padding-left: 0;
    padding-right: 0;
}

.ve-member-page {
    width: 100%;
    max-width: var(--ve-member-col);
    margin: 0 auto;
}

/* ─── Hero ───
   Just a transparent spacer over the fixed photograph, plus a scrim. Scroll and
   the words leave; the photograph stays exactly where it is. 46dvh keeps the
   first card's top edge on screen on a short phone, which is what tells the
   user there is more below. */
.ve-member-hero {
    position: relative;
    min-height: 46dvh;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    padding: 5.5rem 1.15rem 1.4rem;
}

/* White text on a photograph is unreadable wherever the photograph happens to
   be pale, so the type sits on a scrim rather than on luck. Behind the text
   (z-index 0 vs 1) and non-interactive. */
.ve-member-hero::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background: linear-gradient(180deg,
                rgba(15, 31, 28, 0.62) 0%,
                rgba(15, 31, 28, 0.28) 34%,
                rgba(15, 31, 28, 0.78) 100%);
}

.ve-member-hero > * { position: relative; z-index: 1; }

.ve-member-stat {
    color: #fff;
    font-size: 1rem;
    font-weight: 700;
    letter-spacing: 0.01em;
    margin-bottom: 0.7rem;
    text-shadow: 0 1px 6px rgba(0, 0, 0, 0.6);
}

.ve-member-welcome {
    margin: 0;
    color: #fff;
    font-size: 1.85rem;
    line-height: 1.18;
    font-weight: 700;
    letter-spacing: -0.01em;
    text-shadow: 0 2px 10px rgba(0, 0, 0, 0.55);
}

.ve-member-welcome-sub {
    margin: 0.35rem 0 0;
    color: rgba(255, 255, 255, 0.92);
    font-size: 1.05rem;
    font-weight: 500;
    text-shadow: 0 1px 8px rgba(0, 0, 0, 0.6);
}

/* ─── The white cards ───
   Margins on all four sides on purpose: the photograph showing around each card
   is the design, not spare space. */
.ve-member-card {
    background: var(--ve-surface);
    border-radius: 14px;
    margin: 0 0.85rem 0.85rem;
    padding: 1.15rem 1.15rem;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
}

/* Text and button share one row, as in the reference. nowrap + min-width:0 on the
   text is what keeps them there: with wrap the count is wide enough at 390px to
   push BUY onto a line of its own, which reads as two unrelated things. */
.ve-member-card-split {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.85rem;
    flex-wrap: nowrap;
}

.ve-member-credits {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 1.3rem;
    font-weight: 700;
    color: var(--ve-text);
    line-height: 1.2;
}

.ve-member-credits small {
    display: block;
    font-size: 0.78rem;
    font-weight: 500;
    color: var(--ve-muted);
    margin-top: 0.15rem;
}

/* The credit count is a button (#165) — it opens the packages behind the number. It has
   to keep looking exactly like the text it replaced, so the button chrome is stripped
   back to nothing and the affordance is carried by an underline and a chevron. Both,
   deliberately: a phone has no hover state to discover, and an underline alone on a
   1.3rem bold number is easy to read as decoration. 44px min-height is the global touch
   target; the row is already 46px tall because of BUY, so this changes no layout. */
.ve-member-credits-btn {
    display: block;
    width: 100%;
    min-height: 44px;
    padding: 0;
    border: 0;
    background: none;
    text-align: left;
    font-family: inherit;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
}

.ve-member-credits-count {
    text-decoration: underline;
    text-decoration-color: var(--ve-rail);
    text-decoration-thickness: 2px;
    text-underline-offset: 4px;
}

.ve-member-credits-chev {
    display: inline-block;
    margin-left: 0.3rem;
    font-weight: 700;
    color: var(--ve-muted);
    text-decoration: none;
}

.ve-member-credits-btn:hover .ve-member-credits-count,
.ve-member-credits-btn:focus-visible .ve-member-credits-count { text-decoration-color: var(--ve-accent); }
.ve-member-credits-btn:active { opacity: 0.7; }
.ve-member-credits-btn:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: 3px; border-radius: 6px; }

/* The pop-up's one line of orientation — cards under a credits heading otherwise read
   as packages on offer rather than packages owned. */
.ve-pkg-modal-lede { margin: 0 0 0.9rem; font-size: 0.88rem; color: var(--ve-muted); }

/* A form's own body text, in tokens rather than hard-coded colours. The request form carried
   `color:#444` and `color:green` inline, which are light-palette values: they measured at
   1.74:1 and 3.29:1 on the dark form, so the explanation of what Send Request does was
   effectively invisible. Any form needing these three shapes uses them. */
.ve-modal-lede    { margin: 0 0 0.9rem; font-size: 0.86rem; color: var(--ve-muted); }
.ve-modal-note    { margin: 0 0 0.75rem; font-size: 0.9rem; line-height: 1.5; color: var(--ve-text); }
.ve-modal-success { margin: 0 0 1rem; font-size: 0.9rem; font-weight: 600; color: var(--ve-success); }

/* The "are you sure?" panel round a destructive confirmation — the late-cancel warning is the
   one that matters, because it names a charge. It was a white box with a pink border, drawn
   for a white card; on the dark form it was a slab of light with grey text inside it. */
.ve-danger-panel {
    background: rgba(185, 28, 28, .06);
    border: 1px solid rgba(185, 28, 28, .28);
    border-radius: 8px;
    padding: 0.85rem;
}
html.ve-client-redesign .ve-danger-panel {
    background: rgba(255, 118, 130, .10);
    border-color: rgba(255, 118, 130, .34);
}

.ve-member-cta {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 46px;
    min-width: 104px;
    padding: 0 1.5rem;
    border: 1px solid var(--ve-accent);
    border-radius: 8px;
    background: var(--ve-accent);
    color: #fff;
    font-family: inherit;   /* it is a <button> now (#142) — buttons don't inherit the body font */
    font-size: 0.95rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}

.ve-member-cta:hover { background: var(--ve-accent-hover); border-color: var(--ve-accent-hover); }
.ve-member-cta:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: 2px; }

.ve-member-card-title {
    margin: 0;
    font-size: 1.2rem;
    font-weight: 700;
    color: var(--ve-text);
}

.ve-member-card-sub {
    margin: 0.3rem 0 0;
    font-size: 0.85rem;
    color: var(--ve-muted);
}

/* ─── Class rows inside a card ───
   A row is a button-shaped target, not a table row: this shell has no tables. */
.ve-member-class {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    width: 100%;
    text-align: left;
    background: none;
    border: 0;
    border-top: 1px solid var(--ve-border);
    padding: 0.85rem 0;
    min-height: 56px;
    cursor: pointer;
    color: inherit;
    font: inherit;
}

.ve-member-class:first-of-type { border-top: 0; padding-top: 1rem; }
.ve-member-class:hover { background: var(--ve-bg); }
.ve-member-class:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: -2px; }

.ve-member-class-main { flex: 1 1 auto; min-width: 0; }

.ve-member-class-name {
    display: block;
    font-size: 1rem;
    font-weight: 700;
    color: var(--ve-text);
    line-height: 1.25;
}

.ve-member-class-meta {
    display: block;
    font-size: 0.82rem;
    color: var(--ve-muted);
    margin-top: 0.2rem;
}

.ve-member-class-right {
    flex: 0 0 auto;
    text-align: right;
    font-size: 0.78rem;
    color: var(--ve-muted);
}

.ve-member-empty {
    padding: 1rem 0 0.25rem;
    color: var(--ve-muted);
    font-size: 0.9rem;
}

/* ─── Bottom tab bar ───
   The whole navigation for Client and Coach. Fixed to the viewport, so it survives
   the content scroll, and padded for the home indicator.

   THE BAR MUST NEVER DISAPPEAR. That is why this rule is NOT scoped to
   html.ve-member: if the element is in the DOM it is visible, full stop.
   <MemberTabBar/> is the only gate — it renders nothing for an Administrator, and
   nothing while anonymous. Scoping visibility to the html class instead made an
   enhanced navigation able to hide a bar that was still sitting in the page
   (measured: logo click and BUY both did it). Do not add a class gate here. */
.ve-tabbar {
    display: flex;
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 40;
    background: var(--ve-navy);
    border-top: 1px solid rgba(255, 255, 255, 0.10);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    box-shadow: 0 -6px 22px rgba(0, 0, 0, 0.32);
}

.ve-tabbar-inner {
    display: flex;
    width: 100%;
    max-width: var(--ve-member-col);
    margin: 0 auto;
}

.ve-tab {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.2rem;
    /* 44px of touch target plus the label; --ve-tabbar-h must stay in step with
       this, because it is what reserves the space at the foot of .ve-content. */
    min-height: var(--ve-tabbar-h);
    padding: 0.4rem 0.15rem;
    background: none;
    border: 0;
    border-top: 3px solid transparent;
    /* 62% white on Night ink measures 7.2:1 — a resting label has to be readable,
       not merely "dimmed". The active state is pure white (17:1) plus the rail
       above, which carries 3.8:1 against the bar for SC 1.4.11. */
    color: rgba(255, 255, 255, 0.62);
    font: inherit;
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    text-decoration: none;
    cursor: pointer;
}

.ve-tab svg { display: block; }

.ve-tab.is-active {
    color: #fff;
    border-top-color: var(--ve-rail);
}

.ve-tab:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: -3px; }

/* Round one ships the Me tab only. The other four are drawn so the bar reads as
   the finished paradigm, but they do not navigate — their content is the subject
   of the specification still to come. Remove is-pending as each one lands. */
.ve-tab.is-pending { opacity: 0.55; cursor: default; }
.ve-tab.is-pending:hover { color: rgba(255, 255, 255, 0.62); }

/* ─── The account menu, over a photograph ───
   The base rules position it under a solid white top bar; here it hangs off a
   transparent one, so it needs its own edge and lift to stay legible. */
html.ve-member .ve-account-menu {
    top: calc(100% + 0.55rem);
    border: 1px solid var(--ve-border);
    box-shadow: 0 18px 44px rgba(0, 0, 0, 0.45);
}

@media (max-width: 767px) {
    .ve-member-welcome { font-size: 1.6rem; }
    .ve-member-card    { margin: 0 0.8rem 0.8rem; padding: 1rem; }
    .ve-member-hero    { padding: 5rem 1rem 1.2rem; }
    .ve-member-credits { font-size: 1.15rem; }
    .ve-member-cta     { min-width: 92px; padding: 0 1.15rem; }
}

/* ============================================================================
   MEMBER CLASSES PAGE — /classes and /classes/instructor/{id}  (issue #155)

   An ordinary member page, not a full-bleed one: it keeps the Porcelain surface
   and the reserved top padding that html.ve-member .ve-content gives it, because
   its text is dark. Only the landing carries ve-member-home.

   Deliberately NOT scoped to html.ve-member. These class names exist on two
   pages that only a member can reach, so scoping buys nothing and would break
   the page for the one second before the shell class is applied.
   ============================================================================ */

/* ─── A fixed header with the list scrolling UNDER it, not behind it ───
   The header was `position: sticky` inside .ve-content, the app's one scrolling
   window. Sticky keeps its space in flow but paints OVER whatever scrolls
   beneath, so the row against its lower edge was permanently cut in half and a
   row scrolled past it reappeared under the transparent member top bar.

   The page now owns its own scrolling instead: it takes the viewport less the
   shell's top and bottom room (the same two custom properties .ve-content
   spends as padding), the header is a static flex row, and only
   .ve-classes-list scrolls. Nothing scrolls under anything, so nothing can be
   covered. .ve-content has nothing left to scroll on this page. */
.ve-classes-page {
    display: flex;
    flex-direction: column;
    min-height: 0;
    height: calc(100dvh - var(--ve-member-pad-top) - var(--ve-member-pad-bottom));
}

.ve-classes-head {
    flex: 0 0 auto;
    background: var(--ve-bg);
    padding: 0.5rem 0 0.6rem;
    margin: 0;
    border-bottom: 1px solid var(--ve-border);
}

.ve-classes-list {
    flex: 1 1 auto;
    min-height: 0;              /* or the flex item refuses to shrink and the list overflows */
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
}

.ve-classes-month {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: 44px;
    padding: 0 0.4rem;
    background: none;
    border: 0;
    border-radius: 8px;
    font-size: 1.75rem;
    font-weight: 600;
    line-height: 1.1;
    color: var(--ve-text);
    cursor: pointer;
}

.ve-classes-month-caret { font-size: 0.9rem; color: var(--ve-muted); }
.ve-classes-month:hover { background: rgba(0, 0, 0, 0.04); }
.ve-classes-month:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: 2px; }

/* touch-action:pan-y lets the browser keep vertical scrolling while the
   horizontal gesture reaches the swipe handler. */
.ve-classes-week {
    display: flex;
    align-items: stretch;
    gap: 0.1rem;
    margin: 0.35rem 0 0.75rem;
    touch-action: pan-y;
}

.ve-classes-weeknav {
    flex: 0 0 auto;
    min-width: 28px;
    min-height: 44px;
    padding: 0;
    background: none;
    border: 0;
    font-size: 1.35rem;
    line-height: 1;
    color: var(--ve-muted);
    cursor: pointer;
}

.ve-classes-weeknav:hover { color: var(--ve-text); }
.ve-classes-weeknav:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: -2px; }

.ve-classes-day {
    flex: 1 1 0;
    min-width: 0;
    min-height: 56px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.2rem;
    padding: 0.25rem 0;
    background: none;
    border: 0;
    border-radius: 10px;
    cursor: pointer;
    color: var(--ve-muted);
}

.ve-classes-daynum {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    font-size: 0.95rem;
    font-weight: 600;
}

.ve-classes-dow { font-size: 0.8rem; font-weight: 600; }

.ve-classes-day.is-today .ve-classes-daynum { box-shadow: inset 0 0 0 1px var(--ve-control-border); }

/* The selected day is the emerald disc. White on --ve-accent (#005E4F) measures
   7.4:1, so the number is read, not merely located. */
.ve-classes-day.is-sel { color: var(--ve-text); }
.ve-classes-day.is-sel .ve-classes-daynum {
    background: var(--ve-accent);
    color: #fff;
    box-shadow: none;
}

.ve-classes-day:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: -2px; }

/* The same pair of marks the month grid paints (issue #155): the box for a day the
   studio has classes, the dot for a day this client has a booking. The dot's slot is
   reserved on EVERY day — one that only existed on some of them would shunt those
   days' numbers up as the week changed. */
.ve-classes-day::after {
    content: '';
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: transparent;
}

.ve-classes-day.has-classes { background: rgba(0, 136, 104, 0.14); }
.ve-classes-day.has-booking::after { background: var(--ve-rail); }

/* ─── Classes / Instructors segmented control ─── */
.ve-seg {
    display: flex;
    background: rgba(0, 0, 0, 0.05);
    border-radius: 10px;
    padding: 3px;
    gap: 3px;
}

.ve-seg-btn {
    flex: 1 1 0;
    min-height: 44px;
    padding: 0 0.5rem;
    background: none;
    border: 0;
    border-radius: 8px;
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--ve-muted);
    cursor: pointer;
}

.ve-seg-btn.is-on {
    background: var(--ve-surface);
    color: var(--ve-text);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
}

.ve-seg-btn:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: -2px; }

/* ─── A class row ─── */
.ve-cls-row {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.95rem 0.15rem;
    border-bottom: 1px solid var(--ve-border);
}

.ve-cls-main {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.18rem;
    min-height: 44px;
    padding: 0;
    background: none;
    border: 0;
    text-align: left;
    cursor: pointer;
}

.ve-cls-main:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: 3px; border-radius: 6px; }

.ve-cls-name {
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--ve-text);
    line-height: 1.25;
}

.ve-cls-meta { font-size: 0.9rem; color: var(--ve-muted); }

.ve-cls-action {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0.25rem;
    text-align: center;
}

.ve-cls-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    min-width: 104px;
    padding: 0 0.9rem;
    border-radius: 8px;
    font-size: 0.85rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-decoration: none;
    cursor: pointer;
}

.ve-cls-btn.is-book   { background: var(--ve-accent); border: 1px solid var(--ve-accent); color: #fff; }
.ve-cls-btn.is-full   { background: var(--ve-surface); border: 1px solid var(--ve-control-border); color: var(--ve-text); }
.ve-cls-btn.is-booked { background: var(--ve-surface); border: 1px solid var(--ve-accent); color: var(--ve-accent); }
.ve-cls-btn:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: 2px; }

.ve-cls-spots { font-size: 0.8rem; font-weight: 600; color: var(--ve-accent); }
.ve-cls-past  { font-size: 0.85rem; color: var(--ve-muted); align-self: center; }

/* ─── The client's class CARD (issue #175, the approved design 2026-08-15) ───
   Replaces the list row on the Classes tab for a client. The coach keeps .ve-cls-row: their
   list carries booked counts and a confirmation flag the design never drew, and squeezing
   those into this shape would lose them.

   Reading order is the design's: when it is, what it is, who and where, then the state of the
   class beside the one thing the reader can do about it. */
.ve-cls-card {
    background: var(--ve-surface);
    border: 1px solid var(--ve-border);
    border-radius: 14px;
    padding: 0.9rem 1rem;
    margin-bottom: 0.7rem;
}

/* ⚠️ The card is not one big button — the footer holds a real one, and a button inside a
   button is invalid markup that silently does not fire. This is the tappable part. */
.ve-cls-cardmain {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    width: 100%;
    min-height: 44px;
    padding: 0;
    background: none;
    border: 0;
    text-align: left;
    font: inherit;
    cursor: pointer;
}
.ve-cls-cardmain:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: 3px; border-radius: 8px; }

.ve-cls-time { font-size: 0.82rem; color: var(--ve-muted); }
.ve-cls-card .ve-cls-name { font-size: 1.1rem; font-weight: 700; }

.ve-cls-people {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    flex-wrap: wrap;
    font-size: 0.82rem;
    color: var(--ve-muted);
}
.ve-cls-personicon { flex: 0 0 auto; }
.ve-cls-dot { opacity: 0.6; }

.ve-cls-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.6rem;
    margin-top: 0.75rem;
}

/* What the class IS. The button beside it says what you can do about it, and the two must not
   say the same thing twice — the pill is the only place a spot count appears. */
.ve-cls-pill {
    display: inline-flex;
    align-items: center;
    min-height: 26px;
    padding: 0 0.6rem;
    border-radius: 7px;
    border: 1px solid transparent;
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    white-space: nowrap;
}
.ve-cls-pill.is-open   { background: rgba(45, 212, 191, 0.14); border-color: rgba(45, 212, 191, 0.38); color: #5eead4; }
.ve-cls-pill.is-closed { background: rgba(255, 118, 130, 0.12); border-color: rgba(255, 118, 130, 0.40); color: #ff8f99; }
.ve-cls-pill.is-mine   { background: rgba(59, 208, 151, 0.14); border-color: rgba(59, 208, 151, 0.42); color: #4fdcaa; }

/* A class the reader is already on is lifted out of the list — the design tints it rather than
   marking it, so it is findable by eye while scrolling past a dozen others. */
/* ⚠️ The tint is composited ON the card's own ground, not painted instead of it (issue
   #183). As a bare 10% blue it was a hole: with the photograph behind every page the room
   showed through this row and through the words on it, while every other row stayed solid.
   Two identical stops are how a flat colour is layered over another in one background. */
.ve-cls-card.is-mine {
    background: linear-gradient(rgba(0, 93, 232, 0.10), rgba(0, 93, 232, 0.10)), var(--ve-surface);
    border-color: rgba(90, 160, 255, 0.34);
}

.ve-cls-card .ve-cls-btn { min-width: 0; padding: 0 1.1rem; }
/* ⚠️ margin-left:auto, not the footer's space-between: since issue #177 a closed card has
   NO pill, and a lone child of a space-between row sits at the LEFT — CLOSED would step out
   of the column the BOOK buttons below it stand in. */
.ve-cls-card .ve-cls-past { align-self: center; margin-left: auto; font-size: 0.8rem; font-weight: 700; letter-spacing: 0.02em; }

/* An empty day is usually the ONLY thing on screen, so it is set at the landing
   page's scale and in body ink — the muted 0.95rem it started at read as a
   caption for content that wasn't there. */
.ve-classes-empty {
    padding: 2.5rem 1rem;
    text-align: center;
    font-size: 1.2rem;
    font-weight: 500;
    line-height: 1.45;
    color: var(--ve-text);
}

/* ─── Instructors ─── */
.ve-coach-row { align-items: center; }

.ve-coach-avatar {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 56px;
    height: 56px;
    border-radius: 10px;
    overflow: hidden;
    background: var(--ve-navy);
    color: var(--ve-bg);
}

.ve-coach-avatar.is-large { width: 96px; height: 96px; border-radius: 14px; }
.ve-coach-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ve-coach-initials { font-size: 1.15rem; font-weight: 700; letter-spacing: 0.02em; }
.ve-coach-avatar.is-large .ve-coach-initials { font-size: 2rem; }

.ve-coach-name {
    flex: 1 1 auto;
    min-width: 0;
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--ve-text);
}

/* ─── Instructor detail ─── */
.ve-coach-back { padding: 0.35rem 0 0.15rem; }

.ve-coach-hero {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.5rem 0 0.85rem;
}

.ve-coach-title { margin: 0; font-size: 1.5rem; font-weight: 700; color: var(--ve-text); line-height: 1.2; }

.ve-coach-bio { padding-bottom: 0.85rem; border-bottom: 1px solid var(--ve-border); }
/* pre-line, not pre-wrap: the administrator's paragraph breaks are kept, leading
   indentation from a paste is not. */
.ve-coach-bio-text  { margin: 0; font-size: 0.95rem; line-height: 1.55; color: var(--ve-text); white-space: pre-line; }
.ve-coach-bio-empty { margin: 0; font-size: 0.92rem; color: var(--ve-muted); font-style: italic; }

.ve-coach-section { margin: 1.1rem 0 0.25rem; font-size: 1.1rem; font-weight: 700; color: var(--ve-text); }

/* ============================================================================
   MEMBER SCHEDULE PAGE — /my-schedule  (issue #155)

   The month calendar over the selected day's own bookings. Unlike the Classes
   tab this page has NO fixed header: it scrolls with .ve-content like every
   other member page, which is why the sticky-overlap trap documented above
   cannot apply here. Keep it that way — a header that has to stay put is the
   only reason to take over the scrolling.
   ============================================================================ */
/* The cards sit inside .ve-content's gutter already, so they do not need
   .ve-member-card's edge-to-edge margins — only the space between them. */
.ve-sched-cal,
.ve-sched-day { margin: 0 0 0.85rem; }


/* ─── The compact month ───
   This calendar is a wayfinder above the day's bookings, not the subject of the page:
   at the picker's own proportions it ate the screen and pushed the bookings below the
   fold. Everything here is scoped to .ve-sched-cal, so the pop-up picker — a modal with
   a whole screen to itself — keeps its roomier size.

   ⚠️ The day cells are 38px tall, BELOW the project's 44px minimum touch target. That is
   a deliberate, user-directed trade (2026-08-02: "reduce it ... actually less than" the
   reference screen), not an oversight. They stay ~50px WIDE, and 38×50 is far above the
   24×24 floor of WCAG 2.2 SC 2.5.8. Restore the standard by deleting the height rules
   below — the layout absorbs it. */
.ve-sched-cal { padding: 0.55rem 0.6rem 0.7rem; }

/* Month name to the left with the arrows paired at the right, as the reference has it.
   The buttons keep their DOM order (‹ then ›); only the title is pulled in front. */
.ve-sched-cal .ve-dp-head { justify-content: flex-start; gap: 0.3rem; margin-bottom: 0.1rem; }
.ve-sched-cal .ve-dp-title {
    order: 0;
    flex: 1 1 auto;
    text-align: left;
    font-size: 1rem;
    letter-spacing: 0.03em;
    text-transform: uppercase;
}
.ve-sched-cal .ve-dp-head .ve-btn:first-of-type { order: 1; }
.ve-sched-cal .ve-dp-head .ve-btn:last-of-type  { order: 2; }
.ve-sched-cal .ve-dp-head .ve-btn {
    width: 40px; height: 40px; min-width: 40px; min-height: 40px;
    padding: 0;
    border-radius: 50%;
}

.ve-sched-cal .ve-dp-dow { font-size: 0.65rem; padding: 0.1rem 0; }

/* aspect-ratio: auto is the other half of the saving — square cells are ~50px on a
   phone, and six of those is a third of the viewport. */
.ve-sched-cal .ve-dp-day {
    aspect-ratio: auto;
    height: 36px;
    min-height: 36px;
    font-size: 0.95rem;
}
.ve-sched-cal .ve-dp-day.has-booking::after { bottom: 3px; }

/* The two member surfaces agree on what "selected" looks like: this grid's selected
   day is the same emerald as the Classes tab's day disc. Scoped to this page, so the
   administrator's pickers keep the navy they have always had. White on --ve-accent
   measures 7.4:1. */
.ve-sched-cal .ve-dp-day.sel { background: var(--ve-accent); border-radius: 10px; }

.ve-sched-dayhead {
    margin: 0 0 0.25rem;
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--ve-text);
}

/* An empty day is the whole page, so it is set at the landing page's scale
   rather than as a caption for content that isn't there. */
/* Sized so that in a SIX-row month — the worst case — the whole message and its button
   still clear the tab bar on an iPhone 13 without scrolling. Probe check 19b measures it
   against the bar's own top edge, so raising either padding has to be re-measured. */
.ve-sched-empty { padding: 1.2rem 0.5rem 1rem; text-align: center; }

.ve-sched-empty-title {
    margin: 0;
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.2;
    color: var(--ve-text);
}

.ve-sched-empty-sub {
    margin: 0.4rem 0 0;
    font-size: 1.05rem;
    font-weight: 500;
    color: var(--ve-muted);
}

.ve-sched-empty-cta {
    margin-top: 0.95rem;
    min-height: 48px;
    padding: 0 1.6rem;
    font-weight: 700;
    letter-spacing: 0.03em;
}

/* ============================================================================
   MEMBER ACTIVITY PAGE — /activity  (issue #155)

   The lifetime count, a bar per week of one month, and that month's sessions.
   Like /my-schedule this page has no fixed header and scrolls with .ve-content,
   so the sticky-overlap trap documented above cannot apply.
   ============================================================================ */
/* .ve-content already supplies the gutter, so these want only the space between
   them — not .ve-member-card's edge-to-edge margins. */
.ve-act-hero,
.ve-act-card,
.ve-act-list { margin: 0 0 0.85rem; }

/* The reference screen's coloured band. Here it is a card in Deep Forest, because
   the photograph belongs to the landing alone (member-shell rule 3): this page's
   markup assumes the Porcelain surface, and white type over the photo would be
   readable only where the photo happens to be dark. #fff on --ve-navy is 16.4:1. */
.ve-act-hero {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    background: var(--ve-navy);
    color: #fff;
    border-radius: 14px;
    padding: 1.15rem 1.25rem;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
}

.ve-act-count {
    font-size: 3.1rem;
    font-weight: 800;
    line-height: 0.95;
    letter-spacing: -0.02em;
}

.ve-act-heroLabel { font-size: 1.4rem; font-weight: 700; line-height: 1.12; }

/* ─── Month selector ─── */
.ve-act-head {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    margin-bottom: 0.9rem;
}

.ve-act-month {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    min-height: 44px;
    padding: 0 0.35rem;
    background: none;
    border: 0;
    border-radius: 8px;
    font-size: 1.15rem;
    font-weight: 700;
    letter-spacing: 0.03em;
    color: var(--ve-text);
    cursor: pointer;
    /* One line, always. "JUN 2026" wrapping to two is what pushed the caret under the
       month and made the row two rows tall on a 390px screen. */
    white-space: nowrap;
    flex: 0 0 auto;
}

.ve-act-caret { font-size: 0.8rem; color: var(--ve-muted); }
.ve-act-month:hover { background: rgba(0, 0, 0, 0.04); }
.ve-act-month:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: 2px; }

.ve-act-nav { display: flex; gap: 0.15rem; }

.ve-act-navbtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    min-width: 44px;
    padding: 0;
    background: none;
    border: 1px solid var(--ve-control-border);
    border-radius: 50%;
    font-size: 1.25rem;
    line-height: 1;
    color: var(--ve-accent);
    cursor: pointer;
}

.ve-act-navbtn:hover { background: rgba(0, 0, 0, 0.04); }
.ve-act-navbtn:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: 2px; }

/* Shrinks before the month does — the month is the page's subject, the filter is not. */
.ve-act-filter {
    margin-left: auto;
    min-width: 0;
    flex: 0 1 auto;
    max-width: 38%;
    min-height: 44px;
    padding: 0 0.4rem;
    background: var(--ve-surface);
    border: 1px solid var(--ve-control-border);
    border-radius: 8px;
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--ve-accent);
}

/* ─── The chart ───
   Plain CSS: five or six bars do not justify a charting library, and one would have
   to be inlined anyway (the artifact CSP and the offline directive both forbid a CDN).
   gap:0 between the columns is load-bearing — each column draws its own stretch of the
   baseline, and a gap would break the axis into dashes. */
.ve-act-chart {
    display: grid;
    grid-template-columns: auto auto 1fr;
    gap: 0.35rem;
    height: 185px;
}

.ve-act-ylabel {
    writing-mode: vertical-rl;
    transform: rotate(180deg);
    align-self: center;
    font-size: 0.7rem;
    color: var(--ve-muted);
}

/* padding-bottom matches the x-label row below, so "0" lands on the axis line. */
.ve-act-yaxis {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    padding-bottom: 1.25rem;
    font-size: 0.75rem;
    font-weight: 600;
    color: var(--ve-muted);
}

.ve-act-bars {
    display: flex;
    align-items: stretch;
    gap: 0;
    min-width: 0;
    border-left: 1px solid var(--ve-border);
}

.ve-act-col {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.ve-act-track {
    flex: 1 1 auto;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding: 0 4px;
    border-bottom: 1px solid var(--ve-border);
}

.ve-act-bar {
    width: 100%;
    max-width: 34px;
    min-height: 2px;
    background: var(--ve-rail);
    border-radius: 3px 3px 0 0;
    display: flex;
    justify-content: center;
}

/* Pulled above its own bar, so a short bar's number is never trapped inside it. */
.ve-act-barval {
    margin-top: -1.15rem;
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--ve-text);
}

.ve-act-xlabel {
    padding-top: 0.35rem;
    font-size: 0.7rem;
    line-height: 1.2;
    color: var(--ve-muted);
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ─── The month menu ───
   The reference moves months with arrows only. This is the addition: every month the
   client has attendance in, with its count. Chrome is the date picker's, so the two
   "pick a period" surfaces in the member shell look like siblings. */
.ve-act-menu { max-width: 320px; }
.ve-act-menu .ve-dp-foot { justify-content: flex-end; }

.ve-act-menuhead { margin: 0 0 0.6rem; font-size: 1.05rem; font-weight: 700; color: var(--ve-text); }
.ve-act-menuempty { margin: 0; font-size: 0.95rem; color: var(--ve-muted); }

.ve-act-menulist {
    max-height: 52dvh;
    overflow-y: auto;
    overscroll-behavior: contain;
}

.ve-act-menuitem {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    width: 100%;
    min-height: 44px;
    padding: 0 0.6rem;
    background: none;
    border: 0;
    border-radius: 8px;
    font-size: 1rem;
    color: var(--ve-text);
    text-align: left;
    cursor: pointer;
}

.ve-act-menuitem:hover { background: rgba(0, 0, 0, 0.05); }
.ve-act-menuitem:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: -2px; }
.ve-act-menuitem.is-sel { background: var(--ve-accent); color: #fff; }
.ve-act-menumonth { font-weight: 600; }

/* "a small number of the sessions attended in each month" */
.ve-act-menucount {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 26px;
    height: 22px;
    padding: 0 0.4rem;
    border-radius: 11px;
    background: rgba(0, 136, 104, 0.16);
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--ve-accent);
}

.ve-act-menuitem.is-sel .ve-act-menucount { background: rgba(255, 255, 255, 0.24); color: #fff; }

/* ─── The month's sessions ─── */
.ve-act-listhead { margin: 0 0 0.15rem; font-size: 1.05rem; font-weight: 700; color: var(--ve-text); }

.ve-act-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 28px;
    padding: 0 0.7rem;
    border: 1px solid var(--ve-accent);
    border-radius: 8px;
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    color: var(--ve-accent);
}

/* ============================================================================
   MEMBER PRIVATE PAGE — /private  (issue #155, replaced ClassPoints)
   Rows are the shared .ve-cls-* shape, so a private session reads like a class.
   ============================================================================ */
.ve-priv-intro,
.ve-priv-list { margin: 0 0 0.85rem; }

.ve-priv-lede { margin: 0; font-size: 1.15rem; font-weight: 700; color: var(--ve-text); }
.ve-priv-sub  { margin: 0.35rem 0 0; font-size: 0.95rem; line-height: 1.5; color: var(--ve-muted); }

/* ============================================================================
   PERSONAL TRAINING REQUEST QUEUE  (issue #160)
   One shape for three hosts: the coach's Private tab, the client's own list on the
   same tab, and the administrator's /admin/pt-requests. The rows deliberately do
   NOT reuse .ve-cls-row — a request is not a class, it has no time and no capacity,
   and borrowing the class row would make it look like one.
   ============================================================================ */
.ve-priv-queue { margin: 0 0 0.85rem; }

.ve-ptq-head   { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.6rem; }
.ve-ptq-title  { font-size: 1rem; font-weight: 700; color: var(--ve-text); }
.ve-ptq-count  {
    min-width: 1.5rem;
    padding: 0.05rem 0.45rem;
    border-radius: 999px;
    background: var(--ve-surface-2, #eef2f0);
    font-size: 0.8rem;
    font-weight: 700;
    color: var(--ve-muted);
    text-align: center;
}
.ve-ptq-filter { margin-left: auto; display: flex; gap: 0.3rem; }
.ve-ptq-chip   {
    min-height: 32px;
    padding: 0.2rem 0.7rem;
    border: 1px solid var(--ve-line, #d8ded9);
    border-radius: 999px;
    background: transparent;
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--ve-muted);
    cursor: pointer;
}
.ve-ptq-chip.is-on { background: var(--ve-primary); border-color: var(--ve-primary); color: #fff; }

.ve-ptq-row {
    padding: 0.7rem 0;
    border-top: 1px solid var(--ve-line, #e6ebe8);
}
.ve-ptq-row:first-of-type { border-top: 0; }
/* Amber attention, the same treatment "Needs confirmation" already uses, so a coach
   reads one visual language for "this has been left". */
.ve-ptq-row.is-stale { border-left: 3px solid #d9822b; padding-left: 0.6rem; }

.ve-ptq-line   { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.ve-ptq-name   { font-size: 0.98rem; font-weight: 700; color: var(--ve-text); }
.ve-ptq-meta   { margin-top: 0.15rem; font-size: 0.85rem; color: var(--ve-muted); }
.ve-ptq-msg    { margin-top: 0.3rem; font-size: 0.88rem; line-height: 1.45; color: var(--ve-text); font-style: italic; }

.ve-ptq-actions {
    margin-top: 0.55rem;
    display: flex;
    gap: 0.4rem;
    flex-wrap: wrap;
}
.ve-ptq-actions .ve-btn { min-height: 44px; }

.ve-ptq-decline { margin-top: 0.55rem; }
.ve-ptq-empty   { padding: 0.9rem 0; font-size: 0.9rem; color: var(--ve-muted); }
.ve-ptq-error   { margin-top: 0.6rem; font-size: 0.88rem; color: var(--ve-accent); }

/* ============================================================================
   2026 FIGMA CLIENT REDESIGN

   Client-only. Coaches keep the established member-shell palette and administrators
   keep the desktop shell. The pre-boot role sniff applies this class before paint.
   ============================================================================ */
/* ─── The redesign's palette, taken from the approved prototype ───
   ⚠️ These are the prototype's OWN values, not an interpretation of them
   (docs/Codex_figma-redesign-prototype_20260814-1530.htm; its token name is beside each).
   They were approximations until 2026-08-15, which is why the greys read slightly wrong
   against the design and the "Booked" green was still the old mint. The design is the
   authority — see the directive at the top of CLAUDE.md — so where a value here and the
   prototype disagree, the prototype is right and this is the bug. */
html.ve-client-redesign {
    --ve-bg: #080b10;               /* --night       */
    --ve-surface: #11161d;          /* --surface     */
    --ve-surface-2: #202733;        /* --raised      */
    --ve-text: #f7f9fc;             /* --ink         */
    --ve-muted: #b7c0cc;            /* --muted       */
    --ve-quiet: #8793a2;            /* --quiet       */
    --ve-border: #303947;           /* --line        */
    --ve-line: #303947;             /* --line        */
    --ve-control-border: #3a4554;   /* no prototype twin — kept */
    --ve-navy: #080b10;
    --ve-primary: #005de8;          /* --blue-button */
    --ve-accent: #2f7df4;
    --ve-accent-hover: #1769e8;
    --ve-rail: #5aa0ff;             /* --blue-text   */
    --ve-green: #3bd097;            /* --green       */
    --ve-green-soft: rgba(59, 208, 151, .14);
    --ve-red: #ff7682;              /* --red         */
    --ve-success: #3bd097;          /* --green — readable ON DARK, unlike the base #15803d */
    --ve-danger: #ff7682;           /* --red   — likewise */
    /* The card: one colour for every card in the design, at the prototype's own alpha. */
    --ve-card: rgba(24, 30, 39, .96);
    --ve-card-shadow: 0 12px 30px rgba(0, 0, 0, .18);
}

/* The redesign's face, kept to the redesign. The coach and administrator shells were not
   part of it and are left on the type they already had — the owner is reviewing this one
   screen at a time, and a font change is not something to apply to pages nobody has looked
   at yet. Sign-in and the two screens beside it are included because they belong to the
   redesign, and they cannot hang off the class: it is set from the signed-in role, which
   does not exist yet on those screens. Form controls are named because a <button> or an
   <input> does not inherit the document font. */
html.ve-client-redesign,
html.ve-client-redesign body,
.ve-signin-page {
    font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
}
.ve-signin-page input,
.ve-signin-page select,
.ve-signin-page textarea,
.ve-signin-page button { font-family: inherit; }

/* ⚠️ .ve-shell and .ve-main MUST stay transparent. `html.ve-member .ve-shell` is
   `position: relative; z-index: 1`, which puts it ABOVE the fixed .ve-member-bg at z-index 0
   — so the moment either carries an opaque colour it covers the photograph completely, and
   the landing goes flat black with nothing to say why. That is exactly what happened when
   this rule first listed all four. The dark ground belongs on BODY, which paints on the
   canvas behind the photograph, and on .ve-content, which is lifted off again on the
   landing so the picture shows through behind the cards. */
html.ve-client-redesign body { background-color: var(--ve-bg); color: var(--ve-text); }
html.ve-client-redesign .ve-shell,
html.ve-client-redesign .ve-main { background: transparent; color: var(--ve-text); }

/* ─── The photograph is behind EVERY client screen (issue #183) ───
   It used to be the landing's alone: the backdrop element was removed from every other page
   and .ve-content painted the night colour over the top of it. The studio asked on
   2026-08-23 for the picture to carry across the whole application, which beats the approved
   prototype's own drawing of it (the prototype paints it on Home only).

   So the scrolling column is transparent on every client page now, and the ONE thing that
   changes off the landing is the veil below.
   ⚠️ .ve-shell and .ve-main must stay transparent, as the note above says — either of them
   carrying a colour covers the picture completely. Only BODY keeps the night ground, which
   is what the picture sits on. */
html.ve-client-redesign .ve-content { background: transparent; }

/* ⚠️ A CONTENT page is veiled; the landing is not, and must never be.
   The picture is a photograph of a room with windows in it, and the windows are BRIGHT: the
   brightest 16px patch of it measures a relative luminance of 0.896 against the near-black
   page it replaces at 0.0035. The landing can leave that alone because its hero puts no small
   text over the bright band and its cards are all but opaque; every other screen starts its
   content 50px from the top, right across the windows, and its month names, day numbers and
   captions would sit on them.

   0.78 is measured, not chosen. Photographing the composited background alone on the running
   application, with the page's own content hidden, the brightest patch behind the top of a
   content page comes out at:

       veil    brightest L    --ve-muted text on it
       none        0.896            1.66:1     unreadable
       0.55        0.174            2.55:1     unreadable
       0.65        0.107            3.65:1     under AA
       0.72        0.072            4.68:1     AA, no margin
       0.78        0.048            5.85:1     AA with room  <-- this
       0.85        0.026            7.50:1     the picture is nearly gone

   --ve-muted (#b7c0cc) is the weakest ink the redesign actually paints, so 4.5:1 against it
   is the floor. Do not lower this without re-measuring against the ink, and do not raise it
   to be safe: at 0.85 there is no photograph left to carry. */
html.ve-client-redesign:not(.ve-member-home) .ve-member-bg::after {
    content: '';
    position: absolute;
    inset: 0;
    pointer-events: none;
    background: rgba(8, 11, 16, .78);
}

/* ⚠️ The photograph's bottom 18% was baked to dissolve into the OLD green-navy, measured at
   rgb(17, 32, 29). Against this palette's near-black that fade ends in a visible green band
   across the screen. Re-cutting the asset is the studio's call, so the join is closed here
   instead, with a gradient that CAN be trusted to line up: the picture is 1400 x 1483, so at
   `100% auto` its height is always 105.93% of the viewport WIDTH, and a gradient measured in
   vw tracks it exactly. That is the difference from the gradient the earlier note warned
   against, which was sized to the viewport HEIGHT and so slid about with the window. */
html.ve-client-redesign .ve-member-bg {
    background:
        linear-gradient(180deg,
            rgba(8, 11, 16, 0) 0,
            rgba(8, 11, 16, 0) 46vw,
            rgba(8, 11, 16, .70) 84vw,
            #080b10 102vw) center top / 100% 106vw no-repeat,
        url('/img/member-bg-weight-room.jpg') center top / 100% auto no-repeat,
        #080b10;
}

/* The approved design lets the room read THROUGH the cards on the landing rather than boxing
   it off above them — the photograph stays part of the screen instead of becoming a banner at
   the top of it. Only the landing does this; every other screen keeps a solid card, because
   it has no photograph to show through to.

   ⚠️ There was a `backdrop-filter: blur(7px)` here and it is GONE, deliberately. A backdrop
   filter promotes the element to its own compositing layer, and text in a composited layer is
   rendered with grey-scale anti-aliasing instead of sub-pixel — every word on these cards came
   out visibly softer than the same word elsewhere in the app, which is what the owner was
   looking at on 2026-08-14 when they said the type was not sharp. The alpha is raised from .84
   to .90 to give the letters the steady ground the blur was providing. Do not put it back. */
html.ve-client-redesign.ve-member-home .ve-member-card {
    background: var(--ve-card);
}
html.ve-client-redesign .ve-topbar::before {
    background: linear-gradient(180deg, rgba(8, 11, 16, .88), rgba(8, 11, 16, .36), transparent);
}

html.ve-client-redesign .ve-topbar-left { flex: 1 1 auto; }
html.ve-client-redesign .ve-topbar-right { flex: 0 1 auto; }
html.ve-client-redesign .ve-hamburger,
html.ve-client-redesign .ve-topbar-btn-signup,
html.ve-client-redesign .ve-topbar-btn-ghost { display: none; }
html.ve-client-redesign .ve-topbar-btn-door {
    background: var(--ve-primary);
    border-color: var(--ve-primary);
    max-width: min(78vw, 620px);
    white-space: normal;
    text-align: left;
}

/* ⚠️ The hero is `justify-content: flex-end`, so this height — NOT the top padding — is what
   sets the gap between the top of the window and the greeting. The words sit on its floor;
   raising them means a shorter block. 43dvh left a bank of empty photograph above the
   greeting that the design does not have (owner, 2026-08-14). The top padding only has to
   clear the 50px bar, and 4rem does.

   ⚠️ Issue #181 took THREE lines out of the hero, and because the height is min-height and
   not the contents, taking them out moved the greeting DOWN and put their space back as bare
   photograph — the very thing 43dvh was reduced for. Measured on the running application at
   390px: the tally 15px on a 4px margin, "Your training day is ready." 20px on an 8px margin,
   and the age line 15px on no margin — 62px in all, and the first line of hero text fell from
   101px below the top of the window to 163px. The 62px is subtracted here, so the greeting
   sits where the tally sat and the first card comes up with it. It is a pixel subtraction
   rather than a smaller dvh because the three lines were a fixed height of type on every
   screen, and a dvh figure would only be right on the one screen it was measured on.

   The slot itself is not gone: when nothing has been downloaded on this device the hero still
   says so there, which is a different statement and one §7.1 requires. That message is taller
   than the age label it replaces, and it appears only on a landing with no data at all, where
   there is nothing below it to push. */
html.ve-client-redesign .ve-member-hero {
    min-height: calc(31dvh - 62px);
    padding: 4rem 1rem 1rem;
}
html.ve-client-redesign .ve-member-hero::before {
    background: linear-gradient(180deg, rgba(8, 11, 16, .35), rgba(8, 11, 16, .18) 34%, rgba(8, 11, 16, .94));
}

/* The design draws ONE card — credit card, panel, class card, package card, profile card and
   activity card are the same object at the same colour and the same shadow. */
html.ve-client-redesign .ve-member-card,
html.ve-client-redesign .ve-card,
html.ve-client-redesign .ve-auth-card,
html.ve-client-redesign .ve-modal,
html.ve-client-redesign .ve-dp-card {
    background: var(--ve-card);
    color: var(--ve-text);
    border-color: var(--ve-border);
    box-shadow: var(--ve-card-shadow);
}

html.ve-client-redesign .ve-member-card {
    border: 1px solid rgba(98, 164, 255, .16);
    border-radius: 18px;
}

/* ─── Text painted with --ve-navy is INVISIBLE here (owner, 2026-08-15) ───
   The base sheet uses --ve-navy for dark text on a white card. The redesign redefines that
   token as the PAGE BACKGROUND (#080b10), so every one of those rules became black on black:
   the owner found it on the Buy a Package list, where the package names and prices had all
   but disappeared. Sweeping the stylesheet found seventeen selectors in the same state, of
   which the two they could see were only the first to be looked at.

   ⚠️ The token cannot simply be redefined to a readable colour: --ve-navy is also the
   BACKGROUND of the top bar, the modal header and half a dozen controls, and flipping it
   would turn those light-on-light. Text and ground share one name in the base sheet, which is
   the whole cause. So the repair is per-selector, and a probe
   (tests/smoke/spec-175-dark-palette.mjs) now fails on any --ve-navy text with no repair, so
   the next one cannot ship unnoticed.

   The account row's hover state is repaired as a PAIR, below: it painted itself a light grey
   with dark text, which is legible but is a strip of the old palette flashing inside a dark
   menu. Repairing the text alone there would have made it white on light grey — the same
   fault in the other direction — so the ground moves with it. */
html.ve-client-redesign .ve-purchase-pkg-name,
html.ve-client-redesign .ve-stat-value,
html.ve-client-redesign .ve-table th.ve-sortable:hover,
html.ve-client-redesign .ve-pkg-banner-name,
html.ve-client-redesign .ve-pkg-banner-price strong,
html.ve-client-redesign .ve-pkg-history-hint,
html.ve-client-redesign .ve-watch-video,
html.ve-client-redesign .ve-video-title,
html.ve-client-redesign .ve-disclosure,
html.ve-client-redesign .ve-account-toggle,
html.ve-client-redesign .ve-account-name,
html.ve-client-redesign .ve-cal-label-btn,
html.ve-client-redesign .ve-dp-title { color: var(--ve-text); }

/* A link and an emphasis: both have to stand out from the text beside them, not join it. */
html.ve-client-redesign .ve-roster-namelink,
html.ve-client-redesign .ve-week-day-hdr.today { color: var(--ve-rail); }

/* ─── --ve-accent is a BUTTON colour, and fails as text here (owner, 2026-08-15) ───
   Measured at 3.73:1 on a dark card — under the 4.5:1 normal text needs. The same trap as
   --ve-navy one section up: one token doing two jobs, and only one of them survives a change
   of palette. Emphasis takes the lighter blue; anything reporting a fault takes the red,
   which is also what it should have been saying. The inline `color:var(--ve-accent)` in the
   pages was swept to --ve-danger at the same time — every one of them was an error message
   or a destructive control. */
html.ve-client-redesign .ve-auth-phone,
html.ve-client-redesign .ve-week-day-num.today,
html.ve-client-redesign .ve-month-date.today,
html.ve-client-redesign .ve-cls-spots,
html.ve-client-redesign .ve-act-navbtn,
html.ve-client-redesign .ve-act-filter,
html.ve-client-redesign .ve-act-menucount,
html.ve-client-redesign .ve-act-badge { color: var(--ve-rail); }

html.ve-client-redesign .ve-upload-cancel,
html.ve-client-redesign .ve-ptq-error { color: var(--ve-danger); }

/* The second program-structure switch (issue #175) is meaningless while the first is cleared,
   so it dims rather than disappearing — a control that vanishes and returns is harder to
   understand than one that is plainly not available yet. */
.ve-ct-check.is-disabled { opacity: 0.5; cursor: not-allowed; }

/* ─── Class detail, to the approved design (issue #175, 2026-08-15) ───
   The name in the brand blue on the form's own ground instead of in a filled bar; the video as
   a wide still with one play button on it; and the facts, the purpose and the equipment inside
   a single panel. ⚠️ The Close BUTTON keeps the app's shape — the owner named it as the one
   thing in their screenshot not to copy. */
html.ve-client-redesign .ve-cd-header {
    background: transparent;
    border-bottom: 0;
    padding: 1rem 1rem .35rem;
}
html.ve-client-redesign .ve-cd-header h2 {
    color: var(--ve-rail);
    font-size: 1.15rem;
    font-weight: 700;
}

/* The panel. One card holding everything the design draws, so the booking controls below read
   as belonging to the form rather than to the class description. */
html.ve-client-redesign .ve-cd-panel {
    background: var(--ve-surface-2);
    border: 1px solid var(--ve-border);
    border-radius: 14px;
    padding: 1rem;
    margin-bottom: 1rem;
}

html.ve-client-redesign .ve-cd-when {
    margin: 0 0 .9rem;
    font-size: .95rem;
    color: var(--ve-text);
}

/* Labels are small grey capitals and values are plain white, so the eye runs down the values
   without the labels competing with them. */
html.ve-client-redesign .ve-cd-facts { margin: 0; }
html.ve-client-redesign .ve-cd-facts dt {
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--ve-muted);
}
html.ve-client-redesign .ve-cd-facts dd {
    font-size: .95rem;
    color: var(--ve-text);
}

html.ve-client-redesign .ve-cd-rule {
    border: 0;
    border-top: 1px solid var(--ve-border);
    margin: 1.1rem 0;
}

/* A heading over a paragraph, not a label beside a value — hence white and larger than the
   fact labels above it, which is the difference the design draws. */
html.ve-client-redesign .ve-cd-section { margin-bottom: 1rem; }
html.ve-client-redesign .ve-cd-section:last-child { margin-bottom: 0; }
html.ve-client-redesign .ve-cd-sectionhead {
    margin: 0 0 .3rem;
    font-size: .85rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--ve-text);
}
html.ve-client-redesign .ve-cd-body {
    margin: 0;
    font-size: .9rem;
    line-height: 1.5;
    color: var(--ve-muted);
}

/* ─── The video, as a panel (issue #175) ───
   A wide rounded still with one play button in the middle of it, replacing the small "Watch
   Video" text link. The still is the film's own first frame; the panel keeps its dark ground
   and the button when that frame cannot be fetched, so the control is never invisible. */
.ve-watch-video-row { margin: 0 0 1rem; }
.ve-watch-video {
    position: relative;
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--ve-border);
    border-radius: 14px;
    background: #0d1117;
    cursor: pointer;
}
.ve-watch-video-still {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    pointer-events: none;
}
.ve-watch-video-play {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
}
.ve-watch-video-play svg {
    width: 54px;
    height: 54px;
    padding: 14px;
    border-radius: 50%;
    background: rgba(255, 255, 255, .92);
    color: #0d1117;
    box-shadow: 0 6px 20px rgba(0, 0, 0, .45);
}
.ve-watch-video:hover .ve-watch-video-play svg { background: #fff; }
.ve-watch-video:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: 2px; }

.ve-visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* The account row's hover: ground and text together, for the reason in the note above. */
html.ve-client-redesign .ve-account-item:hover,
html.ve-client-redesign .ve-account-item:focus-visible {
    background: rgba(98, 164, 255, .10);
    color: var(--ve-text);
}
html.ve-client-redesign .ve-account-item:hover .ve-account-item-icon,
html.ve-client-redesign .ve-account-item:focus-visible .ve-account-item-icon { color: var(--ve-text); }

/* The package rows are the surface the fault was reported on. Their border was --ve-border
   against a card of nearly the same colour, so the four rows read as one block; a hairline
   of the accent separates them without adding a second card inside a card. */
html.ve-client-redesign .ve-pkg-banner {
    background: var(--ve-surface-2);
    border-color: rgba(98, 164, 255, .18);
}
html.ve-client-redesign .ve-pkg-banner:hover { border-color: var(--ve-rail); }
html.ve-client-redesign .ve-pkg-banner-perclass,
html.ve-client-redesign .ve-pkg-banner-sub { color: var(--ve-muted); }

html.ve-client-redesign .ve-member-credits-count { text-decoration-color: var(--ve-rail); }
html.ve-client-redesign .ve-member-class:hover,
html.ve-client-redesign .ve-row-link:hover,
html.ve-client-redesign .ve-cls-row:hover { background: rgba(98, 164, 255, .07); }

.ve-member-card-actions { display: flex; align-items: center; gap: .55rem; flex: 0 0 auto; }
.ve-member-cta-secondary { background: transparent; color: var(--ve-rail); }
.ve-member-pt-card .ve-member-cta { min-width: 92px; padding: 0 1rem; font-size: .78rem; }

/* ─── Issue #175 — the approved sign-in screen ───
   Unscoped on purpose: it is the screen shown BEFORE anyone is identified, so it cannot hang
   off the client-redesign class the way every other screen here does. The numbers are the
   prototype's own — a 330px column, a 2rem title tightened to -0.045em, and 0.62rem
   letterspaced capitals over each field. */
.ve-signin-page {
    background: radial-gradient(circle at 70% 8%, rgba(0, 93, 232, .32), transparent 18rem), #080b10;
    color: #f7f9fc;
    align-items: center;
    padding: 28px 20px;
}

.ve-signin-card { width: 100%; max-width: 330px; }
/* Create Account asks for more than sign-in does, so it gets a wider column and its own
   scroll — AuthLayout is a non-scrolling viewport-height frame, and a form this long inside
   one would simply have its foot cut off. */
.ve-signin-card-wide { max-width: 420px; }
/* The install banner is fixed to the bottom of the window and covered Create Account's own
   button, which is the one control on the screen that has to be reachable. The room is
   reserved here rather than in the banner, because the banner is not always there and a page
   cannot ask it. */
.ve-signin-page { overflow-y: auto; align-items: flex-start; padding-bottom: 140px; }
.ve-signin-page > .ve-signin-card { margin: auto 0; }

/* A quiet divider inside a long signed-out form. */
.ve-signin-section {
    margin: 1.35rem 0 .7rem;
    padding-top: .9rem;
    border-top: 1px solid #303947;
    color: #f7f9fc;
    font-size: .82rem;
    font-weight: 700;
}
.ve-signin-section span {
    margin-left: .4rem;
    color: #8793a2;
    font-size: .72rem;
    font-weight: 400;
}
.ve-signin-page .ve-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: .7rem; }
.ve-signin-page .ve-alert { font-size: .8rem; }

.ve-signin-eyebrow {
    margin: 0 0 .55rem;
    color: #5aa0ff;
    font-family: "Cascadia Mono", "SFMono-Regular", Consolas, monospace;
    font-size: .75rem;
    font-weight: 800;
    letter-spacing: .15em;
    text-transform: uppercase;
}

.ve-signin-title {
    margin: 0 0 .5rem;
    font-size: 2rem;
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: -.045em;
    color: #f7f9fc;
}

.ve-signin-lede { margin: 0 0 1.55rem; color: #b7c0cc; font-size: .8rem; }

.ve-signin-page .ve-field { margin-bottom: .8rem; }

.ve-signin-page .ve-field label {
    color: #b7c0cc;
    font-size: .62rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
}

.ve-signin-page .ve-field input,
.ve-signin-page .ve-field .ve-input {
    min-height: 44px;
    width: 100%;
    padding: 10px;
    border: 1px solid #303947;
    border-radius: 8px;
    background: #222934;
    color: #f7f9fc;
    font-size: .85rem;
}

.ve-signin-page .ve-field input::placeholder { color: #8793a2; }
.ve-signin-page .ve-field input:focus { outline: 2px solid #5aa0ff; outline-offset: 1px; border-color: #5aa0ff; }

.ve-signin-submit {
    width: 100%;
    min-height: 46px;
    margin-top: .35rem;
    border: 1px solid #005de8;
    border-radius: 10px;
    background: #005de8;
    color: #fff;
    font-size: .85rem;
    font-weight: 800;
    box-shadow: 0 8px 22px rgba(0, 93, 232, .28);
}

.ve-signin-submit:hover:not(:disabled) { background: #1769e8; border-color: #1769e8; }

.ve-signin-links {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    margin-top: 1rem;
    font-size: .75rem;
}

.ve-signin-links a { color: #5aa0ff; }

.ve-signin-notice {
    margin-bottom: .8rem;
    padding: .6rem .8rem;
    border: 1px solid rgba(90, 160, 255, .38);
    border-radius: 8px;
    background: rgba(0, 93, 232, .14);
    color: #cfe0ff;
    font-size: .8rem;
}

/* The social buttons keep their brand faces; only the divider needs to stop being dark-on-dark. */
.ve-signin-page .ve-social-divider span { color: #8793a2; }

/* ─── Issue #175 — Home typography and controls ───
   Every size below is the Figma's own, read out of the file rather than judged by eye
   (node 104:2, a 390px artboard, so its pixels are these pixels on a phone). The owner put
   the two screens side by side on 2026-08-14 and said the type was not sharp and the button
   labels were not readable; measured against the design, ours ran two to four points small
   at every level — 12px button labels against 16px, 13.8px class names against 18px, 11.2px
   captions against 14px. Everything here stays scoped to the client redesign, so the coach
   and administrator shells keep the type they already had.

   ⚠️ The hero's text shadows are switched OFF below. A blurred shadow under a letterform is
   the other half of "not sharp", and it is not in the design: the hero already has its own
   dark scrim (.ve-member-hero::before) doing the contrast work the shadow was added for. */

/* ⚠️ Issue #181 removed this block's two neighbours — the client overrides of
   .ve-member-stat and .ve-member-welcome-sub — with the two hero lines they styled.
   The BASE rules for both classes are still live and must stay: the COACH hero in
   Dashboard.razor carries them ("Classes Today: N" and "Ready when you are, X."), and
   the coach shell was never redesigned, so it never saw these overrides. */

/* Heebo 700 at 28px, tracked -0.7px over 26.6px lines — the design system's display face
   (Heebo is used for nothing else in the app, which is why only its 700 weight ships).
   Sized in vw between floor and ceiling so a 320px screen shrinks it rather than folding
   the greeting onto a second line; 22ch is the same guard from the other direction. */
html.ve-client-redesign .ve-member-welcome {
    font-family: 'Heebo', 'Inter', 'Segoe UI', system-ui, sans-serif;
    font-size: clamp(1.5rem, 7.2vw, 1.75rem);
    line-height: .95;
    letter-spacing: -.025em;
    max-width: 22ch;
    margin: 0;
    text-shadow: none;
}

/* The reader's own name is the one blue word on the screen. */
html.ve-client-redesign .ve-member-welcome .ve-member-welcome-name { color: var(--ve-rail); }

html.ve-client-redesign .ve-member-page { padding: 0; }
html.ve-client-redesign .ve-member-page > .ve-member-card { margin: 0 1rem .7rem; }
html.ve-client-redesign .ve-member-card { padding: .85rem 1rem; }

/* ─── The credit cards ───
   The design stacks each card: heading, then the credit line, then the button on a line of
   its own, all flush with the same left edge (owner, 2026-08-14 — "Figma button alignments
   to the left on smaller screen"). Nothing sits beside anything, so nothing squeezes anything,
   which is what let the heading wrap and made the card a head taller than the one above it.

   ⚠️ Both cards share this arrangement. The Personal Training card was special-cased before,
   with its buttons pushed right and a wide-screen rule that put it back on one row — and a
   card that rearranges itself at 520px is a card that stops matching the one above it. Both
   are the same shape at every width now. Do not reintroduce a per-card layout.

   Figma: heading Inter 700 18px white, credit line Inter 400 14px #9b9b9b, 29px in from the
   card edge. */
html.ve-client-redesign .ve-member-credits { font-size: 1.125rem; font-weight: 700; line-height: 1.22; }
html.ve-client-redesign .ve-member-credits-count {
    display: block;
    text-decoration: none;
    white-space: nowrap;
    font-size: 1.125rem;
    font-weight: 700;
}
html.ve-client-redesign .ve-member-credits small {
    display: block;
    margin-top: .2rem;
    font-size: .875rem;
    font-weight: 400;
    color: #9b9b9b;
    white-space: nowrap;
}
/* "Available Credits: 4" is one line, and only the number takes the blue. */
html.ve-client-redesign .ve-member-credits-sessions {
    color: var(--ve-rail);
    font-weight: 700;
}
/* ⚠️ The heading on each credit card OPENS the packages behind it, and it must look as though
   it does (owner, 2026-08-15 — "this is clickable, but there is no indication it is
   clickable"). The chevron was hidden here and the underline turned off, which left a button
   that read as a plain heading.

   A chevron, not an underline: at 18px bold an underline reads as decoration, and there is no
   hover state on a phone to discover the affordance with. It is set in the blue that means
   "you can act on this" everywhere else in the app, and it slides on press so the tap is
   answered even before the pop-up arrives. */
html.ve-client-redesign .ve-member-credits-chev {
    display: inline-block;
    margin-left: .35rem;
    color: var(--ve-rail);
    font-weight: 700;
    transform: translateX(0);
    transition: transform .12s ease;
}
html.ve-client-redesign .ve-member-credits-btn:hover .ve-member-credits-chev,
html.ve-client-redesign .ve-member-credits-btn:focus-visible .ve-member-credits-chev { transform: translateX(3px); }

/* The two kinds of package, one at a time (issue #175). Full width of the form, above the
   list, and the chosen one is filled rather than merely tinted — the instruction asks that it
   be easy to see which is chosen. */
.ve-pkg-typeseg { margin: 0 0 .9rem; }
.ve-pkg-typeseg .ve-seg-btn { font-size: .8rem; }
html.ve-client-redesign .ve-pkg-typeseg { background: var(--ve-surface-2); }

/* The stack, and the buttons' own row under it. `width: auto` and `flex: 0 0 auto` are both
   overrides of the pre-redesign narrow-screen rules further down the file, which stretch a
   button row edge to edge — the design sizes each button to its words and leaves the rest of
   the line empty. */
html.ve-client-redesign .ve-member-card-split,
html.ve-client-redesign .ve-member-pt-card {
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    gap: .5rem;
}
html.ve-client-redesign .ve-member-card-actions {
    width: auto;
    justify-content: flex-start;
    flex-wrap: wrap;
    gap: .5rem;
}
html.ve-client-redesign .ve-member-card-actions .ve-member-cta { flex: 0 0 auto; }
html.ve-client-redesign .ve-member-pt-card .ve-member-credits { text-align: left; }

/* The Figma's button is 95 x 30 on an 8px radius, filled #005de8, labelled in Inter 600 at
   16px in capitals. Every one of those is taken except the height: 30px is below this
   project's 44px minimum touch target, which has exactly one granted exception and is not
   getting a second by accident. The label — the thing the owner could not read — is the
   design's own 16px. "Buy" stays sentence case in the markup and is capitalised here, so the
   C# string is never the place an ampersand or a stray capital has to be maintained. */
/* The prototype's own button: 44px tall on a 10px radius, and the blue one carries a lift.
   ⚠️ 44px is the design's number as well as this project's touch-target floor — the collision
   people expected between the two was never there, and the 30px in the Figma is the drawing
   the prototype was made FROM. The label is the one deliberate divergence: the prototype sets
   0.75rem and the owner said outright on 2026-08-14 that it could not be read, so it stays at
   the Figma's 16px. A later instruction beats an earlier artefact. */
html.ve-client-redesign .ve-member-cta {
    min-height: 44px;
    min-width: 95px;
    padding: 0 1rem;
    border-radius: 10px;
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: .02em;
    text-transform: uppercase;
    white-space: nowrap;
    box-shadow: 0 8px 22px rgba(0, 93, 232, .28);
}
html.ve-client-redesign .ve-member-cta-secondary {
    border-color: #68788d;
    background: transparent;
    color: #fff;
    box-shadow: none;
}

/* The classes panel: heading and the day it covers, then the rows. Figma: headings and class
   names Inter 700 18px white, the day and the class line Inter 400 14px and 11px #9b9b9b. */
html.ve-client-redesign .ve-member-card-title { font-size: 1.125rem; font-weight: 700; }
html.ve-client-redesign .ve-member-card-sub { margin: .25rem 0 0; font-size: .875rem; color: #9b9b9b; }
html.ve-client-redesign .ve-member-card-note { margin: .75rem 0 0; font-size: .875rem; color: #9b9b9b; }
html.ve-client-redesign .ve-member-class-name { font-size: 1.125rem; font-weight: 700; }
html.ve-client-redesign .ve-member-class-meta { font-size: .6875rem; color: #9b9b9b; }
html.ve-client-redesign .ve-member-class-right { font-size: .6875rem; color: #9b9b9b; }

/* The tab bar, from the prototype: a near-black bar under a blue hairline, its labels in
   capitals, and the active tab lit by a wash rising from the floor rather than by a rule
   above it. ⚠️ The size is the one divergence — the prototype sets 0.53rem, which is 8.5px
   and unreadable on a phone; the design file's own 11px is used instead, for the same reason
   the button label is 16px. */
html.ve-client-redesign .ve-tabbar {
    background: rgba(5, 8, 12, .98);
    border-top: 1px solid rgba(90, 160, 255, .2);
    box-shadow: 0 -10px 28px rgba(0, 0, 0, .28);
}
html.ve-client-redesign .ve-tab {
    color: #9da7b5;
    font-size: .6875rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    border-top-color: transparent;
}
html.ve-client-redesign .ve-tab.is-active {
    color: var(--ve-rail);
    border-top-color: transparent;
    background: linear-gradient(to top, rgba(0, 93, 232, .13), transparent 70%);
}

/* "Any class I'm booked for shows Booked in green" (meeting notes, issue #175). The green is
   the design's own; the mint-on-white chip it replaces belonged to the old palette and was
   the last piece of it left on the landing. */
html.ve-client-redesign .badge-active {
    background: var(--ve-green-soft);
    color: var(--ve-green);
}

/* ⚠️ TRANSPARENT since issue #183, and it was the page colour before. The picture occupies
   only the top ~1.06 screen-widths of the window, and this header stands in exactly that
   band — painted, it is the one page in the application where the photograph would never be
   seen at all. Its own text reads against the veil (see the backdrop rules above). */
html.ve-client-redesign .ve-classes-head { background: transparent; }
html.ve-client-redesign .ve-classes-day.has-classes { background: rgba(47, 125, 244, .18); }

/* ─── Grounds the picture made necessary (issue #183) ───
   Both of these carried NO colour of their own. That was invisible while the page behind
   them was the night ground; with the photograph there, their text sat on the room. They
   take the design's own card — the prototype draws the package card and the activity card
   as the same object as the class card, at the same colour, so this is the design being
   applied rather than a new one being invented. */
html.ve-client-redesign .ve-pkg-card { background: var(--ve-card); }
html.ve-client-redesign .ve-act-hero { background: var(--ve-card); }
html.ve-client-redesign .ve-classes-day.has-booking::after { background: var(--ve-rail); }
html.ve-client-redesign .ve-seg { background: #171d26; border: 1px solid var(--ve-border); }
html.ve-client-redesign .ve-seg-btn.is-on { background: var(--ve-primary); color: #fff; }
html.ve-client-redesign .ve-cls-btn.is-full,
html.ve-client-redesign .ve-cls-btn.is-booked { background: var(--ve-surface-2); }

html.ve-client-redesign .ve-page-header h1,
html.ve-client-redesign .ve-modal-header h2,
html.ve-client-redesign .ve-field label,
html.ve-client-redesign .ve-table,
html.ve-client-redesign .ve-table th,
html.ve-client-redesign .ve-table td,
html.ve-client-redesign .ve-dl dd { color: var(--ve-text); }

html.ve-client-redesign .ve-input,
html.ve-client-redesign select.ve-input,
html.ve-client-redesign textarea.ve-input {
    background: var(--ve-surface-2);
    color: var(--ve-text);
    border-color: var(--ve-control-border);
}

html.ve-client-redesign .ve-modal-header { background: #0c1118; border-bottom-color: var(--ve-border); }

/* ⚠️ Class detail is the one form whose head is NOT a filled bar (issue #175). These two must
   sit AFTER the generic modal-header rules above, which are more specific than the class on
   its own and would otherwise win on source order — the reason the blue title came out white
   the first time. */
html.ve-client-redesign .ve-modal-header.ve-cd-header { background: transparent; border-bottom: 0; }
html.ve-client-redesign .ve-modal-header.ve-cd-header h2 { color: var(--ve-rail); }
html.ve-client-redesign .ve-modal-body,
html.ve-client-redesign .ve-auth-card-body { background: transparent; color: var(--ve-text); }
html.ve-client-redesign .ve-link-btn { color: var(--ve-rail); }

/* ─── Every button label reads on its own ground (owner, 2026-08-15) ───
   Measured: 20 button variants on 5 surfaces — the page, a card, a framed modal's header, a
   modal body, and a modal with no framed header. 26 of the 102 failed 4.5:1.

   • Close was 1:1. `.ve-btn-ghost` takes its colour from --ve-navy, which the redesign makes
     the page colour; only a framed modal HEADER overrode it, so every Close outside one was
     the colour of what was behind it.
   • The blue buttons were 3.92:1 — under, and not by a rounding error. They used --ve-accent
     (#2f7df4). The approved prototype's button is #005de8, which is --ve-primary and reads at
     5.6:1. The design's own value was the fix.
   • The primary button's ground was --ve-navy: a button the same colour as the page, with a
     white label floating on it. Also #005de8 now.
   • BOOKED was blue at 3.83:1 and is green at 8:1 — which is also what the meeting notes ask
     for ("any class I'm booked for shows Booked in green").
   ⚠️ The framed header keeps its inverted pair (white button, dark label, white-outlined
   Close) — it sits on the navy header bar and was already correct, so it is re-stated below
   these rules rather than left to lose on source order. */
html.ve-client-redesign .ve-btn-ghost {
    color: var(--ve-text);
    border-color: #68788d;
}
html.ve-client-redesign .ve-btn-ghost:hover { background: rgba(98, 164, 255, .10); }

html.ve-client-redesign .ve-btn-primary,
html.ve-client-redesign .ve-btn-accent,
html.ve-client-redesign .ve-cls-btn.is-book,
html.ve-client-redesign .ve-member-cta {
    background: var(--ve-primary);
    border-color: var(--ve-primary);
    color: #fff;
}
html.ve-client-redesign .ve-btn-primary:hover,
html.ve-client-redesign .ve-btn-accent:hover,
html.ve-client-redesign .ve-member-cta:hover {
    background: var(--ve-accent-hover);
    border-color: var(--ve-accent-hover);
}

html.ve-client-redesign .ve-cls-btn.is-booked {
    color: var(--ve-green);
    border-color: rgba(59, 208, 151, .45);
}

/* The framed modal header is inverted on purpose — a navy bar with white controls on it. */
html.ve-client-redesign .ve-modal-header .ve-btn-primary { background: #fff; color: var(--ve-navy); border-color: #fff; }
html.ve-client-redesign .ve-modal-header .ve-btn-primary:hover { background: #eef2ff; border-color: #eef2ff; }
html.ve-client-redesign .ve-modal-header .ve-btn-ghost { color: #fff; border-color: rgba(255, 255, 255, .45); }
html.ve-client-redesign .ve-modal-header .ve-btn-ghost:hover { background: rgba(255, 255, 255, .14); }

html.ve-client-redesign .ve-auth-page {
    min-height: 0;
    padding: 0;
    background: transparent;
}

/* ⚠️ …but not to zero at the FOOT inside the member shell. The rule above is what was
   cancelling the tab-bar clearance set further up this file, and it is why Sign Out could not
   be reached: `.ve-content` is a grid scroller, and padding on a SCROLL CONTAINER is not part
   of the scrollable extent past overflowing content — only padding on the content itself is.
   Measured at 390x844: 70px of clearance on the scroller, 0 reachable; the same 70px on this
   element, all of it reachable. The coach shell gets the same clearance from the rule further
   up, which this one would otherwise override. */
html.ve-client-redesign.ve-member .ve-auth-page {
    align-self: start;
    padding-bottom: calc(var(--ve-member-pad-bottom) + 1rem);
}
html.ve-client-redesign .ve-auth-card { width: 100%; max-width: 720px; margin: 0 auto; }
.ve-profile-section { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--ve-border); }
.ve-profile-section h3 { margin: 0 0 .75rem; font-size: .95rem; color: var(--ve-text); }
/* Issue #175 — the sign-out sits at the foot of Profile and must be plainly there: full width,
   its own rule above it, and room beneath so it never ends up flush against the tab bar with
   nothing after it to show the page has finished. */
.ve-profile-signout {
    margin-top: 1.5rem;
    padding-top: 1rem;
    padding-bottom: .5rem;
    border-top: 1px solid var(--ve-border);
}
.ve-profile-signout .ve-btn { width: 100%; }
html.ve-client-redesign .ve-profile-signout .ve-btn { color: #ff8794; border-color: #71313a; }

/* Issue #175 — a state chip that borrows the primary button's shape so it sits level with
   Close, but is not a control: no pointer cursor, and nothing to press. */
.ve-btn-state {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: default;
    pointer-events: none;
    user-select: none;
}

/* Issue #175 — the chart heading names what is drawn and the span it covers; the period
   controls sit UNDER the bars, which is where the meeting put them and where the approved
   prototype draws them. The margin above separates them from the x-axis labels. */
.ve-act-charttitle { font-size: .95rem; font-weight: 700; color: var(--ve-text); }
.ve-act-periodlabel { margin-left: auto; font-size: .82rem; color: var(--ve-muted); }
.ve-act-periods { display: grid; grid-template-columns: repeat(5, 1fr); gap: .35rem; width: 100%; margin-top: .9rem; }
.ve-act-period {
    min-height: 42px;
    padding: 0 .35rem;
    border: 1px solid var(--ve-control-border);
    border-radius: 999px;
    background: transparent;
    color: var(--ve-muted);
    font: inherit;
    font-size: .82rem;
    font-weight: 700;
    cursor: pointer;
}
.ve-act-period.is-active { background: var(--ve-primary); border-color: var(--ve-primary); color: #fff; }
.ve-act-delta { margin-left: auto; font-size: .8rem; color: var(--ve-muted); text-align: right; }
.ve-act-delta.is-up { color: #74d69b; }
.ve-act-delta.is-down { color: #ff8794; }
html.ve-client-redesign .ve-act-bar { background: var(--ve-primary); }
html.ve-client-redesign .ve-act-card,
html.ve-client-redesign .ve-act-list { margin-left: 0; margin-right: 0; }

@media (max-width: 620px) {
    .ve-member-pt-card { align-items: flex-start; flex-direction: column; }
    .ve-member-card-actions { width: 100%; }
    .ve-member-pt-card .ve-member-cta { flex: 1 1 0; min-width: 0; }
    .ve-act-hero { flex-wrap: wrap; }
    .ve-act-delta { width: 100%; margin-left: 0; text-align: left; }
}

/* ============================================================================
   THE COACH HALF OF THE MEMBER SHELL  (issue #155)
   The coach reuses the client's rows wholesale — .ve-cls-row, .ve-act-* and the
   week strip are all shared. Only what a coach has and a client does not is here.
   ============================================================================ */

/* The My/All scope switch, under the Classes/Clients segment. Smaller than the
   segment above it: it qualifies that choice rather than competing with it. */
.ve-seg-scope { margin-top: 0.5rem; }
.ve-seg-scope .ve-seg-btn { min-height: 38px; font-size: 0.85rem; }

/* The booked count on a class row — the one number a coach reads a row for, so it
   is body ink rather than the muted grey the rest of the meta lines use. */
.ve-cls-booked { color: var(--ve-text); font-weight: 600; }

/* Where a client's row carries BOOK, a coach's carries where the class has got to. */
.ve-cls-status {
    align-self: center;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--ve-muted);
    white-space: nowrap;
}

/* The class is over and was never confirmed (issue #112). The established
   attention trio, the same one .badge-pending uses — #92400e on #fef3c7 is 7.4:1. */
.ve-cls-flag {
    align-self: center;
    max-width: 108px;
    padding: 0.3rem 0.5rem;
    background: #fef3c7;
    border: 1px solid #fcd34d;
    border-radius: 8px;
    font-size: 0.72rem;
    font-weight: 700;
    line-height: 1.2;
    text-align: center;
    color: #92400e;
}

/* ─── The Clients segment: the day's bookings, grouped by class ─── */
.ve-roster-group { padding: 0.35rem 0 0.6rem; border-bottom: 1px solid var(--ve-border); }

.ve-roster-grouphead {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.6rem;
    padding: 0.5rem 0.15rem 0.35rem;
}

.ve-roster-groupname  { font-size: 1rem; font-weight: 700; color: var(--ve-text); }
.ve-roster-groupcount { flex: 0 0 auto; font-size: 0.85rem; color: var(--ve-muted); white-space: nowrap; }

/* A client row is the whole tap target — it opens the contact popup, so it has to
   clear 44px the same as any other control. */
.ve-roster-client {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.6rem;
    width: 100%;
    min-height: 44px;
    padding: 0 0.15rem;
    background: none;
    border: 0;
    text-align: left;
    cursor: pointer;
}

.ve-roster-client:focus-visible { outline: 2px solid var(--ve-rail); outline-offset: -2px; border-radius: 6px; }
.ve-roster-clientname   { font-size: 0.95rem; color: var(--ve-text); }
.ve-roster-clientstatus { flex: 0 0 auto; font-size: 0.8rem; color: var(--ve-muted); white-space: nowrap; }

/* ─── Activity's payroll line ─── */
.ve-act-total { margin: 0 0 0.85rem; }
.ve-act-warn {
    margin: 0.6rem 0 0;
    padding: 0.55rem 0.7rem;
    background: #fef3c7;
    border: 1px solid #fcd34d;
    border-radius: 8px;
    font-size: 0.88rem;
    line-height: 1.45;
    color: #92400e;
}
.ve-act-note { margin: 0.6rem 0 0; font-size: 0.88rem; color: var(--ve-muted); }

@media (max-width: 400px) {
    .ve-cls-flag { max-width: 92px; font-size: 0.68rem; }
    .ve-classes-month { font-size: 1.5rem; }
    .ve-cls-btn       { min-width: 88px; font-size: 0.8rem; }
    .ve-classes-day   { min-height: 52px; }
    .ve-classes-daynum { width: 30px; height: 30px; font-size: 0.9rem; }
    .ve-sched-empty-title { font-size: 1.3rem; }
    .ve-act-count      { font-size: 2.6rem; }
    .ve-act-heroLabel  { font-size: 1.18rem; }
    .ve-act-month      { font-size: 1.02rem; }
    .ve-act-xlabel     { font-size: 0.62rem; }
}

/* Issue #154 G4 — the "Updated N minutes ago" snapshot-age line every offline-first
   surface carries. Over the member hero photograph it lightens; elsewhere it is the
   standard muted small print. */
.ve-snap-age { font-size: 12px; color: var(--ve-muted); margin: 2px 0 6px; }
.ve-member-hero .ve-snap-age { color: rgba(255, 255, 255, .82); margin: 0; }
