/* common.css - GAS retirement, shared visual chrome.
 * Real port of WebApp_Common.html's shared rules - the invariant every
 * GAS page already builds on (CSS vars, topbar, card, buttons, form
 * inputs, spinner, footer). Ported wholesale rather than hand-copied
 * fragments into each page - a page's own <link> to this file plus its
 * own page-specific <style> block (overrides only) mirrors GAS's real
 * structure exactly (WebApp_Common.html + e.g. 8.1_OTPPortal.html's own
 * block). Grows as later pages need more of WebApp_Common.html's rules -
 * not a speculative full port of all 2487 lines up front.
 */

:root {
  --color-dark: #2C2C2C;
  --color-brand: #F47920;
  --color-brand-dark: #D96B10;
  --color-green: #3D9B3C;
  --color-green-dark: #2D7A2D;
  --color-pos: #3D9B3C;
  --color-neg: #D93025;
  --color-neg-dark: #B91C1C;
  --color-warn: #F59E0B;
  --color-warn-bg: #FEF3C7;
  --color-warn-border: #FDE68A;
  --color-warn-text: #92400E;
  --color-neu: #64748B;
  --color-muted: #94A3B8;
  --page-bg: #FDF8F5;
  --panel-bg: #FFFFFF;
  --border: #E2E8F0;
  --border-dark: #CBD5E1;
  --surface-1: #F1F5F9;
  --text-primary: #0F172A;
  --text-secondary: #334155;
  --text-tertiary: #475569;
  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
  /* v151.365 - direct report, circled live: "Courier New" (the old value)
     has thin strokes and a small x-height at UI sizes, compounding the
     grey-text contrast issue above rather than helping it - swapped for
     each OS's own modern system monospace (heavier default weight, larger
     x-height, designed for on-screen legibility, not print) - "Courier
     New" stays only as the last-resort fallback for the rare device with
     none of the modern ones installed. Used app-wide (template/event keys,
     account IDs, PIN/OTP digit boxes) - not scoped to one screen, this was
     never a light-theme-specific problem. */
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Roboto Mono", "Courier New", monospace;
  --radius-sm: 4px;
  --radius: 6px;
  --radius-lg: 12px;
  --card-max-width: 560px;
  --shadow: 0 10px 25px rgba(0,0,0,0.10), 0 1px 3px rgba(0,0,0,0.08);

  /* App-wide light/dark theme, Phase 1 (v151.350) - "chrome" tokens for the
     dark-glass shell elements (topbar/drawer/nav-panel), which have always
     hardcoded rgba(44,44,44,0.85)/#fff literals directly rather than a
     variable. This :root block IS the dark set (the app's unchanged
     default look) - see the [data-theme="light"] block below for the
     light-mode redefinition of every one of these. Named/scaled to match
     Mailbox's own pre-existing light palette exactly (--mb-fg: #1e293b,
     --mb-border: #e2e8f0, --mb-hover: #f1f5f9) rather than inventing a
     second light aesthetic - see staff-mailbox.js's own theme functions,
     Phase 2 generalizes those onto this same token set. */
  --chrome-bg: rgba(44,44,44,0.85);
  --chrome-fg: #fff;
  --chrome-fg-70: rgba(255,255,255,0.7);
  --chrome-fg-65: rgba(255,255,255,0.65);
  --chrome-fg-60: rgba(255,255,255,0.6);
  --chrome-fg-50: rgba(255,255,255,0.5);
  --chrome-fg-45: rgba(255,255,255,0.45);
  --chrome-fg-30: rgba(255,255,255,0.3);
  --chrome-fg-25: rgba(255,255,255,0.25);
  --chrome-fg-20: rgba(255,255,255,0.2);
  --chrome-border-08: rgba(255,255,255,0.08);
  --chrome-border-12: rgba(255,255,255,0.12);
  --chrome-hover-08: rgba(255,255,255,0.08);
  --chrome-hover-10: rgba(255,255,255,0.1);
  --chrome-badge-bg: rgba(255,255,255,0.10);

  /* Phase 3 (v151.352+) - the "ink" scale. staff.css's own content screens
     (Submissions/Clients/Form Builder/etc, ~500 rules) hardcode
     rgba(255,255,255,X) at MANY discrete alpha steps (0.05 through 0.9) for
     white-text-on-dark-panel, plus plain #fff for full-opacity text - too
     many distinct steps to give each its own named token pair the way
     --chrome-* did for the smaller topbar surface. Instead,
     --ink holds just the RGB channels (no alpha) as a bare comma triplet -
     every existing rgba(255,255,255,X) call site converts to
     rgba(var(--ink),X), preserving its own original alpha exactly, no
     per-step token needed. --ink-solid covers the handful of plain
     `color: #fff`/`background: #fff` call sites that are genuine panel
     text/surface (NOT the accent-badge convention already established in
     Phase 1 - white text ON a brand-orange/status-color badge stays literal
     #fff always, unconditionally, in both themes, exactly like
     .nav-btn.active/.drawer-btn.active already do - only ambient panel text
     sitting on the panel's own background converts). */
  --ink: 255,255,255;
  --ink-solid: #fff;

  /* v151.368 - modals, direct instruction: "dark theme, dark modals and
     light theme light modals... take the current css as templates (mostly)
     and apply based on theme." Every modal in this app (.modal-box's ~16
     consumers, .review-modal-box, the emt- and doc-preview full-page
     overlays) has always been permanently light, by original design,
     regardless of the chrome theme - unrelated to the --chrome- and --ink
     work above, which deliberately never touched modals at all.
     v151.370 - direct follow-up correction: --modal-bg/-fg/-muted/-border
     etc originally used an invented one-off palette (#252535 and friends,
     copied from Form Builder's own OLD hardcoded literals) instead of the
     app's own already-established dark scale. Reworked to reuse the SAME
     --chrome-fg and --ink family every other dark surface in this app
     already uses - --modal-fg/-muted/-border/-input-bg/-input-border/
     -surface below are now numerically identical to --chrome-fg/--ink at
     those same alpha steps, not a second palette. The one deliberate
     difference,
     direct instruction: "the modal has a darker back[ground] to be seen
     separate from the dark render of the rest of the UI" - --modal-bg is a
     darker, more opaque black (rgba(20,20,20,0.97)) than --chrome-bg
     (rgba(44,44,44,0.85)) specifically so a modal reads as a distinct
     floating layer over the dimmed page behind it, not blending into it. */
  --modal-bg: rgba(20,20,20,0.97);
  --modal-fg: #fff;
  --modal-muted: rgba(255,255,255,0.65);
  --modal-border: rgba(255,255,255,0.12);
  --modal-input-bg: rgba(255,255,255,0.07);
  --modal-input-border: rgba(255,255,255,0.15);
  --modal-surface: rgba(255,255,255,0.05);
  --modal-surface-hover: rgba(255,255,255,0.1);
}

/* App-wide light theme (v151.350) - set on <html data-theme="light">, done
   as early as possible client-side (Phase 2, common.js) to avoid a flash of
   the dark chrome on load. Absent entirely = dark, the app's unchanged
   default. Only the tokens that actually differ between themes are
   redefined here - every non-chrome token (brand orange, status colors,
   page/card backgrounds, radii, shadow) is already light-appropriate and
   stays exactly as :root defines it above. */
/* v151.364 - real legibility bug, reported live with actual WCAG numbers
   checked to confirm it, not just eyeballed: the SAME alpha fraction reused
   from dark theme reads fine on a dark panel but fails contrast on a light
   one - alpha-blending toward white lightens much faster than blending
   toward black darkens, perceptually. Example: --chrome-fg-65 at its old
   light value (rgba(30,41,59,0.65)) measures 4.72:1 against white - barely
   clears WCAG AA body text (4.5:1) - while the SAME alpha against this
   panel's own dark background measures 6.82:1, comfortably legible. Every
   level below here was independently rebalanced (contrast computed both
   ways, light alpha raised until it matched or exceeded the dark theme's
   own contrast at that level - see the deploy_log entry for the actual
   numbers) - NOT a uniform "+0.15" bump, since the gap widens at lower
   alphas. Dark theme's own values (the :root block above) are completely
   untouched - this only fixes what light theme resolves each level TO.
   v151.365 - direct follow-up after seeing v151.364 live: "I would like one
   more bump in the black of the color in equal proportion, so it still
   stays graded between main, sub, ssub text groups." A flat +0.08 applied
   to every level here (each level's own contrast, both before/after,
   checked - see deploy_log) - the relative gaps between levels are
   untouched (same absolute offsets), everything just reads darker. */
:root[data-theme="light"] {
  --chrome-bg: rgba(255,255,255,0.92);
  --chrome-fg: #1e293b;
  --chrome-fg-70: rgba(30,41,59,0.9);
  --chrome-fg-65: rgba(30,41,59,0.86);
  --chrome-fg-60: rgba(30,41,59,0.83);
  --chrome-fg-50: rgba(30,41,59,0.76);
  --chrome-fg-45: rgba(30,41,59,0.7);
  --chrome-fg-30: rgba(30,41,59,0.54);
  --chrome-fg-25: rgba(30,41,59,0.48);
  --chrome-fg-20: rgba(30,41,59,0.42);
  --chrome-border-08: rgba(30,41,59,0.08);
  --chrome-border-12: rgba(30,41,59,0.12);
  --chrome-hover-08: rgba(30,41,59,0.06);
  --chrome-hover-10: rgba(30,41,59,0.08);
  --chrome-badge-bg: rgba(30,41,59,0.06);
  --ink: 30,41,59;
  --ink-solid: #1e293b;
  /* v151.368 - matches .modal-box's own ORIGINAL always-light values
     exactly (#fff/--text-primary/--color-muted/--border/--border-dark) -
     a light-theme user's modals look pixel-identical to before this
     version, zero change. */
  --modal-bg: #fff;
  --modal-fg: var(--text-primary);
  --modal-muted: var(--color-muted);
  --modal-border: var(--border);
  --modal-input-bg: #fff;
  --modal-input-border: var(--border-dark);
  --modal-surface: #f3f4f6;
  --modal-surface-hover: #e5e7eb;
}

/* 2026-09-21 - client pages are ALWAYS light (the user: "client is ALWAYS
   light"). They never set data-theme, so the generic .modal-box family (the
   unsaved-changes modal, confirmModal, the reload prompt) resolved the dark
   default tokens above and rendered a dark modal on a light page. The same
   light values as the light theme's own, scoped to .client-shell pages. */
body.client-shell {
  --modal-bg: #fff;
  --modal-fg: var(--text-primary);
  --modal-muted: var(--color-muted);
  --modal-border: var(--border);
  --modal-input-bg: #fff;
  --modal-input-border: var(--border-dark);
  --modal-surface: #f3f4f6;
  --modal-surface-hover: #e5e7eb;
}

* { box-sizing: border-box; }

body {
  font-family: var(--font);
  background-color: var(--page-bg);
  background-size: cover; background-position: center; background-repeat: no-repeat;
  color: var(--text-secondary);
  font-size: 14px; line-height: 1.5; margin: 0; min-height: 100vh;
  /* v151.44 - real bug: mobile rubber-band/bounce scroll revealed a strip of
     this light --page-bg below the (dark-themed) footer on a real phone -
     jarring on a page that's dark end to end. Disabling the bounce is the
     robust fix (works regardless of any single page's color scheme), not a
     per-page background color patch. */
  overscroll-behavior-y: none;
}

/* Generic full-width sticky topbar (Dashboard/Staff/Form/Profile/Groups).
   8.1_OTPPortal.html-style pages override this with their own narrower,
   rounded, centered version - see that page's own <style> block, not here. */
.topbar {
  background: var(--chrome-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
  padding: 12px 30px; display: flex; align-items: center; gap: 14px;
  flex-shrink: 0; position: sticky; top: 0; z-index: 50;
}
.topbar-logo-img { height: 32px; display: block; flex-shrink: 0; }
.topbar-logo { font-size: 20px; font-weight: 700; color: var(--color-brand); letter-spacing: -0.5px; }
.topbar-sub { font-size: 13px; color: var(--color-neu); letter-spacing: 1px; text-transform: uppercase; }
.topbar-badge { font-size: 11px; font-weight: 700; font-family: var(--font-mono); background: #ff9800; color: #000; padding: 2px 8px; border-radius: 4px; margin-left: 8px; text-transform: none; letter-spacing: normal; }

/* Session countdown pill - session-chrome.js finds #wa-session-countdown-full
   by id and keeps it live; any authenticated page opts in by including that
   id somewhere in its topbar. */
.wa-session-badge { margin-left: auto; display: flex; align-items: center; gap: 8px; font-size: 12px; background: var(--chrome-badge-bg); padding: 7px 16px; border-radius: var(--radius-sm); white-space: nowrap; flex-shrink: 0; }
.wa-session-badge-label { color: var(--chrome-fg-60); font-weight: 500; }
#wa-session-countdown-full { font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: 0.3px; color: var(--chrome-fg); }

/* SysAdmin role-emulation picker + topbar Switch Role button - real port of
   WebApp_Common.html's own rules. staff-role-switch.js inserts the label/
   button as siblings right before .wa-session-badge inside .topbar. */
.wa-rep-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(44,44,44,0.55); z-index: 10002; align-items: center; justify-content: center; }
.wa-rep-overlay.active { display: flex; }
.wa-rep-card { background: var(--modal-bg); border-radius: 10px; padding: 24px 28px; max-width: 360px; width: 90%; box-shadow: 0 12px 32px rgba(0,0,0,0.25); }
.wa-rep-title { font-size: 15px; font-weight: 700; color: var(--modal-fg); margin-bottom: 8px; text-align: center; }
.wa-rep-msg { font-size: 13px; color: var(--modal-muted); margin-bottom: 16px; line-height: 1.5; text-align: center; }
.wa-rep-row { margin-bottom: 12px; }
.wa-rep-label { font-size: 12px; color: var(--modal-muted); display: block; margin-bottom: 4px; }
.wa-rep-select { width: 100%; padding: 8px 10px; border-radius: 6px; border: 1px solid var(--modal-input-border); background: var(--modal-input-bg); color: var(--modal-fg); font-size: 13px; }
.wa-rep-btns { display: flex; gap: 8px; justify-content: center; margin-top: 16px; flex-wrap: wrap; }
.wa-rep-btn { padding: 8px 16px; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer; }
.wa-rep-btn-ignore { border: 1px solid var(--modal-border); background: var(--modal-surface); color: var(--modal-muted); }
.wa-rep-btn-ignore:hover { background: var(--modal-surface-hover); }
.wa-rep-btn-emulate { border: none; background: var(--color-brand); color: #fff; }
.wa-rep-btn-emulate:hover { background: var(--color-brand-dark); }
.wa-rep-btn-revert { border: 1px solid var(--color-neg-dark); background: var(--modal-surface); color: var(--color-neg-dark); }
.wa-rep-card .form-input { background: var(--modal-input-bg); border-color: var(--modal-input-border); color: var(--modal-fg); }
.wa-role-switch-btn { font-size: 11px; font-weight: 600; padding: 4px 10px; border-radius: 6px; border: 1px solid var(--chrome-fg-30); background: transparent; color: var(--chrome-fg); cursor: pointer; white-space: nowrap; }
.wa-role-switch-btn:hover { background: var(--chrome-hover-10); }
.wa-role-switch-btn-emulating { border-color: var(--color-brand); background: var(--color-brand); color: #fff; }
.wa-role-switch-btn-emulating:hover { background: var(--color-brand-dark); }
.wa-role-emulate-label { font-size: 11px; font-weight: 600; color: var(--chrome-fg-70); white-space: nowrap; }

/* Mobile drawer nav - shared by any page that includes app-nav's markup/JS
   (Staff via StaffNav.js, future client pages via app-nav.js's own
   renderSharedNav/renderDesktopNav).
   v151.42 - deliberately NOT a verbatim port any more. AppNav.html's original
   500px-wide/25px-button sizing was designed for a 2-item client drawer and
   genuinely doesn't fit a real phone screen at all (500px alone exceeds most
   phones' full viewport width) - confirmed unusable via a real live
   screenshot on Staff's ~15-item drawer, not just a guess this time. Real
   mobile-app-typical scale instead: width caps at the smaller of 85% of the
   viewport or 320px, text/padding sized for a dense list rather than 2 big
   buttons. Client pages (Dashboard/Groups/Profile) reusing this same drawer
   later inherit this new scale too - if a future 2-item drawer ever wants
   AppNav's old chunkier feel back, that's a deliberate per-page override, not
   a reason to revert the shared default. */
.drawer-backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 100; }
.drawer-backdrop.open { display: block; }
.drawer { position: fixed; top: 0; left: 0; height: 100%; width: min(85vw, 320px); background: var(--chrome-bg); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); transform: translateX(-100%); transition: transform 0.25s ease; z-index: 101; overflow-y: auto; display: flex; flex-direction: column; }
.drawer.open { transform: translateX(0); }
.drawer-identity { padding: 16px 16px 14px; border-bottom: 1px solid var(--chrome-border-08); }
.drawer-identity-name { font-weight: 700; color: var(--chrome-fg); font-size: 17px; line-height: 1.3; }
.drawer-identity-detail { font-size: 12px; color: var(--chrome-fg-45); margin-top: 4px; }
.drawer-identity-id { font-size: 11px; color: var(--chrome-fg-25); margin-top: 3px; font-family: var(--font-mono); word-break: break-all; }
.drawer-identity-tag { display: inline-block; margin-top: 8px; padding: 3px 10px; border-radius: 4px; font-size: 11px; font-weight: 700; background: var(--color-brand); color: #fff; text-transform: uppercase; }
.drawer-nav { flex: 1; padding: 6px 0; overflow-y: auto; }
.drawer-footer { padding: 6px 0 10px; border-top: 1px solid var(--chrome-border-12); }
/* v151.44 - tightened again per direct feedback ("font can reduce a little
   and spacing a little tighter") - was 13px/16px padding, 15px font. */
.drawer-btn { display: block; width: 100%; text-align: left; padding: 10px 16px; font-size: 13px; color: var(--chrome-fg-70); background: none; border: none; cursor: pointer; font-family: var(--font); transition: background 0.12s; }
.drawer-btn:hover:not(.drawer-btn-disabled) { background: var(--chrome-hover-08); color: var(--chrome-fg); }
.drawer-btn.active { background: var(--color-brand); color: #fff; }
.drawer-btn-disabled { opacity: 0.35; cursor: default; pointer-events: none; }

.card { background: rgba(255,255,255,0.95); border: 1px solid var(--border); border-radius: var(--radius-lg); margin-bottom: 20px; overflow: hidden; box-shadow: var(--shadow); }
.card-header { background: var(--surface-1); padding: 10px 16px; font-size: 11px; font-weight: 700; color: var(--color-dark); text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid var(--border); }
.card-header-warn { background: var(--color-warn-bg); color: var(--color-warn-text); }
.card-header-pos { background: #DCFCE7; color: #166534; }
.card-header-neg { background: #FEE2E2; color: #9F1239; }
.card-body { padding: 16px; }
.card-body-flush { padding: 0; }

/* v151.45 - BUTTON PATTERN, LOCKED IN (explicit user direction: "you need
   lock down the button styling in the app. we dont want to rehash each
   time"). Two standard patterns going forward - pick one of these, don't
   invent a third:
   1. .btn-primary (below) - solid orange background, white text. THE
      default action button - use this unless there's a specific reason not
      to, including for pairs of equally-weighted actions (e.g. login.html's
      "OTP Login"/"Reset MPIN" - both switched from .btn-secondary to
      .btn-primary here, v151.45, real user correction, not a guess).
   2. .btn-view-primary (staff.css) - orange border, white background,
      orange text; fills solid orange on hover/click. Used for a lower-
      emphasis "+ Add" action sitting in a panel header next to a title,
      not a form's own submit action.
   .btn-secondary (dark, below) is legacy GAS styling - still used by a few
   not-yet-revisited buttons (Resend/Sign Out/etc in login.html), left alone
   for now since they weren't part of what was flagged, but new work should
   reach for .btn-primary or .btn-view-primary, not this. */
.btn { border: none; border-radius: var(--radius); padding: 10px 24px; font-size: 14px; font-weight: 600; cursor: pointer; font-family: var(--font); transition: background 0.15s; display: inline-flex; align-items: center; gap: 6px; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-primary { background: var(--color-brand); color: #fff; }
.btn-primary:hover:not(:disabled) { background: var(--color-brand-dark); }
/* GAS's own WebApp_Common.html defines --color-blue/--color-blue-dark as
   #F47920/#D96B10 for .btn-secondary - identical values to --color-brand/
   --color-brand-dark (a leftover name from before that var was repurposed),
   not literally blue. Reusing the brand vars here renders the exact same
   color GAS does, without carrying a misleadingly-named "blue" token that
   isn't blue into this file. */
.btn-secondary { background: var(--color-brand); color: #fff; }
.btn-secondary:hover:not(:disabled) { background: var(--color-brand-dark); }
.btn-cancel { background: var(--surface-1); color: var(--color-neu); }
.btn-cancel:hover:not(:disabled) { background: var(--border); }
.btn-sm { padding: 6px 14px; font-size: 12px; }
.btn-warning { background: var(--color-neg); color: #fff; }
.btn-warning:hover:not(:disabled) { background: var(--color-neg-dark); }
/* Shared "Receipt" header-action button - real port of WebApp_Common.html's
   own .btn-receipt (Staff.html's customer header card, Form.html's status
   banner, Groups.html's whole card-action-button convention - every small
   in-card action button in that page uses this one class, not a bespoke
   one per action). Outlined brand colour at rest, fills solid on hover. */
.btn-receipt {
  padding: 5px 14px; border-radius: 6px; font-size: 12px; font-weight: 600;
  border: 1px solid var(--color-brand); color: var(--color-brand); background: #fff;
  cursor: pointer; font-family: var(--font);
}
.btn-receipt:hover:not(:disabled) { background: var(--color-brand); color: #fff; }
.btn-receipt:disabled { opacity: 0.4; cursor: default; background: #fff; color: var(--color-neu); border-color: var(--border); }

.form-input { width: 100%; padding: 8px 12px; border: 1px solid var(--border-dark); border-radius: var(--radius); font-size: 14px; color: var(--color-dark); font-family: var(--font); background: var(--panel-bg); }
.form-input:focus { outline: none; border-color: var(--color-brand); box-shadow: 0 0 0 2px rgba(244,121,32,0.15); }
.form-label { font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }

/* Generic confirm/prompt modal - real port of Staff.html's own rules.
   First real consumer beyond Event Types (v151.47, Environments screen's
   clone-all confirm) - genuinely shared, every future confirm dialog reuses
   this rather than a bespoke one per screen. */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 200; align-items: center; justify-content: center; padding: 24px 0; box-sizing: border-box; }
.modal-overlay.open { display: flex; }
/* v151.371 - real gap, direct report ("this is not fixed at all"): .modal-box
   never set its own default text color - any label/text inside a modal that
   didn't get its OWN explicit color (plenty didn't, this was never
   individually audited across every modal) fell back to the page's own
   default (--text-secondary, a dark slate meant for a light page), which is
   nearly invisible on the modal's own new dark background. One default
   here fixes every such untouched spot at once, app-wide, not just the
   ones found so far. */
/* v151.410 - real gap, direct report with a screenshot: a modal taller than
   the viewport (Add Event, once Start/End date + the full field set landed
   on it) got vertically centered by .modal-overlay with no way to reach
   its own top or bottom - no max-height, no overflow-y, so the title and
   the Save/Cancel row were both clipped off-screen on a real phone with no
   scroll possible. Every other .modal-box consumer happened to fit within
   a typical viewport so this never surfaced before. max-height caps it
   to the space .modal-overlay's own new vertical padding leaves; overflow-y
   lets a genuinely tall one scroll internally instead of clipping. */
.modal-box { background: var(--modal-bg); color: var(--modal-fg); border-radius: 12px; padding: 28px 28px 24px; width: 400px; max-width: 92vw; max-height: calc(100vh - 48px); overflow-y: auto; box-sizing: border-box; box-shadow: 0 8px 32px rgba(0,0,0,0.18); }
.modal-title { font-size: 16px; font-weight: 700; color: var(--modal-fg); margin-bottom: 8px; }
.modal-hint { font-size: 13px; color: var(--modal-muted); margin-bottom: 16px; }
/* v151.368 - a plain .form-input (light-page default, unchanged) inside a
   now-potentially-dark .modal-box needs its own scoped override - the base
   class is still correct on every real client page (login/forms/etc,
   .client-shell, none of which are ever dark).
   v151.414 - real gap widened, direct report with a screenshot: the
   Categories screen's reassign-to <select> (a plain .form-input, NOT
   inside any .modal-box - it sits directly in a dark .view-shell-card)
   showed the exact same light-on-dark break this rule was written to fix,
   just outside the one container this selector required. Broadened from
   ".modal-box .form-input" to "body:not(.client-shell) .form-input" - the
   real invariant was always "any staff page," never specifically "inside
   a modal," this just hadn't been exercised by a non-modal .form-input
   yet when the narrower selector was first written. */
body:not(.client-shell) .form-input, body:not(.client-shell) #profile-modal-overlay .form-input {
  background: var(--modal-input-bg); border-color: var(--modal-input-border); color: var(--modal-fg);
}
.modal-box .form-label, body:not(.client-shell) #profile-modal-overlay .form-label { color: var(--modal-muted); }

/* v151.412 - real gap, direct report with screenshots on both Submissions
   ("just light theme when dark" - readable but wrong) and Events ("text
   cannot read and still light" - actually illegible): the shared
   renderMultiSelect widget (common.js) reuses form-render.css's own MULTI
   field CSS classes verbatim, which are light-theme-only by design -
   correct for their original context (the client-facing form engine,
   .client-shell, never dark) but this widget is ONLY ever called from
   staff.html, which IS dark by default. .multi-panel's background stayed
   the form engine's own light var(--surface-1) while .multi-opt-label's
   text color fell through to the staff page's own dark-mode default
   (near-white/faint), producing pale-grey-on-pale-grey in Events and a
   readable-but-visually-wrong light box in Submissions (its option-label
   text happened to inherit a dark color there instead). Same
   body:not(.client-shell) guard the .form-input override above already
   uses - never touches the real client-facing form engine's own rendering. */
body:not(.client-shell) .multi-trigger { background: var(--modal-input-bg); border-color: var(--modal-input-border); }
body:not(.client-shell) .multi-panel { background: var(--modal-bg); border-color: var(--modal-border); box-shadow: 0 8px 24px rgba(0,0,0,0.3); }
body:not(.client-shell) .multi-panel-actions { border-bottom-color: var(--modal-border); }
body:not(.client-shell) .multi-opt-label { color: var(--modal-fg); }
body:not(.client-shell) .multi-placeholder,
body:not(.client-shell) .multi-arrow { color: var(--modal-muted); }
/* v151.374 - real correction, direct report: "profile modal does not
   respect theme. stays light on dark theme as well." The v151.373 fix
   above targeted .modal-box, but the Profile modal was never built on
   .modal-box at all - it's .rcpt-modal-overlay/.rcpt-modal-box (real port
   of ReceiptModal.html's shell, common.css line ~1002), the SAME shared
   component the Receipt modal on client pages also uses via the identical
   classes, plus a hardcoded inline background:#fff on .rcpt-modal-body in
   each page's own markup that no CSS rule can beat regardless of
   specificity (removed from staff.html's copy only). None of the v151.373
   .modal-box overrides below this comment ever applied to the real thing.
   Also a genuine cross-role risk, not just a wrong selector: this same
   shell (id #profile-modal-overlay) is shared verbatim across staff.html
   AND dashboard.html/groups.html (Client-facing) - unscoped var(--modal-*)
   would resolve to the DARK bare value on a Client page too, since Client
   pages never set data-theme at all (same reasoning as .client-shell
   below) - a real regression, not the "Clients default light" the user
   asked for. Every rule here is guarded with body:not(.client-shell) (an
   existing marker, dashboard.html/groups.html's own <body class>) so it
   can never affect the Receipt modal or a Client's Profile modal at all -
   .modal-box is kept alongside as a harmless no-op fallback in case any
   real .modal-box consumer ever reuses this same markup. */
.modal-box .digit-box, body:not(.client-shell) #profile-modal-overlay .digit-box { background: var(--modal-input-bg); border-color: var(--modal-input-border); color: var(--modal-fg); }
.modal-box .pin-group-label, body:not(.client-shell) #profile-modal-overlay .pin-group-label { color: var(--modal-muted); }
.modal-box .p-row, body:not(.client-shell) #profile-modal-overlay .p-row { border-bottom-color: var(--modal-border); }
.modal-box .p-label, body:not(.client-shell) #profile-modal-overlay .p-label { color: var(--modal-muted); }
.modal-box .p-val, body:not(.client-shell) #profile-modal-overlay .p-val { color: var(--modal-fg); }
.modal-box .p-val.muted, body:not(.client-shell) #profile-modal-overlay .p-val.muted { color: var(--modal-muted); }
.modal-box .otp-hint, body:not(.client-shell) #profile-modal-overlay .otp-hint { color: var(--modal-muted); }
.modal-box .p-danger-note, body:not(.client-shell) #profile-modal-overlay .p-danger-note { color: var(--modal-muted); }
.modal-box .btn-pen, body:not(.client-shell) #profile-modal-overlay .btn-pen { color: var(--modal-muted); }
.modal-box .p-section-hdr, body:not(.client-shell) #profile-modal-overlay .p-section-hdr { background: var(--modal-surface); color: var(--modal-fg); border-bottom-color: var(--modal-border); }
.modal-box .p-section-danger .p-section-hdr, body:not(.client-shell) #profile-modal-overlay .p-section-danger .p-section-hdr { color: var(--color-neg); }
:root:not([data-theme="light"]) .modal-box .btn-pen.cancel,
body:not(.client-shell) #profile-modal-overlay .btn-pen.cancel { color: rgba(255,120,100,0.95); }
:root:not([data-theme="light"]) .modal-box .p-section-danger .p-section-hdr,
body:not(.client-shell) #profile-modal-overlay .p-section-danger .p-section-hdr { color: rgba(255,120,100,0.95); }
.modal-box .btn-cancel, body:not(.client-shell) #profile-modal-overlay .btn-cancel { background: var(--modal-surface); color: var(--modal-muted); }
.modal-box .btn-cancel:hover:not(:disabled), body:not(.client-shell) #profile-modal-overlay .btn-cancel:hover:not(:disabled) { background: var(--modal-surface-hover); }
body:not(.client-shell) #profile-modal-overlay .rcpt-modal-box { background: var(--modal-bg); }
body:not(.client-shell) #profile-modal-overlay .rcpt-modal-body { background: var(--modal-bg); }
body:not(.client-shell) #profile-modal-overlay .rcpt-modal-hdr { border-bottom-color: var(--modal-border); }
body:not(.client-shell) #profile-modal-overlay .rcpt-modal-title { color: var(--modal-fg); }
body:not(.client-shell) #profile-modal-overlay .rcpt-modal-close { color: var(--modal-muted); }

/* Modal form fields (real port of Staff.html's own .acct-form block) -
   first real consumer is the Documents template modal (v151.64). v151.67 -
   the remaining sub-patterns (.row/.phone-row/.check-row/.hint-test/
   .modal-err) added now that Add/Edit Staff is the first real consumer that
   needs them - deferred at v151.64, not spec'd ahead of time.
   v151.371 - real gap, direct report: "are you not seeing the stark white
   text boxes and dropdowns on dark theme... same issue in all dark modals."
   This whole block hardcoded a fixed light palette (assumed .modal-box was
   always white, true until v151.368) - every input/select had no
   background at all (so it fell back to the browser's own stark-white
   default sitting directly against the new dark modal), labels/check-row
   text used fixed non-modal-aware colors. Every consumer of .acct-form
   (Add/Edit Staff, Add/Edit Client, and others) gets this fix for free. */
.acct-form label {
  display: block; font-size: 11px; font-weight: 600; color: var(--modal-muted);
  margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.3px;
}
.acct-form input, .acct-form select {
  width: 100%; box-sizing: border-box; padding: 7px 10px;
  border: 1px solid var(--modal-input-border); border-radius: 5px; font-size: 13px;
  font-family: var(--font); color: var(--modal-fg); background: var(--modal-input-bg); margin-bottom: 10px;
}
.acct-form input:focus, .acct-form select:focus { outline: none; border-color: var(--color-brand); }
.acct-form .row { display: flex; gap: 10px; }
.acct-form .row > div { flex: 1; }
/* v151.413 - real gap, direct instruction: "on a mobile, seeing all fields
   vertically stacked is expected. no such need on a desktop with enough
   real estate." This rule never had a mobile stack-back-to-column
   override at all - every existing .acct-form consumer (Add/Edit Client,
   Add/Edit Staff) would have stayed side-by-side and cramped on a real
   phone too, just never reported. Same 768px breakpoint the rest of the
   app already keys off. */
@media (max-width: 768px) {
  .acct-form .row { flex-direction: column; gap: 0; }
}
.acct-form .phone-row { display: flex; gap: 8px; margin-bottom: 10px; }
.acct-form .phone-row .cc-sel { flex: 2; margin-bottom: 0; }
.acct-form .phone-row .cc { width: 70px; flex-shrink: 0; margin-bottom: 0; background: var(--modal-surface); }
.acct-form .phone-row .ph { flex: 3; margin-bottom: 0; }
.acct-form .check-row {
  display: flex; align-items: center; gap: 8px; margin-bottom: 10px;
  color: var(--modal-fg); font-weight: 500; font-size: 13px;
}
.acct-form .check-row input { width: auto; margin: 0; }
/* Event Logs View modal - direct report with a screenshot: "everything
   looks the same and not clean to look at" - the date picker, the
   selected entry, and the gap chips ran together as one flat column with
   no boundary telling the eye where one ends and the next begins. Each
   becomes its own bordered zone with a small caps label, reusing the
   same modal tokens as everything else here - no new colors, just added
   structure. Generic (not Events-specific) so any future modal that
   wants the same "grouped sub-sections" treatment can reuse it. */
.vlm-zone {
  border: 1px solid var(--modal-border); border-radius: 7px;
  padding: 10px 12px; margin-bottom: 10px; background: var(--modal-input-bg);
}
.vlm-zone:last-of-type { margin-bottom: 0; }
.vlm-zone-label {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  font-size: 10.5px; font-weight: 700; color: var(--color-brand);
  text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 8px;
}
.vlm-zone-label-text { display: flex; align-items: center; gap: 7px; }
.vlm-zone-label-text::before {
  content: ''; width: 3px; height: 11px; border-radius: 2px;
  background: var(--color-brand); display: inline-block;
}
.vlm-zone-count { font-size: 11px; font-weight: 500; color: var(--modal-muted); text-transform: none; letter-spacing: normal; }
.vlm-zone select { margin-bottom: 0; }
.vlm-zone-arrow { cursor: pointer; }
/* Show/Hide Gaps used .btn-cat-icon before - the exact same outline
   style as the gap-day chips it sits directly above, which is why a
   direct report said "i did not even see the hide gaps button": a
   button that looks identical to its own children doesn't read as a
   control. Filled .btn-primary (the app's locked default action style)
   reads as a real button against the outline chips below it; scoped
   smaller here since the full default .btn padding is sized for a form
   submit action, not an inline toggle. */
.vlm-gaps-toggle { padding: 5px 14px; font-size: 12px; margin-bottom: 8px; }
/* Native <option> popup always renders on the OS/browser's own light
   background regardless of app theme - same precedent as .fb-input's own
   identical fix (common.css). v151.414 - widened alongside the
   .form-input background/border override above (same reasoning - a
   .form-input select outside .acct-form, e.g. Categories' reassign
   picker, needs this too, not just an .acct-form consumer). */
select.acct-form option, .acct-form select option,
body:not(.client-shell) .form-input option { color: #1e293b; background: #fff; }
.acct-form .hint-test {
  font-size: 11px; color: #0369a1; background: #e0f2fe;
  padding: 6px 10px; border-radius: 4px; margin-bottom: 12px;
}
/* v151.215 - shared boxed-warning treatment, same pill shape as .hint-test
   above but in the app's own --color-neg red instead of that class's fixed
   info-blue, and NOT scoped to .acct-form - used on both login.html (the
   client OTP-lockout screen) and staff.html (the staff MPIN-lockout
   message), neither of which sits inside an .acct-form wrapper. Replaces
   what was previously bare colored text with no box at all. */
.hint-danger {
  font-size: 13px; color: var(--color-neg); background: rgba(217,48,37,0.06);
  border: 1px solid rgba(217,48,37,0.18); border-radius: 8px;
  padding: 10px 12px; line-height: 1.5;
}
.text-danger { color: var(--color-neg); }

/* v151.218 - Silent Mode's mandatory choice gate (Add/Edit Client modal,
   staff.html) - same boxed-warning shape as .hint-danger above but in the
   app's own warn palette (orange, not red - this isn't an error state),
   plus a new card-radio component (no equivalent existed anywhere in the
   app before this). */
/* v151.222 real bug, caught live from a screenshot: the mockup's own
   .gf-banner used display:flex to lay its icon div and text div out
   side-by-side, but this banner has no icon - it's set via innerHTML as a
   single flowing string ("<b>Silent Mode is active.</b> Choose how...").
   With flex still on, the browser treats the <b> and the surrounding bare
   text nodes as separate anonymous flex items, splitting "Silent Mode is
   active." into its own narrow column instead of flowing as one paragraph
   with the rest of the sentence. Plain block flow instead - no icon here,
   nothing to distribute side-by-side. */
.gate-banner {
  background: var(--color-warn-bg); border: 1px solid var(--color-warn-border);
  border-radius: 8px; padding: 12px 14px; margin-bottom: 16px;
  font-size: 13px; color: var(--color-warn-text); line-height: 1.5;
}
/* v151.218 - single-line variant for spots that need a heads-up but not a
   full multi-line box (Client Forms Modal) - same palette, no icon column,
   tighter padding/font so it doesn't crowd the form list underneath. */
.gate-banner-compact { padding: 7px 12px; font-size: 12px; margin-bottom: 10px; }
.gate-title { font-size: 13px; font-weight: 700; color: var(--color-dark); margin: 0 0 4px; }
.gate-sub { font-size: 12px; color: var(--color-neu); margin: 0 0 14px; line-height: 1.5; }
.gate-choice-group { display: flex; flex-direction: column; gap: 10px; margin-bottom: 18px; }
.gate-choice-option {
  display: flex; gap: 10px; align-items: flex-start; border: 1.5px solid var(--border-dark);
  border-radius: 8px; padding: 12px 14px; cursor: pointer; transition: all 0.12s;
}
/* v151.219/220 - real bug, caught live (twice): this is itself a <label>
   (for the radio it wraps), so .acct-form label's uppercase/letter-spacing
   rules (meant for plain form field labels like "FIRST NAME") were
   cascading onto .gate-opt-title/.gate-opt-desc's text, neither of which
   sets its own text-transform. First fix attempt (plain .gate-choice-option
   rule, specificity 0,1,0) still LOST to .acct-form label (0,1,1 - a class
   plus a type selector beats a single class) - looked fixed in a live
   !important-injected preview, which masked the real specificity gap;
   only caught on the real deployed CSS after redeploying. This scoped
   selector matches .acct-form label's own specificity shape (two classes,
   0,2,0, beats 0,1,1) instead of relying on !important. */
.acct-form .gate-choice-option { text-transform: none; letter-spacing: normal; }
.gate-choice-option:hover { border-color: var(--color-brand); }
.gate-choice-option.selected { border-color: var(--color-brand); background: rgba(244,121,32,0.06); }
/* v151.250 - real bug, same class as the text-transform fix above but on
   the radio itself: .acct-form input's width:100%/padding/border/
   margin-bottom (meant for text/select fields) was never overridden here,
   just added to - the radio stretched to fill the row instead of sitting
   beside the text, pushing gate-opt-title/gate-opt-desc onto the line
   below it instead of to its right. Shared by Add and Edit (both Client and
   Group Member) - one shared #acl-silent-gate block, not separate copies. */
.gate-choice-option input {
  margin-top: 2px; margin-bottom: 0; accent-color: var(--color-brand); flex-shrink: 0;
  width: auto; padding: 0; border: none; border-radius: 0;
}
.gate-opt-title { font-size: 13px; font-weight: 700; color: var(--color-dark); margin-bottom: 2px; }
.gate-opt-desc { font-size: 11.5px; color: var(--color-neu); line-height: 1.45; }
.gate-actions { display: flex; justify-content: flex-end; gap: 10px; }
.gate-err { font-size: 12px; color: var(--color-neg); margin-bottom: 10px; }
/* v151.373 - real gap, direct report ("check all the error texts in dark
   theme (modals). that got missed. same fore color as the light theme"):
   .modal-err/.acct-status-ok/.acct-status-err are all modal-exclusive
   (confirmed via grep - never used outside a .modal-box/.acct-form
   consumer), so unlike --color-neg (used app-wide, including always-light
   client pages, and left untouched) these get their own light/dark split -
   the light values are the ORIGINAL literals unchanged (still correct for
   the light-theme modal), the dark values are real light-on-dark
   equivalents, not just an ink recolor of a semantic color. */
.modal-err { font-size: 12px; color: #dc2626; margin-top: 6px; display: none; }
:root:not([data-theme="light"]) .modal-err { color: rgba(255,120,100,0.95); }
body.client-shell .modal-err { color: #dc2626; } /* client pages are always light */
.acct-status-ok  { color: #166534; font-size: 12px; margin-top: 8px; }
.acct-status-err { color: #9f1239; font-size: 12px; margin-top: 8px; }
:root:not([data-theme="light"]) .acct-status-ok  { color: #4ade80; }
:root:not([data-theme="light"]) .acct-status-err { color: rgba(255,120,100,0.95); }
.modal-btns { display: flex; gap: 10px; margin-top: 18px; justify-content: flex-end; }
/* v151.82 - Move to Group's picker box (add-client-modal), real port of
   Staff.html's own .acl-move-box-active - a bordered container that only
   appears while the primary-client/relationship picker is actually open. */
.acl-move-box-active { border: 1px solid var(--border-dark); border-radius: 8px; padding: 10px; margin-top: 8px; }

/* v151.85 - status badges, real port of WebApp_Common.html's own block -
   first client-facing consumer is the Dashboard's forms overview
   (WA.STATUS_BADGE). Standardized on title case per that file's own v149.1
   comment (was 3 copies before consolidation there, Staff's own sentence-
   case version lost). */
.badge { display: inline-flex; align-items: center; justify-content: center; gap: 4px; padding: 2px 8px; min-width: 110px; border-radius: var(--radius-sm); font-size: 11px; font-weight: 600; text-align: center; }
.badge-ok { background: #DCFCE7; color: #166534; }
.badge-fail { background: #FEE2E2; color: #9F1239; }
.badge-warn { background: var(--color-warn-bg); color: var(--color-warn-text); }
.badge-na { background: var(--surface-1); color: var(--color-muted); }
.badge-draft { background: #DBEAFE; color: #1E40AF; }
.badge-returned { background: #F3E8FF; color: #7C3AED; }

/* v151.85 - small generic utilities, real port of WebApp_Common.html's own
   block - first client-facing consumer is the Dashboard. */
.mb-16 { margin-bottom: 16px; }
.text-muted { color: var(--color-muted); }
/* v151.166 - real bug, caught live: a conditional comment/notes textarea
   sat flush against the field above it with zero gap ("stuck to the
   object's bottom"). Root cause: form-render.js references class="mt-8"
   on every one of these textareas (10+ call sites), but the class was
   never actually defined anywhere in this stack - WebApp_Common.html's
   own .mt-8 (margin-top:8px) was never ported over when the shared
   utilities above were. Not a missing feature, a straight omission. */
.mt-8 { margin-top: 8px; }

/* v151.160 - the v151.85 comment above claimed "a staff session never
   reaches this, staff.css has its own separate drawer" - false; staff.css
   only overrides .drawer-group-label/.drawer-btn-child, never the base
   .drawer/.drawer-btn/.drawer-identity* sizing, so it fell through to
   whichever of the two same-specificity blocks in this file happened to
   load last. This whole block was that: a second, unscaled verbatim port
   of AppNav.html's original 500px/25-28px sizing, added after the v151.42
   block above had already fixed exactly this ("unusable on a real phone
   screenshot" - see that block's own comment) - silently reintroducing the
   bug it fixed, for both client pages (Groups) and staff, confirmed live
   via a real screenshot showing an oversized drawer/hamburger on both.
   Removed outright - the v151.42 block above is the only .drawer* ruleset
   now, real single source of truth instead of a last-write-wins accident. */
.btn-modal-cancel { background: var(--modal-surface); color: var(--modal-muted); border: none; border-radius: 6px; padding: 8px 18px; font-size: 13px; cursor: pointer; font-family: var(--font); }
.btn-modal-cancel:hover { background: var(--modal-surface-hover); }
.btn-modal-confirm { border: 1px solid var(--color-brand); color: var(--color-brand); background: transparent; border-radius: 6px; padding: 8px 18px; font-size: 13px; font-weight: 600; cursor: pointer; font-family: var(--font); }
.btn-modal-confirm:hover:not(:disabled) { background: var(--color-brand); color: #fff; }
/* v151.402 - cursor:not-allowed, was cursor:default - real gap in the
   double-arm construct itself, found live: armGate's own brief post-arm
   disable window (WA.armGate, ~900ms) gives no visual cue at all on any
   button using this class - Restore Account, Document Template Delete,
   and every other confirmModal(opts.armed)/armGate consumer sharing it -
   the button just looked identical, arrow cursor and all, during the
   exact moment it's deliberately locked. Matches .btn:disabled's own
   already-correct cursor above; this class just never got the same rule. */
.btn-modal-confirm:disabled { background: var(--modal-surface); color: var(--modal-muted); border-color: var(--modal-muted); cursor: not-allowed; }

/* Small dark-panel "clone/reset" action button - real port of FormBuilder's
   .fb-deact-btn, reused here for Environments' per-table "Clone from Prod".
   v151.362 - Environments is the first screen using this class to get a
   light-theme pass; the ambient panel wash converts straight to --ink, but
   the danger-red text/border is a semantic pastel tuned for a dark
   background (same reasoning as staff-environments.js's own
   _envStatusColor_) so it gets a real distinct light-theme value below,
   not an --ink recolor - FormBuilder/SysVars/etc's own other consumers of
   this class inherit the fix for free, still dark-only everywhere else on
   those screens until their own Phase 3 pass. */
.fb-deact-btn { padding: 5px 12px; font-size: 12px; font-weight: 600; background: rgba(var(--ink),0.06); color: rgba(255,130,110,0.9); border: 1px solid rgba(255,100,100,0.22); border-radius: 6px; cursor: pointer; font-family: var(--font); }
.fb-deact-btn:hover { background: rgba(255,100,100,0.12); }
:root[data-theme="light"] .fb-deact-btn { color: #b91c1c; border-color: rgba(185,28,28,0.3); }
:root[data-theme="light"] .fb-deact-btn:hover { background: rgba(185,28,28,0.08); }

.status { font-size: 13px; min-height: 18px; margin-top: 12px; text-align: center; }
.status.error { color: var(--color-neg); }
.status.success { color: var(--color-pos); }

@keyframes spin { to { transform: rotate(360deg); } }
/* v151.82 - real port of WebApp_Common.html's own .spinner-inline, small
   enough to sit inside a button's own text (Promote/Move to Group's
   in-flight state) - a third size alongside spinner-md/lg, not a
   replacement for either. */
.spinner-inline { display: inline-block; width: 16px; height: 16px; border: 2px solid var(--border); border-top-color: var(--color-brand); border-radius: 50%; animation: spin 0.8s linear infinite; vertical-align: middle; margin-right: 8px; flex-shrink: 0; }
.spinner-md { width: 32px; height: 32px; border: 3px solid var(--border); border-top-color: var(--color-brand); border-radius: 50%; animation: spin 0.8s linear infinite; }
.spinner-lg { width: 40px; height: 40px; border: 4px solid var(--border); border-top-color: var(--color-dark); border-radius: 50%; animation: spin 0.8s linear infinite; }
.spinner-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(248,247,243,0.92); z-index: 9999; align-items: center; justify-content: center; flex-direction: column; gap: 16px; }
.spinner-overlay.active { display: flex; }
.spinner-overlay-msg { font-size: 13px; color: var(--color-neu); }

.hidden { display: none !important; }

/* armGate's armed-state colors - common.js's WA.armGate applies these. */
.wa-armed-confirm     { background: var(--color-pos) !important; border-color: var(--color-pos) !important; color: #fff !important; }
.wa-armed-destructive { background: var(--color-neg) !important; border-color: var(--color-neg) !important; color: #fff !important; }

/* Shared Active/Inactive-style toggle switch - real port of
   WebApp_Common.html's own .toggle-switch component. Track color + thumb
   position + label text all flip together. First real consumer here is
   Clients' bulk expand/collapse-all control (v151.59); the same component
   also covers a plain settings toggle wherever GAS used it (Groups.html's
   Edit member modal, Add/Edit Client/Staff modals) once those are ported. */
.toggle-switch { display: inline-flex !important; align-items: center; gap: 12px; cursor: pointer; user-select: none; }
.toggle-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.toggle-prefix { font-size: 12px; font-weight: 600; color: var(--color-neu); white-space: nowrap; flex-shrink: 0; }
.toggle-track {
  position: relative; width: 36px; height: 20px; border-radius: 999px;
  background: var(--border); transition: background 0.15s; flex-shrink: 0;
}
.toggle-thumb {
  position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
  border-radius: 50%; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.3);
  transition: transform 0.15s;
}
.toggle-switch input:checked + .toggle-track { background: var(--color-pos); }
.toggle-switch input:checked + .toggle-track .toggle-thumb { transform: translateX(16px); }
.toggle-switch input:disabled + .toggle-track { opacity: 0.45; }
/* v151.166/168 - two attempts at a dark-context override for the drawer
   (var(--border) alone, then a semi-transparent white) both turned out
   unnecessary and were reverted here - direct comparison against the
   topbar's own toggle (same base rule, no override, sitting on the exact
   same dark background tone as the drawer) showed the plain base
   var(--border) already reads clearly there. Removed the override outright
   so both instances render identically off the one shared rule, rather
   than chasing a second color that was never actually the problem. */
.toggle-switch:has(input:disabled) { cursor: default; }
.toggle-label { font-size: 12px; font-weight: 600; text-transform: none; letter-spacing: normal; }
.toggle-label-on  { display: none; color: var(--color-pos); }
.toggle-label-off { display: inline; color: var(--color-muted); }
.toggle-switch input:checked ~ .toggle-label-on  { display: inline; }
.toggle-switch input:checked ~ .toggle-label-off { display: none; }
/* v151.366 - real bug, direct follow-up question ("was the header also
   fixed?") that turned out to have a real answer, not just "yes": this
   shared component's own .toggle-prefix/.toggle-label-off use fixed
   --color-neu/--color-muted (slate grays, same in both themes, tuned for
   sitting on a light modal/card - the toggle's usual home) - the Compact
   View toggle instances (#compact-toggle-topbar/#compact-toggle-drawer)
   sit directly on the topbar/drawer chrome instead, which flips between
   dark and light. Measured, not assumed: --color-muted (.toggle-label-off,
   "Off") is 5.45:1 on the dark chrome but only 2.56:1 on light - a real
   failure the theme work introduced. --color-neu (.toggle-prefix,
   "Compact") was ALREADY only 2.93:1 on the original dark chrome, before
   any of this session's work - a pre-existing bug, not a regression, fixed
   here too since it's the same two lines. Scoped to just the chrome
   contexts (topbar/drawer) via the already-dual-themed --chrome-fg-*
   scale - every other .toggle-switch consumer (Profile modal, Add/Edit
   Client modals, etc, all genuinely light-card contexts) keeps the
   original --color-neu/--color-muted untouched. */
.topbar .toggle-prefix, .drawer .toggle-prefix { color: var(--chrome-fg-60); }
.topbar .toggle-label-off, .drawer .toggle-label-off { color: var(--chrome-fg-45); }

/* Small dark-panel form input/button primitives - real port of
   FormBuilder.html's own .fb-input/.fb-save-btn/.fb-tab-btn (name kept
   despite the "fb-" prefix, for fidelity with GAS's own class names - these
   are reused well beyond Forms Builder there too, e.g. System Variables/
   Event Types/Status Config/Roles all lean on the same three classes). */
.fb-tab-btn {
  padding: 4px 12px; font-size: 12px; font-weight: 600;
  border-radius: 5px; border: 1px solid rgba(var(--ink),0.15);
  color: rgba(var(--ink),0.6); background: none; cursor: pointer;
  font-family: var(--font); transition: all 0.12s;
}
.fb-tab-btn:hover { background: rgba(var(--ink),0.08); color: var(--ink-solid); }
/* v151.195 - real gap: GAS's own FormBuilder.html also has this rule for
   the same class, never ported alongside the rest of .fb-tab-btn - used by
   the Edit/Preview tabs and the right panel's View/Edit toggle. */
.fb-tab-btn.active { background: var(--color-brand); border-color: var(--color-brand); color: #fff; }
.fb-input {
  width: 100%; box-sizing: border-box;
  background: rgba(var(--ink),0.07); border: 1px solid rgba(var(--ink),0.12);
  border-radius: 6px; padding: 6px 10px; color: var(--ink-solid); font-size: 13px;
  font-family: var(--font); outline: none;
}
.fb-input:focus { border-color: var(--color-brand); }
/* v151.45 - real repeat bug (same class the user had fixed on GAS "many
   times"): a <select>'s own OPTION POPUP renders on the browser/OS's native
   light background regardless of the closed control's dark styling above -
   .fb-input's inherited color:#fff then reads as white-on-white in the open
   dropdown until an option is hovered. Scoped to select specifically (an
   <option> element only exists inside a <select>, but being explicit here
   matches the same precedent already set for .acct-search in Staff.html). */
select.fb-input option { color: #1e293b; background: #fff; }
.fb-save-btn {
  padding: 5px 14px; font-size: 12px; font-weight: 700;
  background: var(--color-brand); color: #fff; border: none;
  border-radius: 6px; cursor: pointer; font-family: var(--font);
}
.fb-save-btn:hover:not(:disabled) { opacity: 0.88; }
/* v151.54 real gap, confirmed via grep - no disabled state existed at all,
   so a genuinely-disabled .fb-save-btn (Environments' "Already Live") was
   pixel-identical to a normal clickable one - same color, same cursor,
   nothing signaled "this does nothing right now." */
.fb-save-btn:disabled { background: rgba(var(--ink),0.12); color: rgba(var(--ink),0.4); cursor: not-allowed; }

/* fb-form-* - "labeled input in a form row" primitives, used by every
   SysAdmin-style "Add X" row (Event Types today; System Variables/Status
   Config/Roles/Data Sync will reuse the same shape). Pulled out of inline
   styles specifically so the mobile media query below can actually target
   them - a stylesheet rule can never override an inline style="" without
   resorting to !important everywhere, so anything that needs to change
   size on a narrow viewport has to be a class, not inline. */
.fb-form-field { }
.fb-form-label { display: block; font-size: 11px; color: rgba(255,255,255,0.65); margin-bottom: 4px; }
.fb-form-status { font-size: 11px; min-height: 14px; margin-top: 6px; }

/* JS-appended by common.js's initFooter() on every page - orange brand bar,
   pinned to the viewport bottom via margin-top:auto on a flex-column body. */
.page-footer { background: var(--color-brand); color: #fff; text-align: center; padding: 7px 16px; font-size: 12px; letter-spacing: 0.3px; align-self: stretch; margin-top: auto; }

/* ── QUERIES PANE (v151.110) ──
   qm- prefix ("Queries, Mailbox-shaped"). Client-facing Queries redrawn in
   the same folder-nav + list + detail idiom Staff's own Mailbox uses
   (.mb-* in staff.css) - genuinely the same shape, but a separate class
   set rather than sharing .mb-* directly: this pane lives inside a fixed-
   height card on an otherwise light-only client page (no theme toggle, see
   design conversation), where .mb-* carries Mailbox's own dark-by-default
   theming baked into every rule. Lives in common.css, not dashboard.css/
   groups.css - queries-pane.js is already shared between both pages, same
   reasoning as every other rule in this file. */
/* 2026-09-21 (user: "increase the height. there is enough real estate") -
   fills the page down to the footer (measured: frame top ~84px, footer
   32px, content padding 28px -> 100vh - 144px), never shorter than the old
   fixed 560px. The mobile rule further down keeps its own 78vh. */
.qm-frame {
  background: var(--panel-bg); border: 1px solid var(--border); border-radius: var(--radius-lg);
  box-shadow: var(--shadow); overflow: hidden; height: calc(100vh - 144px); min-height: 560px; max-width: 960px;
  display: flex; flex-direction: column;
}
.qm-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 20px; border-bottom: 1px solid var(--border); flex-shrink: 0; gap: 10px;
}
.qm-toolbar-title { font-size: 15px; font-weight: 700; display: flex; align-items: center; gap: 8px; }
.qm-mobile-hamburger, .qm-mobile-back {
  display: none; background: none; border: none; cursor: pointer; font-size: 18px; color: var(--color-neu); padding: 2px 4px;
}
.qm-body { display: flex; flex: 1; min-height: 0; }

.qm-folders { width: 140px; flex-shrink: 0; border-right: 1px solid var(--border); padding: 10px 0; }
.qm-folder {
  padding: 9px 18px; font-size: 13px; cursor: pointer; display: flex; align-items: center;
  justify-content: space-between; gap: 8px; color: var(--color-dark);
}
.qm-folder:hover { background: var(--surface-1); }
.qm-folder.qm-folder-active { background: var(--color-brand); color: #fff; }
.qm-folder-count { font-size: 11px; color: var(--color-muted); }
.qm-folder.qm-folder-active .qm-folder-count { color: rgba(255,255,255,0.8); }

.qm-list-col { width: 270px; flex-shrink: 0; border-right: 1px solid var(--border); display: flex; flex-direction: column; }
.qm-search { padding: 10px 12px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
.qm-search input {
  width: 100%; padding: 7px 10px; border: 1px solid var(--border-dark); border-radius: var(--radius);
  font-size: 12.5px; font-family: var(--font); background: var(--panel-bg); color: var(--color-dark);
}
.qm-search input:focus { outline: none; border-color: var(--color-brand); }
.qm-list { flex: 1; overflow-y: auto; }
.qm-list-empty { padding: 28px 16px; text-align: center; font-size: 12.5px; color: var(--color-muted); font-style: italic; }
.qm-row { padding: 11px 14px; border-bottom: 1px solid var(--border); cursor: pointer; display: flex; gap: 9px; }
.qm-row:hover { background: var(--surface-1); }
.qm-row.qm-row-selected { background: #FFF1E6; }
.qm-row-dot { width: 7px; height: 7px; border-radius: 50%; margin-top: 6px; flex-shrink: 0; }
.qm-row-dot.qm-dot-open { background: var(--color-brand); }
.qm-row-dot.qm-dot-done { background: var(--color-pos); }
.qm-row-main { min-width: 0; flex: 1; }
.qm-row-top { display: flex; justify-content: space-between; gap: 6px; font-size: 12px; }
.qm-row-subj { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qm-row-time { color: var(--color-muted); flex-shrink: 0; }
.qm-row-snip { font-size: 12px; color: var(--color-neu); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.qm-detail { flex: 1; min-width: 0; display: flex; flex-direction: column; background: var(--surface-1); }
.qm-detail-empty { flex: 1; display: flex; align-items: center; justify-content: center; color: var(--color-muted); font-size: 13px; }
.qm-detail-hdr { padding: 13px 20px; border-bottom: 1px solid var(--border); background: var(--panel-bg); flex-shrink: 0; display: flex; align-items: center; gap: 8px; }
.qm-detail-hdr-main { min-width: 0; }
.qm-detail-subj { font-size: 14px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qm-detail-status { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; margin-top: 3px; }
.qm-detail-status.qm-status-open { color: var(--color-brand); }
.qm-detail-status.qm-status-done { color: var(--color-pos); }

.qm-chat { flex: 1; overflow-y: auto; padding: 16px 20px; display: flex; flex-direction: column; gap: 12px; }
.qm-bubble-row { display: flex; flex-direction: column; max-width: 78%; }
.qm-bubble-row.qm-me { align-self: flex-end; align-items: flex-end; }
.qm-bubble-row.qm-staff { align-self: flex-start; align-items: flex-start; }
.qm-bubble { padding: 9px 13px; border-radius: 14px; font-size: 13px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
.qm-bubble-row.qm-me .qm-bubble { background: var(--color-brand); color: #fff; border-bottom-right-radius: 4px; }
.qm-bubble-row.qm-staff .qm-bubble { background: var(--panel-bg); color: var(--color-dark); border: 1px solid var(--border); border-bottom-left-radius: 4px; }
.qm-bubble-meta { font-size: 10.5px; color: var(--color-muted); margin-top: 3px; padding: 0 3px; display: flex; align-items: center; gap: 6px; }
.qm-bubble-row.qm-me .qm-bubble-meta { flex-direction: row-reverse; }
.qm-bubble-meta b { color: var(--color-neu); font-weight: 600; }
/* Edit/delete pencil+trash (v151.113) - only ever rendered on the single
   eligible bubble (own last, unanswered message), inline in its meta row
   rather than a hover overlay - hover doesn't work on mobile, and this
   whole app is tap-first everywhere else already. */
.qm-msg-icon-btn {
  background: none; border: none; padding: 2px; cursor: pointer; color: var(--color-muted);
  display: inline-flex; align-items: center; justify-content: center; line-height: 1;
}
.qm-msg-icon-btn svg { width: 12px; height: 12px; }
.qm-msg-icon-btn:hover { color: var(--color-brand); }
.qm-msg-icon-btn:disabled { opacity: 0.5; cursor: default; }

/* v151.116 - .qm-composer is now a flex COLUMN (pending-attachment chips
   stack above the input row), .qm-composer-row is what used to be
   .qm-composer's own flex-row behavior - split out so the chip row has
   somewhere to sit without fighting the row's own align-items:flex-end. */
.qm-composer { border-top: 1px solid var(--border); background: var(--panel-bg); padding: 10px 14px; flex-shrink: 0; display: flex; flex-direction: column; gap: 6px; }
.qm-composer-row { display: flex; gap: 8px; align-items: flex-end; }
/* 2026-09-21 - one line used to overflow its own box by a hair (36px
   border-box minus 18px padding and 2px border left 16px for an 18.2px
   line), so an empty or one-line reply showed scroll arrows. One line now
   fits exactly (8px padding, 18px line), and the scrollbar only appears
   once the auto-grow (queries-pane.js _qOnReplyInput_) reaches its 110px
   cap. */
.qm-composer textarea {
  flex: 1; resize: none; border: 1px solid var(--border-dark); border-radius: 18px; padding: 8px 15px;
  font-size: 13px; font-family: var(--font); color: var(--color-dark); background: var(--surface-1);
  height: 36px; max-height: 110px; line-height: 18px; overflow-y: hidden; transition: border-color 0.15s;
}
.qm-composer textarea:focus { outline: none; border-color: var(--color-brand); background: var(--panel-bg); }
.qm-attach-btn, .qm-send-btn {
  width: 36px; height: 36px; border-radius: 50%; border: none; background: var(--border);
  color: var(--color-muted); cursor: pointer; flex-shrink: 0; display: flex; align-items: center; justify-content: center;
  transition: background 0.15s, color 0.15s; padding: 0;
}
.qm-attach-btn:hover { background: var(--border-dark); color: var(--color-dark); }
.qm-attach-btn svg { width: 16px; height: 16px; }
.qm-send-btn svg { width: 16px; height: 16px; transform: translateX(-1px); }
.qm-send-btn.qm-send-ready { background: var(--color-brand); color: #fff; }
.qm-send-btn.qm-send-ready:hover { background: var(--color-brand-dark); }

/* Attachment chips - inside a bubble (sent), reused for the composer's own
   pending/not-yet-sent list too, distinguished only by the remove button
   the pending variant adds. */
.qm-bubble-attach { margin-top: 6px; display: flex; flex-direction: column; gap: 4px; }
.qm-attach-chip {
  display: flex; align-items: center; gap: 6px; font-size: 11.5px; padding: 5px 9px;
  border-radius: 8px; cursor: pointer; max-width: 220px;
}
.qm-attach-chip svg { width: 13px; height: 13px; flex-shrink: 0; }
.qm-attach-chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qm-bubble-row.qm-me .qm-attach-chip { background: rgba(255,255,255,0.2); color: #fff; }
.qm-bubble-row.qm-staff .qm-attach-chip { background: var(--surface-1); color: var(--color-dark); border: 1px solid var(--border); }
.qm-attach-chip:hover { opacity: 0.85; }

.qm-pending-attach { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 2px; }
.qm-pending-chip {
  display: flex; align-items: center; gap: 6px; font-size: 11.5px; padding: 5px 8px 5px 10px;
  border-radius: 14px; background: var(--surface-1); border: 1px solid var(--border); color: var(--color-dark);
  max-width: 200px;
}
.qm-pending-chip button { background: none; border: none; cursor: pointer; color: var(--color-muted); font-size: 14px; line-height: 1; padding: 0; flex-shrink: 0; }
.qm-pending-chip button:hover { color: var(--color-neg); }
.qm-send-btn:disabled { opacity: 0.6; cursor: default; }
/* Cancel-edit "x" (v151.113) - only rendered while the composer holds a
   loaded-for-editing message, sits between the textarea and the send
   button so backing out never means clearing the text by hand first. */
.qm-cancel-edit-btn {
  width: 28px; height: 36px; border: none; background: none; color: var(--color-muted);
  cursor: pointer; font-size: 18px; line-height: 1; flex-shrink: 0;
}
.qm-cancel-edit-btn:hover { color: var(--color-neg); }

/* New Query compose pane - replaces qm-list-col + qm-detail in place */
.qm-compose-pane { flex: 1; display: flex; flex-direction: column; }
.qm-compose-hdr { padding: 13px 20px; border-bottom: 1px solid var(--border); font-size: 14px; font-weight: 700; display: flex; align-items: center; gap: 10px; }
.qm-compose-body { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 14px; max-width: 480px; }
.qm-field label { font-size: 11px; font-weight: 700; color: var(--color-neu); text-transform: uppercase; letter-spacing: 0.04em; display: block; margin-bottom: 5px; }
.qm-field input, .qm-field textarea {
  width: 100%; padding: 9px 12px; border: 1px solid var(--border-dark); border-radius: 7px;
  font-size: 13.5px; font-family: var(--font); color: var(--color-dark); background: var(--panel-bg);
}
.qm-field textarea { height: 110px; resize: vertical; line-height: 1.5; }
.qm-field input:focus, .qm-field textarea:focus { outline: none; border-color: var(--color-brand); }
.qm-compose-actions { display: flex; justify-content: flex-end; align-items: center; gap: 10px; padding: 13px 20px; border-top: 1px solid var(--border); flex-shrink: 0; }
.qm-compose-status { font-size: 11px; color: var(--color-neu); margin-right: auto; }
.qm-compose-status.qm-status-error { color: var(--color-neg); font-weight: 700; }

/* ── MOBILE TYPOGRAPHY/TOUCH SCALE (v151.41) ──
   Applies to every page that links common.css, not just Staff - genuinely
   shared, same reasoning as the rest of this file. Two real, non-cosmetic
   reasons this exists, not just "looks nicer":
   1. Any text <input>/<select> under 16px triggers iOS Safari's
      auto-zoom-on-focus - jarring, not a style preference.
   2. A fixed px width (Event Types' "Add" row inputs, its search box, any
      future screen's) doesn't know the viewport is 375px wide - !important
      is the only way a shared stylesheet rule can override an inline
      style="width:...px" without hand-editing every call site's markup. */
@media (max-width: 768px) {
  /* v151.42/43 - padding/button size tightened twice now (v151.42's delta
     was real but too small to register in a side-by-side screenshot - this
     pass cuts further, deliberately). 16px stays on .fb-input/.form-input
     regardless - that floor is the real iOS-zoom fix, not negotiable - only
     the padding around it shrinks. */
  .fb-input {
    font-size: 16px !important; padding: 6px 10px !important; width: 100% !important;
  }
  .fb-save-btn, .fb-tab-btn {
    font-size: 13px; padding: 7px 14px;
  }
  .fb-form-field { width: 100%; }
  .fb-form-label { font-size: 12px; }
  .fb-form-status { font-size: 12px; }
  .form-input { font-size: 16px; padding: 6px 10px; }
  .btn { font-size: 14px; padding: 8px 18px; }

  /* v151.44 - real overflow bug: the session countdown badge's full
     "Session timeout in: HH:MM:SS" text doesn't fit next to the logo/STAFF
     label on a real phone width - was pushing the topbar wider than the
     viewport. Label text drops, digits alone stay (still the useful part -
     what matters in the moment is the countdown, not the sentence around
     it), topbar's own horizontal padding also tightened since 30px a side
     was sized for desktop. */
  .topbar { padding: 10px 12px; gap: 8px; }
  .wa-session-badge { padding: 5px 10px; font-size: 11px; }
  .wa-session-badge-label { display: none; }
  /* Same overflow reasoning as the session badge above - "Viewing as: ..."
     doesn't fit a phone width either; the Switch Role button alone stays. */
  .wa-role-emulate-label { display: none; }

  /* v151.79 - real overflow bug, back again: v151.44's fix above covered
     the topbar as it existed then; the Switch Role button was added to it
     afterward and was never given the same treatment - hiding just its own
     label (immediately above) wasn't enough, the button itself was still
     one element too many for one row on a real phone, forcing the whole
     page horizontally scrollable again. Split into two rows instead of
     shrinking further - explicit user direction, and the logo/STAFF pairing
     on row 1 was already fitting fine before Switch Role existed, so it's
     left alone.
     First attempt at this (giving #wa-role-switch-btn AND .wa-session-badge
     their own flex-basis:100% each) was wrong and caught before shipping,
     not after: an item with flex-basis:100% consumes its ENTIRE wrapped
     line for itself, so with both set the same way they landed on two
     SEPARATE rows (3 rows total), not sharing one - confirmed via a real
     rendered-DOM test, not just reasoned about. Fixed with the standard
     flexbox no-extra-markup line-break trick instead: a zero-height
     `.topbar::before` pseudo-element is the thing that actually forces the
     wrap (flex-basis:100%, ordered between row 1's items and row 2's via
     `order`), and the two real row-2 elements sit AFTER it at their own
     normal (non-100%) width, sharing that same new line - the badge's
     existing margin-left:auto (already there for desktop right-alignment)
     then pushes it to the row's right edge for free. Whichever of the two
     is actually present (a non-SysAdmin session never gets the button at
     all, staff-role-switch.js) lands correctly either way - tested both. */
  .topbar { flex-wrap: wrap; }
  .topbar::before { content: ''; order: 5; flex-basis: 100%; height: 0; }
  #wa-role-switch-btn { order: 6; }
  .wa-session-badge { order: 7; }
  /* v151.177 - real bug, found live: form.html (v151.175's form_locks) added
     a SECOND element sharing .wa-session-badge (#form-timeout-badge) - this
     rule predates that and was only ever written for ONE such badge per
     page. With two, both get margin-left:auto and order:7 - the first
     badge's auto margin consumes all the free space pushing the PAIR to the
     right edge, leaving the second badge's own auto margin nothing left to
     distribute, so they land jammed together with no real separation.
     Giving the form-timeout badge its own order/margin here instead: it
     sits at the row's left edge, the session badge's own auto-margin still
     pushes IT alone to the right edge - same "not scrunched at the far
     right" fix v151.79 already established, just for the new second badge. */
  #form-timeout-badge { order: 6; margin-left: 0; }
}

/* Queries pane (v151.110) - same drawer-nav + full-width-swap pattern
   Mailbox already established for its own folder nav / list-vs-detail on
   narrow viewports (staff.css's own .mb-folder-nav/.mb-mobile-* mobile
   rules), not a new pattern invented for this screen.

   v151.167 - real gap, found live: Compact View only patches JS-driven
   IS_MOBILE (nav-panel/hamburger/drawer) - a raw @media query can't see
   that override at all, it only ever looks at the real viewport. So on a
   wide screen with Compact forced on, the client nav went mobile while
   Queries stayed desktop. Switched the trigger from @media(max-width:768px)
   to `body.is-mobile-view`, a class the client pages' own IS_MOBILE-setup
   scripts now keep in sync (set at load AND on a resize listener, so real-
   viewport responsiveness is unchanged from before) - same fix as staff.css's
   own Mailbox/Notification Log block, same reasoning, see that block's own
   comment for the full story. */
body.is-mobile-view .qm-frame { height: 78vh; max-height: 620px; border-radius: var(--radius); }
body.is-mobile-view .qm-mobile-hamburger { display: inline-block; }
body.is-mobile-view .qm-folders {
  position: absolute; top: 0; left: -160px; bottom: 0; width: 160px; z-index: 5;
  background: var(--panel-bg); box-shadow: 2px 0 10px rgba(0,0,0,0.12);
  transition: left 0.18s; border-right: 1px solid var(--border);
}
body.is-mobile-view .qm-body { position: relative; overflow: hidden; }
body.is-mobile-view .qm-body.qm-drawer-open .qm-folders { left: 0; }
body.is-mobile-view .qm-body.qm-drawer-open::before {
  content: ''; position: absolute; inset: 0; background: rgba(15,23,42,0.35); z-index: 4;
}
body.is-mobile-view .qm-list-col { width: 100%; border-right: none; }
body.is-mobile-view .qm-detail { display: none; }
body.is-mobile-view .qm-body.qm-thread-open .qm-list-col { display: none; }
body.is-mobile-view .qm-body.qm-thread-open .qm-detail { display: flex; }
body.is-mobile-view .qm-mobile-back { display: inline-block; }

/* Receipt modal shell - real port of ReceiptModal.html's own CSS (GAS
   includes this file once and shares it across Dashboard.html/Groups.html
   via HtmlService; this stack has no server-side include, so this is the
   single canonical copy every page's own receipt-modal.js consumer shares -
   moved here from dashboard.html once Groups.html needed a second consumer,
   rather than duplicating the block a second time). Also reused as-is for
   the Profile modal shell (public/profile-modal.js) via distinct ids on the
   same classes - see dashboard.html's own profile-modal-overlay markup. */
.rcpt-modal-overlay { position: fixed; inset: 0; background: rgba(15,23,42,0.55); display: none; align-items: center; justify-content: center; z-index: 1000; }
.rcpt-modal-overlay.open { display: flex; }
.rcpt-modal-box { background: #fff; border-radius: 10px; width: min(720px, 92vw); height: min(880px, 90vh); display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.3); }
.rcpt-modal-hdr { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid #e2e8f0; }
.rcpt-modal-title { flex: 1; font-size: 14px; font-weight: 600; color: #1e293b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rcpt-modal-status { font-size: 12px; color: #16a34a; white-space: nowrap; }
.rcpt-modal-status.error { color: #dc2626; }
.rcpt-modal-btn { font-size: 12px; font-weight: 600; padding: 6px 14px; border-radius: 6px; border: 1px solid var(--color-brand); background: #fff; color: var(--color-brand); cursor: pointer; white-space: nowrap; display: inline-flex; align-items: center; gap: 6px; }
.rcpt-modal-btn:hover { background: var(--color-brand); color: #fff; }
.rcpt-modal-btn:disabled { opacity: 0.6; cursor: default; }
.rcpt-modal-btn:disabled:hover { background: #fff; color: var(--color-brand); }
.rcpt-spin { width: 11px; height: 11px; border: 2px solid rgba(244,121,32,0.25); border-top-color: var(--color-brand); border-radius: 50%; animation: _rcptSpin 0.7s linear infinite; display: none; }
@keyframes _rcptSpin { to { transform: rotate(360deg); } }
.rcpt-modal-close { font-size: 20px; line-height: 1; padding: 2px 8px; border: none; background: none; color: #64748b; cursor: pointer; }
.rcpt-modal-body { flex: 1; position: relative; background: #f8fafc; }
.rcpt-modal-body iframe { width: 100%; height: 100%; border: none; display: block; }
.rcpt-skeleton { position: absolute; inset: 0; padding: 28px 32px; background: #fff; overflow: hidden; }
.sk-bar { border-radius: 4px; background: linear-gradient(90deg, #ecf0f4 25%, #f7f8fa 37%, #ecf0f4 63%); background-size: 400% 100%; animation: _rcptShimmer 1.4s ease infinite; }
@keyframes _rcptShimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
.sk-title  { height: 20px; width: 55%; margin-bottom: 20px; }
.sk-meta   { height: 11px; margin-bottom: 9px; }
.sk-meta.w1 { width: 42%; }
.sk-meta.w2 { width: 32%; }
.sk-group  { height: 10px; width: 28%; margin: 26px 0 14px; }
.sk-fld    { display: flex; align-items: center; gap: 16px; margin-bottom: 12px; }
.sk-fld .sk-bar:first-child { width: 30%; height: 10px; }
.sk-fld .sk-bar:last-child  { flex: 1; height: 10px; }
.rcpt-modal-error { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; font-size: 13px; color: #64748b; background: #fff; }

/* Client-facing page shell (topbar + desktop nav-panel + mobile layout) -
   real port of Dashboard.html's own shell CSS. Moved here from
   dashboard.css once Groups.html needed a second consumer - genuinely
   missed the first time (only .rcpt-modal-* got this treatment originally),
   which is why Groups' nav rendered as bare unstyled buttons and both
   desktop/mobile layouts stayed visible at once (no .mobile-layout{display:
   none} to hide the second one) until this was caught live. Every
   client-facing page (dashboard.html, groups.html, profile if it ever
   becomes its own page) shares this unchanged - only page-specific content
   (.content-inner, .profile-card, etc.) stays in the page's own CSS file. */
/* v151.225 - same 100vh-vs-real-mobile-viewport bug fixed on staff.css's
   own body rule (see that comment for the full explanation) - this is the
   identical pattern, shared by every client-facing page, so the same real
   risk applies here: a real phone's address bar showing means 100vh
   overstates the actual visible area, and overflow:hidden then makes
   whatever falls in that phantom gap physically unreachable. Confirmed on
   the staff side live; fixed proactively here too rather than waiting for
   the same report from the client side of the app. */
body.client-shell { height: 100vh; height: 100dvh; overflow: hidden; display: flex; flex-direction: column; }
.desktop-layout { display: flex; flex: 1; overflow: hidden; }
/* v151.343 - real bug fix, found live: the whole nav-panel scrolled as ONE
   block (identity + list + footer together) - once nav-list's own content
   grew taller than the viewport (Form Preview pushed it over that edge),
   Profile/Sign Out (nav-footer, the LAST child) could scroll out of reach
   entirely, with no visual cue a scroll was even needed. Direct
   instruction: header/footer must always stay visible, only the middle
   list scrolls. overflow:hidden here + flex:1/min-height:0/overflow-y:auto
   on .nav-list alone (below) is the standard fix for a flex child that
   needs to scroll independently within a fixed-height flex column parent -
   min-height:0 is the actual fix (a flex item's default min-height is
   "auto", which means "never shrink below your own content's height",
   exactly the bug here). */
.nav-panel {
  width: 220px; flex-shrink: 0;
  background: var(--chrome-bg);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  display: flex; flex-direction: column; overflow: hidden;
}
.nav-identity { padding: 16px; border-bottom: 1px solid var(--chrome-border-08); flex-shrink: 0; }
.nav-identity-name  { font-weight: 700; color: var(--chrome-fg); font-size: 13px; line-height: 1.4; }
.nav-identity-sub   { font-size: 11px; color: var(--chrome-fg-45); margin-top: 3px; }
.nav-identity-email { font-size: 10px; color: var(--chrome-fg-30); margin-top: 3px; word-break: break-all; }
.nav-identity-id    { font-size: 10px; color: var(--chrome-fg-20); margin-top: 4px; font-family: var(--font-mono); word-break: break-all; }
.nav-identity-tag   {
  display: inline-block; margin-top: 8px; padding: 2px 8px;
  border-radius: 3px; font-size: 10px; font-weight: 700;
  background: var(--color-brand); color: #fff; text-transform: uppercase;
}
.nav-list { flex: 1; min-height: 0; overflow-y: auto; padding: 6px 0; }
.nav-btn {
  display: block; width: 100%; text-align: left;
  padding: 10px 16px; font-size: 13px; color: var(--chrome-fg-65);
  background: none; border: none; cursor: pointer;
  font-family: var(--font); transition: background 0.12s, color 0.12s;
}
.nav-btn:hover:not(.nav-btn-disabled) { background: var(--chrome-hover-08); color: var(--chrome-fg); }
.nav-btn.active { background: var(--color-brand); color: #fff; }
.nav-btn-disabled { opacity: 0.35; cursor: default; }
.nav-sep    { border-top: 1px solid var(--chrome-border-08); margin: 6px 0; flex-shrink: 0; }
.nav-footer { padding: 8px 0 12px; flex-shrink: 0; }
.content-panel { flex: 1; overflow-y: auto; background: transparent; padding: 28px var(--content-gutter, 32px); }
.hamburger {
  display: none; background: none; border: none;
  cursor: pointer; padding: 4px 8px 4px 0; flex-shrink: 0;
}
/* v151.160 - was 50x8px/8px gap, real port of AppNav.html's own oversized
   original (same root cause as the removed duplicate .drawer* block above -
   never revisited for a real phone width). Staff pages never hit this rule
   (staff.css's own .hamburger-icon span at 24x3px/5px gap wins there,
   loaded after common.css) - this is the client-page (Groups etc.) icon,
   confirmed oversized live. Matched to staff's own scale for consistency. */
.hamburger-icon { display: flex; flex-direction: column; gap: 5px; }
.hamburger-icon span { display: block; width: 24px; height: 3px; background: var(--chrome-fg); border-radius: 2px; }
.mobile-layout { flex: 1; overflow: hidden; position: relative; display: none; }
.mobile-content { width: 100%; height: 100%; overflow-y: auto; background: transparent; padding: 20px var(--content-gutter, 20px); }

@media (max-width: 768px) {
  .client-shell .topbar { padding: 18px 20px; }
  .client-shell .topbar-logo { font-size: 22px; }
  .client-shell .hamburger { display: flex; }
  .client-shell .desktop-layout { display: none; }
  .client-shell .mobile-layout { display: block; }
}

/* Shared field-widget CSS - real port of Profile.html's own "Account
   Details" section styling. Two consumers now (Dashboard's Profile modal,
   Groups' member add/edit modal + its own Profile modal) - moved here
   rather than left duplicated a third time, same reasoning as the shell/
   receipt-modal moves above. .modal-field/.p-state/.g-state stay page-local
   (GAS itself duplicates .modal-field per page too, not a shared class
   there either). */
.p-section-hdr { padding: 11px 16px; font-size: 13px; font-weight: 600; color: var(--color-dark); background: var(--surface-1); border-bottom: 1px solid var(--border); }
.p-row { display: flex; align-items: flex-start; padding: 11px 16px; border-bottom: 1px solid var(--border); gap: 12px; }
.p-row:last-child { border-bottom: none; }
.p-label { flex-shrink: 0; width: 90px; font-size: 11px; color: var(--color-neu); padding-top: 3px; line-height: 1.4; }
.p-val-wrap { flex: 1; min-width: 0; }
.p-val { font-size: 13px; color: var(--color-dark); min-height: 22px; line-height: 1.45; word-break: break-word; }
.p-val.muted { color: var(--color-neu); font-style: italic; }
.p-edit { display: none; flex-direction: column; gap: 8px; margin-top: 8px; }
.p-edit.open { display: flex; }
.p-save-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.p-status { font-size: 11px; }
.p-status.ok  { color: var(--color-pos); }
.p-status.err { color: var(--color-neg); }
.btn-pen { background: none; border: none; cursor: pointer; color: var(--color-neu); font-size: 14px; padding: 2px 4px; line-height: 1; border-radius: var(--radius); flex-shrink: 0; transition: color 0.12s; }
.btn-pen:hover { color: var(--color-brand); }
.btn-pen.cancel { color: var(--color-neg); }
.otp-step { display: none; flex-direction: column; gap: 8px; }
.otp-step.open { display: flex; }
.otp-hint { font-size: 12px; color: var(--color-neu); }
/* Close Account (v151.127) - the one "danger zone" section on Profile,
   visually set apart from the ordinary field rows above it. */
.p-section-danger { margin-top: 16px; border: 1px solid var(--color-neg); border-radius: var(--radius); overflow: hidden; }
.p-section-danger .p-section-hdr { color: var(--color-neg); }
.p-section-danger .otp-step { padding: 12px 16px; }
.p-danger-note { font-size: 12px; color: var(--color-neu); line-height: 1.5; margin: 0 0 4px; }
.name-fields { display: flex; gap: 8px; flex-wrap: wrap; }
.name-fields .form-input { flex: 1; min-width: 90px; }
.phone-fields { display: flex; gap: 8px; }
.phone-cc-sel { flex: 2; }
.phone-cc     { width: 70px; flex-shrink: 0; }
.phone-num    { flex: 3; }
.code-input { display: flex; gap: 8px; margin-top: 4px; }
.digit-box {
  width: 38px; height: 44px; border: 1px solid var(--border-dark); border-radius: 6px;
  font-size: 20px; font-weight: 700; text-align: center;
  color: var(--color-dark); background: #fff; outline: none; font-family: var(--font-mono);
}
.digit-box:focus { border-color: var(--color-brand); box-shadow: 0 0 0 2px rgba(244,121,32,0.15); }
.pin-group-label { font-size: 11px; font-weight: 700; color: var(--color-neu); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
