/* COS Kiosk Timeclock — Styles
   Designed for iPad portrait (768×1024) and Chromebook landscape touchscreens.
   Touch targets: 60px minimum. Dark navy theme with marble background and glass surfaces.
   Visual reference: Figma "Timeclock — Current State" page in COS-Library file. */

/* ── Geist, self-hosted (build 1.78) ─────────────────────────────────────── */
/* Until 1.78 index.html loaded these from Google Fonts, and a stylesheet in
   <head> holds the first paint until it arrives: on a store Wi-Fi that passed
   nothing, that one request kept the whole kiosk white. They are the web app's
   own files (@fontsource/geist-sans, latin subset, OFL -- assets/fonts/OFL.txt),
   cached by the service worker with the other assets. The weights are the five
   the Google Fonts link asked for; the one font-weight: 800 rule still resolves
   to 900. The URLs are relative so the phone copy under /static/mobile/timeclock/
   finds them too. */
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('../assets/fonts/geist-sans-latin-400-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('../assets/fonts/geist-sans-latin-500-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('../assets/fonts/geist-sans-latin-600-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('../assets/fonts/geist-sans-latin-700-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Geist';
  font-style: normal;
  font-weight: 900;
  font-display: swap;
  src: url('../assets/fonts/geist-sans-latin-900-normal.woff2') format('woff2');
}

:root {
  /* Surfaces */
  --bg:                 #0f172a;
  --bg-card:            #1e293b;
  --bg-input:           #0f172a;
  --border:             #334155;

  /* Text */
  --text:               #f1f5f9;
  --text-muted:         #94a3b8;
  --text-pure-white:    #ffffff;
  --text-placeholder:   #64748b;

  /* Brand / accent */
  --primary:            #3b82f6;
  --primary-dark:       #2563eb;
  --brand-navy:         #001f5b;

  /* Semantic */
  --success:            #10b981;
  --error:              #ef4444;
  --warning:            #f59e0b;

  /* Glass system (placeholder approximation of Figma Native Glass — tune during QA) */
  --glass-tint:         rgba(160, 161, 162, 0.20);
  --glass-tint-active:  rgba(160, 161, 162, 0.30);
  --glass-tint-flash:   rgba(160, 161, 162, 0.45);  /* key-press flash — whole button, ~50% brighter than active */
  --glass-border:       rgba(255, 255, 255, 0.18);
  --glass-blur:         24px;
  --glass-blur-sm:      16px;
  --glass-saturate:     160%;
  --glass-sheen-from:   rgba(255, 255, 255, 0.15);
  --glass-shadow:       0 4px 12px rgba(0, 0, 0, 0.25);

  /* Selection tint (rows on screens 04, 05, 11) */
  --selected-tint:      rgba(59, 130, 246, 0.10);

  /* Break-elapsed and offline pills */
  --pill-warn-bg:       rgba(245, 158, 11, 0.12);
  --pill-warn-border:   rgba(245, 158, 11, 0.35);
  --pill-warn-bg-soft:  rgba(245, 158, 11, 0.10);
  --pill-warn-border-soft: rgba(245, 158, 11, 0.30);

  /* Radii */
  --radius:             12px;
  --radius-lg:          20px;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  /* Kiosk-wide: a long-press must never select button/label text or pop the
     iOS callout menu. The bare `user-select: none` on <button> alone isn't
     honored on iOS Safari, and the tappable non-button elements (numpad keys,
     .role-row, .shift-row) get nothing — so suppress selection + callout
     globally and re-enable it only on real text inputs below. */
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* Re-enable text selection where the employee actually types/edits. */
input,
textarea {
  -webkit-user-select: text;
  -moz-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

html {
  height: 100%;
  background: var(--bg);
}

/* ── Mobile timeclock scale ──────────────────────────────────────────────── */
/* The layout is designed for iPad portrait (768×1024); on phones everything
   reads oversized. `zoom` scales every px-based length (text, buttons, status
   bar, logo, spacing) uniformly while participating in real layout — unlike
   transform:scale, which breaks fixed positioning. .mode-mobile is set on
   <html> before first paint by an inline <head> script in index.html (same
   /mobile/timeclock/ path test as config.js MODE) and is the hook for any
   further mobile-only overrides. Kiosk mode is untouched. */
html.mode-mobile {
  zoom: 0.75;
}

body {
  height: 100%;
  overflow: hidden;
  /* No background here — html bg is the fallback, body::before/::after layer
     the marble + vignette on top. Setting body bg would occlude the negative-z
     pseudo-elements via CSS painting order. */
  color: var(--text);
  font-family: 'Geist', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  font-size: 18px;
  line-height: 1.4;
}

/* ── Marble background system ────────────────────────────────────────────── */
/* Layer 1: silk marble image scaled cover. Layer 2: radial vignette darkening edges.
   Both fixed to viewport so they don't scroll with content. */

body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -2;
  /* Stylesheet-relative (#602): resolves at / on cos-kiosk AND under the
     Django /static/mobile/timeclock/ mount (mobile timeclock). */
  background: url('../assets/bg-marble.jpg') center / cover no-repeat;
  opacity: 0.78;
  pointer-events: none;
}

/* Edge-darkening vignette: transparent through ~35% of the radius,
   fading to solid black at the corners. Sits at z-index: 0; .screen
   below is bumped to z-index: 2 so UI paints above this overlay. */
body::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  background: radial-gradient(ellipse at center,
    transparent 35%,
    rgba(0, 0, 0, 1) 100%);
  pointer-events: none;
}

/* Citrin brand mark — Figma node 142:106 (citrinWhite). Position
   and dimensions taken straight from the 768×1024 Background frame:
   x=287, y=970.5, width=193, height=30 → 23.5px from the bottom,
   horizontally centered. Opacity 0.62 verbatim. Sits above the
   vignette but below screens so empty regions of any timeclock
   screen show the mark; screens with opaque content cover it. */
#bg-citrin-white {
  position: fixed;
  bottom: 72px;
  left: 50%;
  transform: translateX(-50%);
  width: 193px;
  height: 30px;
  opacity: 0.62;
  z-index: 1;
  pointer-events: none;
  transition: opacity 0.2s ease;
}

/* Set by _updateLogoVisibility() (app.js) whenever the active screen's
   content overflows its viewport — on short windows/phones the mark yields
   instead of colliding with content. Kiosk-normal viewports never overflow
   the nav screens, so the mark sits exactly where it always has. */
#bg-citrin-white.logo-hidden {
  opacity: 0;
}

/* ── Status Bar — Figma StatusBar / Online > Rectangle ───────────────────── */
/* Translucent-white fill over the page bg + hairline bottom border + 6px
   backdrop-blur. Values pulled directly from Figma node 193:155 in the
   COS-Library file — do not substitute defaults. */

#status-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 48px;
  background: rgba(255, 255, 255, 0.06);
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
  z-index: 100;
  font-size: 16px;
  color: var(--text-pure-white);
}

#status-indicator {
  display: flex;
  align-items: center;
  gap: 8px;
}

#status-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--text-muted);
  transition: background 0.3s;
}

#status-bar.online  #status-dot { background: var(--success); }
/* Offline reads as a neutral, non-alarming GRAY rather than red — a kiosk that
   queues punches and syncs automatically isn't an error state (Mike). */
#status-bar.offline #status-dot { background: var(--text-muted); }
#status-bar.syncing #status-dot {
  background: var(--warning);
  animation: pulse 1s infinite;
}

#status-text {
  font-weight: 400;
  color: var(--text-pure-white);
}

#status-right {
  display: flex;
  align-items: center;
  gap: 10px;
}

#pending-badge {
  background: var(--brand-navy);
  color: var(--text-pure-white);
  padding: 3px 10px;
  border-radius: 20px;
  font-weight: 500;
  font-size: 14px;
}

/* Roster-age indicator, beside the pending pill. Amber, not red: a kiosk
   running on an old roster is still clocking people in correctly — this asks a
   manager to reconnect it, it does not report a failure. The schedule-age
   indicator (build 1.67) wears the same pill for the same reason. */
#stale-badge,
#schedule-badge {
  border: 1px solid var(--warning);
  color: var(--warning);
  padding: 2px 10px;
  border-radius: 20px;
  font-weight: 500;
  font-size: 14px;
  white-space: nowrap;
}

/* Build-version marker — unobtrusive, far top-right. Confirms which build the
   iPad is actually running (PWA caches assets, so this is the visible signal). */
#build-version {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--text-muted);
}

/* Live-version status next to the build number. */
.build-status {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.02em;
}
.build-status.latest { color: var(--success); }
.build-status.stale  { color: var(--warning); }

/* DEV environment tag beside the build number. Revealed by app.js only when
   KIOSK_CONFIG.API_BASE is not the production API; `.hidden` (display:none
   !important, further down) wins over the inline-block here until then.
   Warning amber with near-black text so it reads at a glance on the navy bar,
   and survives the 0.75 zoom the mobile mount applies to the whole page. */
.env-tag {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 20px;
  background: var(--warning);
  color: #1a1200;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.08em;
}

/* Toolbar refresh — compact glyph button that fits the 48px bar. Same action
   as the admin Force Refresh, available on every screen. */
#btn-toolbar-refresh {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  font-size: 18px;
  line-height: 1;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-pure-white);
}
#btn-toolbar-refresh:hover { background: rgba(255, 255, 255, 0.16); }

/* Phone status bar (build 1.57, Mike): a title, not a control panel. The build
   number, its "(latest)" status and the refresh button are kiosk tools -- the
   phone has no service worker and no cached build to refresh, so ⟳ did nothing
   useful and the build number meant nothing to an employee. The running build
   is still one request away (version.json) and shows again under `?debug=1`.
   The green dot goes with "ONLINE"; the OFFLINE dot stays, because OFFLINE
   still shows. The DEV tag stays too -- it is hidden on production anyway, and
   on dev it is how a tester knows which environment a punch is about to hit. */
html.mode-mobile:not(.kiosk-debug) #build-version,
html.mode-mobile:not(.kiosk-debug) #build-status,
html.mode-mobile:not(.kiosk-debug) #btn-toolbar-refresh,
html.mode-mobile #status-bar.online #status-dot {
  display: none;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.4; }
}

/* ── Loading Overlay ─────────────────────────────────────────────────────── */

#loading-overlay {
  position: fixed;
  inset: 0;
  background: var(--bg);
  display: flex;
  /* Column + gap for the phone's one-line note under the spinner (build 1.56);
     with the note hidden the spinner centers exactly as it did in a row. */
  flex-direction: column;
  gap: 24px;
  align-items: center;
  justify-content: center;
  z-index: 200;
  transition: opacity 0.3s ease;
}

#loading-overlay-text {
  font-size: 22px;
  font-weight: 500;
  color: var(--text);
  text-align: center;
  padding: 0 32px;
}

#loading-overlay.hidden {
  opacity: 0;
  pointer-events: none;
}

/* Mobile punch "Saving…" overlay — same full-screen treatment as the loading
   overlay but translucent over the active screen (the frozen capture frame
   stays dimly visible behind it, so the moment reads as "your photo is being
   saved" rather than a context switch). Shown only by the mobile submit
   paths in app.js; kiosk punches confirm instantly from the local queue. */
#saving-overlay {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.82);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  z-index: 200;
}

#saving-overlay.hidden {
  display: none;
}

#saving-overlay-text {
  font-size: 24px;
  font-weight: 500;
  color: var(--text);
}

/* Manager PIN second-factor overlay (pair/admin flows). Full-screen above the
   active screen; reuses the marble page background and .card/.numpad styling. */
#manager-pin-overlay {
  position: fixed;
  inset: 0;
  z-index: 210;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow-y: auto;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(4px);
}

#manager-pin-overlay.hidden {
  display: none;
}

/* ── Inactivity warning overlay ─────────────────────────────────────────────
   Shown after a short idle on post-PIN navigation screens. Sits above every
   screen; the global `.hidden { display:none !important }` rule hides it. */
#idle-warning {
  position: fixed;
  inset: 0;
  z-index: 220;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

#idle-warning-card {
  background: rgba(15, 23, 42, 0.92);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 36px 40px;
  text-align: center;
  max-width: 90%;
  box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5);
}

#idle-warning-card h2 {
  color: var(--text-pure-white);
  margin-bottom: 12px;
}

#idle-warning-body {
  color: var(--text);
  font-size: 20px;
  margin-bottom: 28px;
}

#idle-warning-count {
  font-weight: 700;
  color: #ff6b6b;
  font-variant-numeric: tabular-nums;
}

#btn-idle-stay {
  margin: 0 auto;
  min-width: 220px;
}

.loading-spinner {
  width: 48px;
  height: 48px;
  border: 4px solid var(--border);
  border-top-color: var(--primary);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

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

/* ── Screens ─────────────────────────────────────────────────────────────── */

.screen {
  display: none;
  position: fixed;
  inset: 48px 0 0 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  /* Stacking layers (low → high):
       body::before  marble    z: -2
       body::after   vignette  z:  0
       #bg-citrin-white logo   z:  1
       .screen                 z:  2  ← here
       #status-bar             z:100
     Screens stay transparent except for actual content, so the
     citrin-white mark behind them is visible through empty space. */
  z-index: 2;
}

.screen.active {
  display: flex;
  flex-direction: column;
}

/* Camera screen STANDBY (kiosk camera modes standby / pin / always, build
   1.49): a render box while another screen is up. WebKit builds no decode or
   presentation pipeline for an element without one, and between punches the
   camera screen was display:none -- so every punch rebuilt #camera-video's
   pipeline from nothing while the employee stood there. Every property here
   is load-bearing: display keeps the box; near-zero but NON-zero opacity
   (a fully transparent layer is one WebKit may skip painting) keeps it in the
   compositor, which is what requestVideoFrameCallback keys on; z-index 1 puts
   it under every active .screen (z-index 2) and over the marble (the "its own
   layer" that used to be listed here is .camera-wrap's now -- see the build
   1.64 note below); pointer-events keeps
   the Back button unreachable. The active class wins by cascade order and
   the :not() guard. app.js toggles the class; mode `off` never sets it.

   Build 1.64: the layer promotion that used to live HERE (transform:
   translateZ(0)) moved to .camera-wrap, permanently. Declared on a
   :not(.active) rule it was CREATED when the punch screen went to standby and
   DESTROYED the instant .active was added -- i.e. the compositing layer
   holding a live <video> was torn down and rebuilt at the exact moment the
   employee arrived at the photo screen. On iPadOS 26.6 (Jim Ellis Genesis
   Kiosk 1, 2026-09-22) that left a second, mis-sized copy of the preview
   painted over the face oval and outside the countdown veil. The render box
   this rule exists to guarantee is unaffected: display, opacity, z-index and
   pointer-events all stay, and .camera-wrap is now promoted in BOTH states
   rather than one, so the sub-tree that actually holds the video is never
   culled. */
.screen.screen-camera-standby:not(.active) {
  display: flex;
  flex-direction: column;
  opacity: 0.02;
  pointer-events: none;
  z-index: 1;
}

.screen-content {
  flex: 1;
  /* Figma uses 124px horizontal padding on iPad portrait (768px wide → 520px content).
     Clamp lets wider Chromebook landscape screens center the 520px column at 24px min.
     Bottom padding reserves the fixed Citrin-mark band (bottom: 72px + 30px tall
     + breathing room) so the last element of a scrolling screen always comes to
     rest clear of the logo instead of on top of it. */
  padding: 32px clamp(24px, calc((100vw - 520px) / 2), 124px) 112px;
  max-width: 768px;
  margin: 0 auto;
  width: 100%;
}

.center-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  min-height: calc(100vh - 48px);
  text-align: center;
  /* Same logo-band reservation for centered screens, but SYMMETRIC so the
     content's center point is pixel-identical on kiosk viewports where it
     already fits. On short viewports (phones, small windows) the screen
     scrolls and the bottom padding guarantees the last button can never
     rest in the logo band. */
  padding-top: 112px;
  padding-bottom: 112px;
}

/* Auto-margin spacers center the content when it fits — pixel-identical to
   the old justify-content: center — but unlike justify center (which
   overflows BOTH ends on short viewports, clipping the top unreachably and
   swallowing the bottom padding) they collapse to 0 on overflow, leaving a
   normally scrollable start-aligned column whose bottom padding keeps the
   last element clear of the Citrin mark. */
.center-content::before,
.center-content::after {
  content: '';
}

.center-content::before { margin-top: auto; }
.center-content::after  { margin-bottom: auto; }

/* Vertically center the content block of .screen-centered screens (scoped so
   other screens keep their flex:1 stretch behavior). Auto-margin spacers, NOT
   justify-content: center — justify center overflows BOTH ends on short
   viewports, clipping the top unreachably (the mobile questionnaire's first
   question was cut off with no way to scroll to it). The spacers collapse to
   0 on overflow, leaving a normally scrollable start-aligned column — same
   pattern as .center-content above. */
.screen-centered.active::before,
.screen-centered.active::after {
  content: '';
}

.screen-centered.active::before { margin-top: auto; }
.screen-centered.active::after  { margin-bottom: auto; }

.screen-centered .screen-content {
  flex: 0 1 auto;
}

/* Mid-screen action cluster (PIN pad + Submit; confirm-screen action
   buttons). Auto vertical margins center the group in the space left below
   the top-aligned header, collapsing to 0 on short viewports so the column
   scrolls normally. */
.centered-action-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  margin-top: auto;
  margin-bottom: auto;
}

/* PIN screen + off-clock confirm: the header (location name / Welcome +
   name + status) rides the top; the action group centers itself in the
   remaining space via .centered-action-group's auto margins. The base
   .center-content spacers would fight those, so remove them here. */
#screen-pin .center-content::before,
#screen-pin .center-content::after,
#screen-confirm:not(.state-active) .center-content::before,
#screen-confirm:not(.state-active) .center-content::after {
  content: none;
}

#screen-pin .center-content,
#screen-confirm:not(.state-active) .center-content {
  padding-top: 40px;
}

/* Clocked-in / on-break confirm (.state-active, toggled by JS): the whole
   column (Welcome + name + status + actions) centers as ONE block via the
   base .center-content spacers, so the status rows land near the middle
   instead of hugging the top — the off-clock state keeps the top-aligned
   header because its picker already fills the middle. The fixed-height
   area (below) is re-tuned for this geometry so Clock Out / End Break stay
   on the PIN-Submit spot. */
#screen-confirm.state-active .centered-action-group {
  margin-top: 0;
  margin-bottom: 0;
}

#screen-confirm.state-active #confirm-actions-area {
  margin-top: 40px; /* consistent gap between the status rows and the actions */
  min-height: 186px;
}

/* Fast-path scheduled-shift picker centers exactly like the PIN keypad
   group — both are .centered-action-group, so both center in the same space
   below their (same-padding) header. The fixed-height card area makes this
   group's total height match the keypad group's, so the "Clock in" button
   lands at the SAME resting position as the PIN Submit button at ANY
   viewport height, not just one. The cards center within the area; spare
   room is breathing space above/below them (like the keypad in its column).
   The clocked-in action buttons keep their own centered group. */
#confirm-preselect {
  max-width: 340px;
}

/* Holds SELECTED ROLE + the merged role bubbles + "Choose a different
   role", centered as one cluster with even padding above/below (the space
   between the OFF THE CLOCK status and the Clock in button). The fixed
   min-height keeps the group as tall as the keypad group so "Clock in"
   still lands on the PIN Submit button. */
#confirm-shift-area {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  width: 100%;
  min-height: 334px;
}

#confirm-selected-label {
  margin-bottom: 16px;
}

#confirm-shift-area #btn-choose-different-role {
  margin-top: 16px;
}

/* Choose Role: same geometry as the PIN screen / confirm fast path — the
   location header rides the top, and the prompt + role list + confirm button
   center as ONE .centered-action-group in the remaining space. The fixed
   list-area height matches #confirm-shift-area (334px) and the internal
   role-list scroll caps at 240px (prompt ≈68px + list 240px + 24px gap ≤
   334px even when full), so "Clock into selected role" lands on the SAME
   resting spot as the PIN Submit / confirm "Clock in" button at any
   viewport height and any number of roles. */
#screen-choose-role .center-content::before,
#screen-choose-role .center-content::after {
  content: none;
}

#screen-choose-role .center-content {
  padding-top: 40px;
}

#choose-role-area {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  width: 100%;
  min-height: 334px;
}

#choose-role-area .role-list {
  max-height: 240px;
}

.screen-title-centered {
  text-align: center;
}

/* Back/Exit glass pill, pinned to the bottom-right corner of EVERY screen
   (2026-07-24; previously flat top-left text, which sat directly on top of
   centered content — on mobile it overlapped the questionnaire's first
   question). position: fixed, not absolute, so it can never scroll out of
   view; only the active screen's button shows because inactive screens are
   display: none. The glass treatment keeps it legible over any content, the
   safe-area inset clears the iOS home bar, and .screen-content's 112px
   bottom padding lets scrolled content come to rest clear of it. */
.btn-screen-back {
  position: fixed;
  right: 16px;
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  z-index: 30;
  background: var(--glass-tint);
  border: 1px solid var(--glass-border);
  backdrop-filter: blur(var(--glass-blur-sm)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur-sm)) saturate(var(--glass-saturate));
  border-radius: 999px;
  box-shadow: var(--glass-shadow);
  padding: 12px 24px;
  min-height: 48px;
  font-family: inherit;
  font-size: 20px;
  font-weight: 500;
  color: var(--text);
  cursor: pointer;
}

.btn-screen-back:hover,
.btn-screen-back:focus {
  background: var(--glass-tint-active);
}

.btn-screen-back:active {
  transform: scale(0.98);
}

/* Exit-semantic marker class (confirm-screen Back/Exit, mobile sign-in and
   error-screen Exits). Positionally a no-op since 2026-07-24: EVERY
   .btn-screen-back now lives at the bottom-right exit position, so the
   modifier that used to move only these buttons there has nothing left to
   override. Kept as a semantic hook in the markup. */
.btn-screen-exit {}

/* In-screen location context line — shown above the role selection on the
   confirm screen (kiosk and mobile). Collapses when the location name isn't
   known yet. (The Choose Role screen upgraded to the large .location-header
   treatment — the punch commitment happens there.) */
.screen-location-name {
  font-size: 16px;
  font-weight: 500;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 1.6px;
  text-align: center;
  margin-bottom: 10px;
}

.screen-location-name:empty {
  display: none;
}

/* Centered employee name + role-title display. Replaces the old
   .employee-card avatar bubble on the confirm screen and the small
   .hint-styled employee line on the select-shift screen. */
.employee-name-display {
  margin: 8px 0 4px;
  font-size: 30px;
  font-weight: 800;
  color: var(--text);
  text-align: center;
}

.employee-role-label {
  margin-top: 4px;
  font-size: 24px;
  font-weight: 400;
  letter-spacing: 1px;
  color: var(--text-muted);
  text-align: center;
}

/* Action page (confirm screen) only: 96px gap between the EE name
   and the CLOCKED IN ROLE / OFF THE CLOCK label, plus a tighter
   Welcome → name gap. Scoped via #screen-confirm so the shared
   classes (.employee-name-display, h2) keep their default spacing
   on every other screen. */
#screen-confirm h2 {
  margin-bottom: 16px;
}

#screen-confirm .employee-name-display {
  margin-top: 0;
  margin-bottom: 16px;
}

#screen-confirm #confirm-role-label {
  margin-top: 0;
}

/* Clock-state dot ahead of the status label — red when off the clock,
   green when clocked in / on break. Class-gated so the other
   .employee-role-label consumers (select-shift, questionnaire headers)
   are untouched. */
.employee-role-label.status-on::before,
.employee-role-label.status-off::before {
  content: '';
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  margin-right: 10px;
  vertical-align: 2px;
}

.employee-role-label.status-on::before  { background: var(--success); }
.employee-role-label.status-off::before { background: var(--error); }

.employee-role-display {
  margin: 0;
  font-size: 26px;
  font-weight: 400;
  color: var(--text);
  text-align: center;
}

/* Center h2 and hint text on every timeclock screen for a consistent
   header style. Screens that already use .center-content inherit the
   same alignment, so this is a no-op there. */
.screen-content > h2,
.screen-content > .hint {
  text-align: center;
}

/* ── Typography (Geist single family — weight differentiation) ───────────── */

h1 { font-size: 36px; font-weight: 700; margin-bottom: 8px; color: var(--text); }
h2 { font-size: 32px; font-weight: 700; margin-bottom: 12px; color: var(--text); }

.subtitle { color: var(--text-muted); font-size: 18px; margin-bottom: 32px; }
.hint     { color: var(--text-muted); font-size: 16px; margin-bottom: 24px; }

/* ── Cards ───────────────────────────────────────────────────────────────── */

.card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 32px 28px;
  width: 100%;
  text-align: left;
}

.card h2 { font-size: 25px; margin-bottom: 12px; text-align: center; }

/* ── Form Fields ─────────────────────────────────────────────────────────── */

.field {
  margin-bottom: 20px;
}

.field label {
  display: block;
  font-size: 14px;
  color: var(--text-muted);
  margin-bottom: 6px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.7px;
}

.field input,
.field select,
.field textarea {
  width: 100%;
  background: var(--bg-input);
  border: 1.5px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-size: 18px;
  padding: 14px 16px;
  outline: none;
  transition: border-color 0.2s;
  -webkit-appearance: none;
  font-family: inherit;
}

.field input::placeholder,
.field textarea::placeholder {
  color: var(--text-placeholder);
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--primary);
}

.field textarea {
  resize: none;
  min-height: 110px;
  font-size: 16px;
}

.money-input-wrap {
  display: flex;
  align-items: stretch;
  background: var(--bg-input);
  border: 1.5px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color 0.2s;
  height: 61px;
}

.money-input-wrap:focus-within {
  border-color: var(--primary);
}

.currency {
  padding: 0 14px;
  color: var(--text-muted);
  font-size: 20px;
  background: var(--bg-card);
  display: flex;
  align-items: center;
  border-right: 1px solid var(--border);
}

.money-input-wrap input {
  border: none;
  border-radius: 0;
  flex: 1;
  font-size: 22px;
  font-weight: 700;
  color: var(--text);
  padding: 16px;
  background: transparent;
}

/* ── Buttons (base) ──────────────────────────────────────────────────────── */

button {
  cursor: pointer;
  border: none;
  border-radius: var(--radius);
  font-weight: 600;
  font-family: inherit;
  transition: opacity 0.15s, transform 0.1s, background 0.15s;
  user-select: none;
}

button:active { transform: scale(0.97); }
button:disabled { opacity: 0.4; cursor: not-allowed; }
button:disabled:active { transform: none; }

.btn-primary {
  background: var(--primary);
  color: #fff;
  padding: 20px 32px;
  font-size: 19px;
  font-weight: 600;
  border-radius: var(--radius-lg);
}

.btn-primary:hover:not(:disabled) { background: var(--primary-dark); }

.btn-large {
  width: 100%;
  padding: 20px;
  font-size: 19px;
  border-radius: var(--radius-lg);
}

/* Primary mid-screen CTA — the PIN Submit button and the confirm screen's
   "Clock into scheduled role" share this so the second tap of a clock-in
   lands in the same spot as the first (muscle memory). Sized to the 0.75×
   keypad column. Declared after .btn-large so it wins the cascade. */
.btn-cta {
  width: 100%;
  max-width: 270px;
  padding: 13px 20px;
  font-size: 22px; /* matches .confirm-shift-role so Submit / Clock in read as loud as the role bubbles */
  border-radius: var(--radius-lg);
}

/* Mobile timeclock zooms the whole layout to 0.75 — extra padding keeps the
   physical tap target ≥44px. */
html.mode-mobile .btn-cta {
  padding: 20px;
}

.btn-secondary {
  background: var(--bg-card);
  border: 2px solid var(--border);
  color: var(--text);
  font-size: 19px;
  font-weight: 600;
  padding: 20px;
  border-radius: var(--radius-lg);
}

.btn-danger {
  background: var(--error);
  color: #fff;
  padding: 20px 32px;
  font-size: 19px;
  font-weight: 600;
  border-radius: var(--radius-lg);
}

.btn-danger:hover:not(:disabled) {
  filter: brightness(1.1);
}

.btn-ghost {
  background: transparent;
  color: var(--text-muted);
  border: 1.5px solid var(--border);
  padding: 14px 28px;
  font-size: 15px;
  font-weight: 600;
  border-radius: var(--radius);
}

/* ── Glass system ────────────────────────────────────────────────────────── */
/* Compose alongside .card / .btn-primary / .btn-action / .numpad-key.
   Visual approximation of Figma "liquid glass": tint + backdrop-blur + saturate +
   inner border highlight + top sheen + drop shadow. Tune blur/sheen during QA. */

.glass {
  position: relative;
  isolation: isolate;
  background: var(--glass-tint);
  border: 1px solid var(--glass-border);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  border-radius: var(--radius-lg);
  box-shadow: var(--glass-shadow);
  overflow: hidden;
  transition: transform 100ms ease, background 150ms ease;
}

.glass::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(180deg, var(--glass-sheen-from) 0%, transparent 50%);
  border-radius: inherit;
  z-index: 1;
}

.glass > * {
  position: relative;
  z-index: 2;
}

.glass:active {
  background: var(--glass-tint-active);
  transform: scale(0.98);
}

.glass:disabled {
  opacity: 0.5;
}

.glass:disabled:active {
  transform: none;
}

.glass.glass-sm {
  border-radius: var(--radius-lg);
  backdrop-filter: blur(var(--glass-blur-sm)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur-sm)) saturate(var(--glass-saturate));
}

/* ── Location Header ─────────────────────────────────────────────────────── */
/* Used on screen 02 to show "JIM ELLIS FORD — VALET" near the top status-bar area. */

.location-header {
  font-size: 30px;
  color: var(--text-pure-white);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 2.4px; /* keeps the 0.08em ratio at the larger size */
  margin-bottom: 12px;
  text-align: center;
}

/* JS-populated location headers (Choose Role) collapse until the name is
   known — same convention as .screen-location-name:empty. The PIN screen's
   header wraps a <span>, so it's never :empty and is unaffected. */
.location-header:empty {
  display: none;
}

/* ── Hero / page titles inside screens ───────────────────────────────────── */

.punch-prompt {
  font-size: 32px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 24px;
}

/* ── PIN Display ─────────────────────────────────────────────────────────── */

/* Fixed-width row of 6 PIN slots, sized to match the numpad (270px) so the
   display never widens as digits are entered. Slots are evenly distributed;
   filled left-to-right as the employee types. margin: 0 auto matters — the
   Manager PIN overlay hosts this inside a non-flex .card. */
.pin-display {
  box-sizing: border-box;
  width: 100%;
  max-width: 270px;
  height: 60px;
  padding: 0 20px;
  margin: 0 auto 24px;
  display: flex;
  align-items: center;
  justify-content: space-evenly;
  color: var(--text);
  border-radius: var(--radius-lg);
  /* When .glass is applied, glass styles take over background/border. */
}

.pin-display:not(.glass) {
  background: var(--bg-card);
  border: 2px solid var(--border);
}

/* One PIN digit position. Empty = hollow ring, filled = solid dot. Fixed size
   so the row width is identical whether the PIN is empty or full. */
.pin-slot {
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid var(--text-muted);
  background: transparent;
  transition: background 80ms ease, border-color 80ms ease;
}

.pin-slot.filled {
  background: var(--text);
  border-color: var(--text);
}

/* Shake the PIN display when a partial PIN times out and is silently cleared
   (replaces the old "Still you?" text warning). */
.pin-display.shake {
  animation: shake 0.45s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}

@keyframes shake {
  10%, 90% { transform: translateX(-3px); }
  20%, 80% { transform: translateX(6px); }
  30%, 50%, 70% { transform: translateX(-10px); }
  40%, 60% { transform: translateX(10px); }
}

/* ── Number Pad ──────────────────────────────────────────────────────────── */

.numpad {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 9px;
  width: 100%;
  max-width: 270px;
  margin: 0 auto 16px; /* auto centers inside the Manager PIN overlay's card */
}

/* PIN Submit aligns with the numpad above via the shared .btn-cta sizing
   (both 270px max). */

.numpad-key {
  color: var(--text);
  font-size: 22px;
  font-weight: 600;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-lg);
  font-family: inherit;
  /* Governs the press-flash FADE-OUT when JS removes .key-flash. Declared after
     .glass so it overrides that rule's shorter transition for keypad keys.
     50ms = another 2x quicker release (Mike: still too slow for rapid input). */
  transition: transform 80ms ease, background 50ms ease;
}

/* Press flash — app.js adds .key-flash on pointerdown and removes it ~200ms
   later, so the highlight is clearly visible even on a fast tap (the native
   :active highlight is released the instant the finger lifts, which on a quick
   tap barely registers). The WHOLE button brightens (~50% more opaque than the
   active tint) with a slight scale — no outline ring — then fades out over the
   base transition above. */
.numpad-key.key-flash {
  background: var(--glass-tint-flash);
  transform: scale(0.97);
  /* Snap the highlight IN near-instantly; the fade-OUT is governed by the base
     .numpad-key transition above when JS removes the class. */
  transition: transform 40ms ease, background 40ms ease;
}

.numpad-key:not(.glass) {
  background: var(--bg-card);
  border: 1.5px solid var(--border);
  transition: background 0.1s, border-color 0.1s;
}

.numpad-key:not(.glass):active {
  background: var(--primary);
  border-color: var(--primary);
}

.numpad-clear {
  color: var(--error);
  font-size: 17px;
}

.numpad-back {
  color: var(--text-muted);
  font-size: 20px;
}

.numpad-clear.glass:active,
.numpad-back.glass:active {
  background: var(--glass-tint-active);
  transform: scale(0.98);
}

/* ── Clock In / Out Action Buttons (screen 03) ───────────────────────────── */
/* Secondary actions (Switch Role, Start Break) stack vertically in
   .btn-cta sizing; the primary action (Clock Out / End Break) lives
   OUTSIDE this stack, pinned to the PIN-Submit spot (see
   #confirm-actions-area below). */
.confirm-actions {
  display: flex;
  flex-direction: column;
  align-items: center; /* center the 270px .btn-cta buttons in the 320px column */
  gap: 16px;
  width: 100%;
  max-width: 320px;
  margin: 0 auto;
}

/* Scheduled-shift picker on the confirm screen — each shift is a glass
   card (role + time range); the time-appropriate one is preselected and
   the employee can tap another to switch. Positioning of the whole
   #confirm-preselect block lives in the #screen-confirm rule above. */
.confirm-shift-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: 100%;
  max-width: 320px;
  margin-bottom: 20px;
}

.confirm-shift-card {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 14px 20px;
  border-radius: var(--radius-lg);
  color: var(--text);
  font-family: inherit;
  text-align: center;
}

.confirm-shift-role {
  font-size: 22px;
  font-weight: 700;
  color: var(--text);
  line-height: 1.15;
}

.confirm-shift-time {
  font-size: 15px;
  font-weight: 500;
  color: var(--text-muted);
}

/* Selected shift stands apart — bright primary-accent glass (a crisp inset
   ring avoids the layout shift a border-width change would cause). */
.confirm-shift-card.selected {
  background: var(--selected-tint);
  box-shadow: inset 0 0 0 2px var(--primary), var(--glass-shadow);
}

.confirm-shift-card.selected .confirm-shift-time {
  color: var(--text);
}

/* Escape hatch under the fast-path CTA — quiet text button; padding keeps
   the physical target comfortably ≥44px. */
.btn-link-muted {
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 17px;
  font-weight: 500;
  padding: 12px 16px;
  margin-top: 12px;
}

.btn-link-muted:hover,
.btn-link-muted:focus {
  color: var(--text);
}

/* Secondary-action area on the clocked-in landing: same fixed height as
   the shift-picker area so the primary button below it (Clock Out /
   End Break) lands in the exact PIN-Submit spot. Secondary buttons
   (Switch Role, Start Break) rest at the bottom of the area, just above
   the primary, with the padding-bottom providing the separation. */
#confirm-actions-area {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  width: 100%;
  min-height: 298px;
  padding-bottom: 20px;
}

.confirm-actions .btn-cta {
  color: var(--text-pure-white);
}

.break-elapsed {
  margin-bottom: 16px;
  padding: 10px 16px;
  background: var(--pill-warn-bg);
  border: 1px solid var(--pill-warn-border);
  border-radius: var(--radius);
  font-size: 15px;
  color: var(--text);
  text-align: center;
  font-weight: 600;
}

/* Build 1.76: the one line under Clock in when the server closed a clock-in
   nobody clocked out of in time (`state_notice`). Quiet, under the CTA, so
   the button keeps its PIN-Submit resting spot; the Report-an-issue link is
   centred below whichever of the two is lower (_positionReportIssueButton). */
.confirm-state-notice {
  margin-top: 14px;
  max-width: 340px;
  font-size: 15px;
  line-height: 1.35;
  color: var(--text-muted);
  text-align: center;
}

/* ── Camera (screen 06) ──────────────────────────────────────────────────── */

.camera-content {
  padding: 0;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  height: calc(100vh - 48px);
  overflow: hidden;
}

.camera-header {
  padding: 12px 24px;
  text-align: center;
  font-size: 18px;
  color: var(--text-muted);
  flex-shrink: 0;
}

/* Build 1.64: the preview box owns the compositing layer, in every state.
   The only thing that used to force one was .screen-camera-standby's
   transform + opacity:0.02, and BOTH of those are undone by the .active
   toggle -- so the layer holding a live <video> was rebuilt on every punch.
   Here the promotion is state-independent: arriving at the photo screen now
   changes only opacity, z-index and pointer-events, and WebKit keeps the same
   layer (and the same media layer inside it) across the flip. It also pins
   the backdrop root for the .glass punch HUD inside the preview instead of
   letting it float up to #screen-camera, whose opacity 0.02 -> 1 moved it on
   every punch.

   Deliberately on .camera-wrap and NOT on #screen-camera: a transform makes
   an element the containing block for any position:fixed descendant, and
   #btn-camera-cancel (.btn-screen-back, position:fixed) is a child of the
   screen. It would land on the same pixels today but become clippable by the
   screen's own overflow -- a trap for whoever next changes this screen's
   height. .camera-wrap has no fixed descendants.

   The flex centring that used to live here is gone with it: every child is
   now out of flow, so it centred nothing, and leaving it would invite the
   next reader to put an in-flow child back into the arrangement that
   produced the ghost. Height is unaffected -- .camera-wrap is sized by
   flex: 1 from .camera-content, never by its contents. */
.camera-wrap {
  position: relative;
  flex: 1;
  min-height: 0;
  background: #000;
  overflow: hidden;
  transform: translateZ(0);
}

/* Builds 1.65/1.66: no permanent compositing layer under the LEAN launch
   profile. app.js sets `launch-lean` on <html> from the server's answer (or,
   before any heartbeat, the UA rule: below iPadOS 26 -- see the note by
   LEGACY_IOS_BELOW there): Germain Ford's iPadOS 18.7.8 kiosk came up as a
   pure WHITE page the morning after 1.64 shipped, which is WebKit no longer
   rendering the document at all -- and 1.64's promotion was only ever checked
   on iPadOS 26.6. A lean iPad also runs camera mode `off`, so the standby
   screen never exists and the preview is laid out exactly as it was before
   1.49. The positioned video and its z-index stack stay: they are layout,
   not layer churn, and they are what keeps the crop right on every iPad. */
html.launch-lean .camera-wrap {
  transform: none;
}

/* Build 1.64: the preview is POSITIONED, not a flex item. It had been the
   sole in-flow child of a centring container -- a replaced element with an
   intrinsic size well away from its box's (iOS hands back a 480x854 portrait
   stream for our 854x480 ideal, inside a box of about 768x856), at 100%/100%,
   transformed, with no position and no z-index -- and on iPadOS 26.6 WebKit
   painted a second, differently-fitted copy of it above the oval and outside
   the countdown veil. There is no flex path left to get wrong: one
   absolutely-positioned box, one explicit z-index, one stacking order (see
   the table below).

   The box itself was never wrong, which is the part that took prod data to
   establish: captureFrame derives its crop from the element's MEASURED rect,
   and every photo off both Genesis kiosks on 2026-09-21/22 came back 480x535
   -- a display aspect of 0.897, i.e. the full wrap. So the element was laid
   out correctly while the screen showed two of it. This rule is hardening,
   not the diagnosis; the layer churn above is.

   top/left + width/height rather than inset:0 + width/height: the latter is
   over-constrained and leans on the UA dropping right/bottom.

   scaleX(-1) deliberately STAYS on the video. Moving the mirror to a wrapper
   would reverse the countdown digit, the punch HUD and every button, needing
   five counter-mirrors -- five more compositing candidates sitting directly
   over a video layer, and a scale transform on an ANCESTOR of the media
   layer, which is closer to the suspected fault rather than further from it.
   The capture is unaffected either way: captureFrame draws with a plain
   drawImage and no flip, which is why the saved photo was always correct
   while the screen was not. */
#camera-video,
#camera-canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scaleX(-1);
  z-index: 0;
}

/* Build 1.64: every positioned child of .camera-wrap now declares its z-index,
   so the paint order is stated rather than inferred from DOM order. It
   reproduces what shipped before, low to high:
     0  #camera-video / #camera-canvas   the preview, and the post-capture still
     1  .face-overlay                    the dashed oval
     2  .camera-countdown                the dark veil and the number
     3  .camera-punch-info               "Clocking in to <role>"
     4  .camera-flash                    the white shutter
     5  #btn-camera-take-photo           the manual override
     6  .camera-bringup                  "Starting camera..."
   One deliberate change: the shutter flash now covers the punch HUD for its
   450ms instead of passing behind it. A shutter should white out the whole
   preview. .camera-wrap is a stacking context now, so these numbers are
   scoped to it and can no longer compete with #btn-camera-cancel (z-index 30,
   fixed, outside the wrap), which still paints above all of them. */
.face-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding-top: 12%;
  pointer-events: none;
}

.face-overlay svg {
  width: 70%;
  max-width: 320px;
  height: auto;
}

.camera-countdown {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: rgba(0,0,0,0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 8rem;
  font-weight: 900;
  color: #fff;
  text-shadow: 0 0 40px rgba(59, 130, 246, 0.8);
}

/* Punch-summary HUD, top-right over the live camera preview (clock-in only). */
.camera-punch-info {
  position: absolute;
  top: 16px;
  right: 16px;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
  padding: 12px 16px;
  min-width: 150px;
  text-align: right;
  border-radius: var(--radius-lg);
}

.camera-punch-label {
  font-size: 13px;
  font-weight: 500;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.6px;
}

.camera-punch-role {
  font-size: 22px;
  font-weight: 700;
  color: var(--text-pure-white);
  line-height: 1.15;
}

.camera-punch-time {
  font-size: 16px;
  font-weight: 500;
  color: var(--text);
}

.camera-flash {
  position: absolute;
  inset: 0;
  z-index: 4;
  background: #fff;
  opacity: 0;
  pointer-events: none;
}

.camera-flash.flash {
  animation: shutter 0.45s ease-out forwards;
}

@keyframes shutter {
  0%   { opacity: 0.95; }
  100% { opacity: 0; }
}

.camera-status {
  text-align: center;
  padding: 12px 24px;
  font-size: 16px;
  color: var(--text-muted);
  min-height: 48px;
  flex-shrink: 0;
}

/* Manual "Take Photo" override — overlaid on the bottom-center of the live
   preview so it's always visible and tappable regardless of flex layout (it
   previously sat below a flex:1 video and got clipped off-screen on iPad).
   Sits above the face-overlay (pointer-events:none) AND above the countdown
   veil -- z-index 5 has always beaten the veil's auto, so the comment here
   claimed the opposite of what shipped (corrected build 1.64). Above is the
   right answer: the button is hidden the instant a capture commits, so the
   only moment both are up is a countdown the employee may still want to
   interrupt. */
#btn-camera-take-photo {
  position: absolute;
  /* ~2/3 of the way down between the face oval and the bottom of the preview,
     so the button isn't glued to the very bottom edge (Mike). */
  bottom: 15%;
  left: 50%;
  transform: translateX(-50%);
  width: auto;
  min-width: 200px;
  z-index: 5;
  pointer-events: auto;
}
#btn-camera-take-photo:active { transform: translateX(-50%) scale(0.97); }

#btn-camera-cancel {
  flex-shrink: 0;
  margin: 8px 24px 16px;
  align-self: center;
}

/* ── Questionnaire (screen 07) ───────────────────────────────────────────── */

.questions-wrap {
  display: flex;
  flex-direction: column;
  gap: 28px;
  margin-bottom: 32px;
  width: 100%;
}

.question-group label.question-text {
  font-size: 18px;
  font-weight: 600;
  color: var(--text);
  display: block;
  margin-bottom: 12px;
  text-transform: none;
  letter-spacing: 0;
  text-align: center;
}

.yes-no-buttons {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.btn-yn {
  height: 60px;
  font-size: 17px;
  font-weight: 600;
  background: var(--bg-card);
  border: 2px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  font-family: inherit;
}

.btn-yn.selected-yes { background: var(--success); border-color: var(--success); color: #fff; }
.btn-yn.selected-no  { background: var(--error);   border-color: var(--error);   color: #fff; }

.rating-buttons {
  display: flex;
  gap: 10px;
}

.btn-rating {
  flex: 1;
  height: 60px;
  font-size: 17px;
  font-weight: 600;
  background: var(--bg-card);
  border: 2px solid var(--border);
  color: var(--text);
  border-radius: var(--radius);
  font-family: inherit;
}

.btn-rating.selected {
  background: var(--primary);
  border-color: var(--primary);
  color: #fff;
}

.rating-scale-help {
  margin-top: 12px;
  font-size: 14px;
  font-weight: 400;
  color: var(--text-muted);
  text-align: center;
}

/* ── Complex closing-report fields (hospitality) ───────────────────────── */

.question-section {
  font-size: 15px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--primary);
  border-bottom: 2px solid var(--border);
  padding-bottom: 8px;
  margin: 8px 0 4px;
  width: 100%;
  text-align: left;
}

.question-help {
  margin-top: 8px;
  font-size: 13px;
  color: var(--text-muted);
  text-align: center;
}

/* Question pages (build 1.32): one section per page; the active page is the
   only one shown — visibility is driven by the shared .hidden utility. */
.question-page {
  display: flex;
  flex-direction: column;
  gap: 18px;
  width: 100%;
}

/* Conditional-comment companion (build 1.32): revealed under its question
   when the answer matches comment_required_if. */
.question-comment {
  margin-top: 12px;
  padding: 10px 12px;
  border-left: 3px solid var(--primary);
  background: rgba(0, 0, 0, 0.03);
  border-radius: 6px;
  text-align: left;
}

.question-comment textarea {
  width: 100%;
  margin-top: 6px;
}

/* Live "Total cars parked: N" line under cars-parked / multi-lot inputs. */
.question-total {
  margin-top: 10px;
  font-size: 15px;
  font-weight: 700;
  color: var(--primary);
  text-align: left;
}

/* Checkbox acknowledgment (custom clock-out reminders etc.). */
.btn-checkbox {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 14px 16px;
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  background: var(--surface);
  border: 2px solid var(--border);
  border-radius: 12px;
  text-align: left;
  cursor: pointer;
}

.btn-checkbox .checkbox-mark {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
  border: 2px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
}

.btn-checkbox.checked {
  background: var(--primary);
  border-color: var(--primary);
  color: #fff;
}

.btn-checkbox.checked .checkbox-mark {
  background: #fff;
  border-color: #fff;
  /* Navy check drawn via an inline gradient-free glyph. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23001F5B' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
  background-size: 16px 16px;
  background-position: center;
  background-repeat: no-repeat;
}

/* Reminder question (7/28) — builder-authored acknowledgment. Amber callout
   card wrapping the standard tap-to-acknowledge control so it reads as a
   reminder, not a data question. */
.question-reminder {
  padding: 12px 14px;
  background: #FFFBEB; /* amber-50 */
  border: 1px solid #F59E0B; /* amber-500 */
  border-radius: 12px;
}

.question-reminder .question-text {
  color: #92400E; /* amber-800 */
}

.currency-input {
  display: flex;
  align-items: center;
  gap: 8px;
}

.currency-input .currency-prefix {
  font-size: 20px;
  font-weight: 600;
  color: var(--text-muted);
}

.currency-input input { flex: 1; }

.distribution-grid {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.distribution-row {
  display: grid;
  grid-template-columns: 1fr 110px;
  align-items: center;
  gap: 12px;
}

.distribution-label {
  font-size: 16px;
  font-weight: 500;
  color: var(--text);
  text-align: left;
  text-transform: none;
  letter-spacing: 0;
}

.question-group.field-error label.question-text { color: var(--error); }

.question-group.field-error input,
.question-group.field-error select,
.question-group.field-error textarea {
  border-color: var(--error);
}

/* Inline validation message under a question (tip-pool check, Build 1.30). */
.question-error {
  margin: 6px 0 0;
  color: var(--error);
  font-size: 14px;
  text-align: left;
}

/* ── Tips (screen 08) ────────────────────────────────────────────────────── */

.tip-fields {
  margin-bottom: 32px;
  width: 100%;
}

/* Tips-screen pool validation error (Build 1.30). */
.tips-error {
  margin: -16px 0 16px;
  color: var(--error);
  font-size: 15px;
}

.tip-fields .field label {
  text-align: center;
}

.divider {
  font-size: 14px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 1.12px;
  color: var(--text-muted);
  padding: 16px 0 12px;
  border-top: 1px solid var(--border);
  margin: 8px 0 16px;
  text-align: center;
}

/* ── Form Actions ────────────────────────────────────────────────────────── */

.form-actions {
  padding-bottom: 32px;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* ── Success Screen (screen 09) ──────────────────────────────────────────── */

.success-icon {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  background: var(--success);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 56px;
  font-weight: 900;
  color: #fff;
  margin-bottom: 24px;
  animation: pop 0.3s ease;
}

@keyframes pop {
  0%   { transform: scale(0.5); opacity: 0; }
  80%  { transform: scale(1.1); }
  100% { transform: scale(1);   opacity: 1; }
}

.success-name    { font-size: 26px; font-weight: 600; color: var(--text); margin-bottom: 24px; }
.success-time    { font-size: 22px; font-weight: 400; color: var(--text-muted); margin-bottom: 24px; }
.success-hours   { font-size: 16px; font-weight: 400; color: var(--success); }
/* Tagline matches the "Enter your PIN" prompt (32px/700, --text) per the
   confirm-page guidelines. */
.success-tagline { font-size: 32px; font-weight: 700; color: var(--text); margin-top: 24px; }

/* Breathing room on the Clocked-In/Out confirmation: a clear gap below the
   checkmark + action name before the details, and between each detail line
   (Mike, widened again with the fast clock-in redesign). */
#success-action { margin-bottom: 24px; }
#screen-success .employee-role-label { margin-top: 24px; }
#screen-success .employee-role-display { margin-bottom: 24px; }

.offline-note {
  margin-top: 20px;
  font-size: 15px;
  font-weight: 400;
  color: var(--warning);
  padding: 10px 16px;
  background: var(--pill-warn-bg-soft);
  border-radius: var(--radius);
  border: 1px solid var(--pill-warn-border-soft);
}

/* ── Error Screen (screen 10) ────────────────────────────────────────────── */

.error-icon {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  background: var(--error);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 56px;
  font-weight: 900;
  color: #fff;
  margin-bottom: 24px;
}

/* The error screen's message honours line breaks: the phone's "Camera
   permission needed" message (build 1.55) is three short paragraphs, set as
   textContent. Every other message is a single line and is unaffected. */
#error-message {
  white-space: pre-line;
  /* A two-line sentence sat flush on the button below it — visible the moment
     the geofence refusals (1.58) made multi-line messages the common case. */
  margin-bottom: 20px;
}

/* ── Lockout + Error Messages ────────────────────────────────────────────── */

.error-msg {
  color: var(--error);
  font-size: 15px;
  margin-top: 12px;
  padding: 12px 16px;
  background: rgba(239, 68, 68, 0.10);
  border-radius: var(--radius);
  border: 1px solid rgba(239, 68, 68, 0.30);
  transition: opacity 0.5s ease;
}

/* Auto-dismiss fade — app.js adds .fade-out ~5s after showing a PIN error so a
   stale "not recognized" message clears itself instead of lingering until the
   next action (Mike). */
.error-msg.fade-out {
  opacity: 0;
}

/* Inline "Retry" control surfaced when the Google Identity Services library
   fails to load (e.g. the kiosk booted during a network outage). */
.gsi-retry-btn {
  margin-left: 4px;
  padding: 4px 14px;
  font: inherit;
  font-weight: 600;
  color: #fff;
  background: var(--error);
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
  min-height: 36px;
}
.gsi-retry-btn:hover { filter: brightness(1.08); }
.gsi-retry-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

.lockout-msg {
  color: var(--warning);
  font-size: 15px;
  margin-top: 12px;
  padding: 12px 16px;
  background: var(--pill-warn-bg-soft);
  border-radius: var(--radius);
  border: 1px solid var(--pill-warn-border-soft);
  font-weight: 600;
}

/* ── Select Shift / Choose Role (screens 04, 05) ─────────────────────────── */

.role-banner {
  font-size: 24px;
  font-weight: 700;
  color: var(--text);
  text-align: center;
  padding: 20px 16px;
  margin-bottom: 20px;
  background: var(--bg-card);
  border: 2px solid var(--primary);
  border-radius: var(--radius);
  min-height: 70px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.role-banner.empty {
  color: var(--text-muted);
  font-weight: 500;
  font-size: 17px;
  border-color: var(--border);
  border-style: dashed;
}

.shift-list,
.role-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-height: 48vh;
  overflow-y: auto;
  margin-bottom: 24px;
  padding: 4px;
  width: 100%;
}

.shift-row,
.role-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 18px 20px;
  background: var(--bg-card);
  border: 2px solid var(--border);
  border-radius: var(--radius);
  font-size: 17px;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
}

/* Choose Role rows: stack name centered; the optional "Tipped" pill
   sits below the name instead of being pushed to the right edge. */
.role-row {
  flex-direction: column;
  justify-content: center;
  text-align: center;
  gap: 4px;
}

.shift-row.selected,
.role-row.selected {
  border-color: var(--primary);
  background: var(--selected-tint);
}

.shift-row.matches-selected {
  border-color: var(--primary);
  background: rgba(59, 130, 246, 0.04);
}

.shift-row .shift-role,
.role-row .shift-role {
  font-weight: 700;
}

.shift-row .shift-times,
.role-row .role-meta {
  color: var(--text-muted);
  font-size: 15px;
  font-weight: 400;
  margin-left: 16px;
  white-space: nowrap;
}

/* Reset the left-shift used in the side-by-side .shift-row layout —
   in the centered column layout used by .role-row, the meta should
   sit centered under the name without offset. */
.role-row .role-meta {
  margin-left: 0;
}

.empty-note {
  color: var(--text-muted);
  font-style: italic;
  text-align: center;
  padding: 24px 0;
}

/* ── Report Issue (screens 11–14) ────────────────────────────────────────── */

.btn-report-issue {
  font-size: 24px;
  font-weight: 400;
  color: var(--text-muted);
  background: transparent;
  border: none;
  padding: 8px 16px;
}

/* Pinned to the very bottom, out of the flex flow, so it can't push the
   footer's "Clock in" button up (which would break its alignment with the
   PIN Submit button). Also satisfies "move report an issue lower". */
#screen-confirm .btn-report-issue {
  position: absolute;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  /* Wrap instead of extending under the bottom-right Back/Exit pill on
     narrow (mobile) viewports. */
  max-width: calc(100vw - 220px);
}

/* Phone (build 1.56, Mike's iPhone): at 24px the link wrapped onto three
   ragged lines beside the Exit pill. It takes the size of the "No scheduled
   shifts found" note (the body's 18px), stays on ONE line, and sits at an even
   distance between the main button above it and the Exit pill below it.

   The size and the single line are CSS. The even distance is NOT: under
   `zoom: 0.75` the confirm column floats with slack above and below it, so no
   share-the-free-space rule can see where the Exit pill really is (tried, and
   measured 30px above vs 131px below). app.js measures the two buttons and
   shifts the link with --report-link-shift (see reportLinkShift). The link
   stays absolutely positioned, so nothing else on the screen moves. Kiosk is
   untouched. */
html.mode-mobile #screen-confirm .btn-report-issue {
  max-width: none;
  white-space: nowrap;
  font-size: 18px;
  transform: translateX(-50%) translateY(var(--report-link-shift, 0px));
  /* NO transform transition (build 1.57). Every <button> animates `transform`
     over 0.1s, and app.js measures this link to place it: a measurement taken
     while the last placement is still animating reads a position the link is
     only passing through. That is what put it on top of the Exit pill on every
     second placement in 1.56. The other two transitions are the base rule's. */
  transition: opacity 0.15s, background 0.15s;
}

/* The base `button:active { transform: scale(0.97) }` would REPLACE the
   placement above for as long as the link is pressed, throwing it half its
   width to the right and back down to the Exit pill under the thumb. Keep the
   placement and add the press scale to it. */
html.mode-mobile #screen-confirm .btn-report-issue:active {
  transform: translateX(-50%) translateY(var(--report-link-shift, 0px)) scale(0.97);
}

.report-reason-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin: 12px 0 24px;
  width: 100%;
}

.report-reason-btn {
  width: 100%;
  text-align: center;
  padding: 18px 20px;
  background: var(--bg-card);
  border: 2px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-size: 18px;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
  font-family: inherit;
}

.report-reason-btn:hover,
.report-reason-btn:focus {
  border-color: var(--primary);
  background: rgba(59, 130, 246, 0.05);
}

.report-reason-btn.selected {
  border-color: var(--primary);
  background: var(--selected-tint);
}

.report-date-wrap {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 24px 0;
  width: 100%;
}

.report-date-wrap label {
  font-size: 14px;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.7px;
}

.report-date-input {
  font-size: 22px;
  font-weight: 400;
  padding: 16px 18px;
  border: 2px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-card);
  color: var(--text);
  width: 100%;
  min-height: 72px;
  font-family: inherit;
  -webkit-appearance: none;
}

.report-review-summary {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 16px 20px;
  margin-bottom: 20px;
  font-size: 17px;
  color: var(--text);
  width: 100%;
  line-height: 1.5;
}

/* Each row is a <div> containing a <span class="label">…</span><br>value */
.report-review-summary > div {
  margin-top: 10px;
}

.report-review-summary > div:first-child {
  margin-top: 0;
}

.report-review-summary .label {
  display: inline-block;
  margin-bottom: 4px;
  color: var(--text-muted);
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.65px;
  font-weight: 600;
}

.hint-inline {
  color: var(--text-muted);
  font-weight: 400;
}

/* Centered "EXTRA NOTES (OPTIONAL)" header on review screen. */
#screen-report-review .field > label {
  text-align: center;
}

/* ── Utilities ───────────────────────────────────────────────────────────── */

.hidden { display: none !important; }

/* ── Camera bring-up overlay ───────────────────────────────────────────────
   Covers the preview and the dashed oval while a camera start is genuinely
   slow. app.js reveals it after BRINGUP_SPINNER_GRACE_MS and hides it in the
   bring-up's finally, so an ordinary warm punch never sees it. */
.camera-bringup {
  position: absolute;
  inset: 0;
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding: 0 24px;
  background: rgba(0, 0, 0, 0.72);
  color: #fff;
  text-align: center;
}

.camera-bringup.hidden {
  display: none;
}

/* Phone, while the iOS "Allow camera?" sheet is up (build 1.56): the sheet sits
   over the middle of the screen, so the spinner and the "Tap Allow…" line drop
   into the bottom third where they stay readable around it. app.js toggles the
   class; the kiosk never sets it. */
.camera-bringup.camera-bringup--awaiting-grant {
  justify-content: flex-end;
  padding-bottom: 14%;
}

.camera-bringup--awaiting-grant .camera-bringup-text {
  font-size: 24px;
  font-weight: 500;
}

.camera-bringup-spinner {
  width: 56px;
  height: 56px;
  border: 5px solid rgba(255, 255, 255, 0.25);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.9s linear infinite;
}

.camera-bringup-text {
  font-size: 20px;
  letter-spacing: 0.3px;
}

/* ── PIN-screen camera banner ──────────────────────────────────────────────
   Shown while the camera grant is known to be missing and no stream is live
   (app.js pinCameraBannerVisible). A tap opens the Enable Camera screen. */
/* Launch hold (build 1.50): the PIN pad is inert for the few seconds the
   launch camera check runs, so the permission sheet lands here and not inside
   a punch. Build 1.52 (Mike, after testing 1.51 on the prod iPad): the pad is
   BLURRED, not merely dimmed, and the note is twice the size -- "that will
   make it super obvious whats happening and why keypad disabled during". The
   keys stay in place under the blur (a blank pad reads as broken). */
#screen-pin.launch-hold .numpad-key,
#screen-pin.launch-hold #btn-submit-pin {
  pointer-events: none;
}
#screen-pin.launch-hold .numpad,
#screen-pin.launch-hold #btn-submit-pin,
#screen-pin.launch-hold #pin-display {
  filter: blur(4px);
  opacity: 0.55;
  transition: filter 150ms ease, opacity 150ms ease;
}
.pin-launch-hold {
  margin-top: 16px;
  font-size: 28px;
  font-weight: 600;
  text-align: center;
  color: rgba(255, 255, 255, 0.92);
}
/* A spinner ahead of the text (build 1.51, Mike: a loading spinner that
   blocks PIN entry until the camera is proven ready). Scaled with the note. */
.pin-launch-hold::before {
  content: '';
  display: inline-block;
  width: 24px;
  height: 24px;
  margin-right: 12px;
  vertical-align: -3px;
  border: 3px solid rgba(255, 255, 255, 0.35);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.9s linear infinite;
}

/* Phone drift warning (build 1.59). Fixed just under the 48px status bar so
   the questions/tips layout is untouched; same amber as the PIN camera banner. */
#mobile-range-warning {
  position: fixed;
  top: 58px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 150;
  width: min(92vw, 640px);
  padding: 12px 18px;
  border: 1px solid rgba(251, 191, 36, 0.8);
  border-radius: 12px;
  background: rgba(60, 42, 4, 0.96);
  color: #fde68a;
  font-size: 18px;
  font-weight: 600;
  line-height: 1.35;
  text-align: center;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
}

.pin-camera-banner {
  display: block;
  width: min(92vw, 520px);
  margin: 0 auto 14px;
  padding: 12px 18px;
  border: 1px solid rgba(251, 191, 36, 0.7);
  border-radius: 12px;
  background: rgba(251, 191, 36, 0.14);
  color: #fde68a;
  font-size: 17px;
  font-weight: 600;
  text-align: center;
  cursor: pointer;
}

.pin-camera-banner.hidden {
  display: none;
}

/* ── PIN-screen offline banner (build 1.74) ─────────────────────────────
   "Connect to Wi-Fi - this device has been offline X hours": yellow from
   4 hours, red from 8 (app.js offlineBannerState). Not a button: the fix is
   in the iPad's own Settings, which the kiosk cannot open. */
.pin-offline-banner {
  display: block;
  width: min(92vw, 520px);
  margin: 0 auto 14px;
  padding: 12px 18px;
  border: 1px solid rgba(251, 191, 36, 0.7);
  border-radius: 12px;
  background: rgba(251, 191, 36, 0.14);
  color: #fde68a;
  font-size: 17px;
  font-weight: 600;
  text-align: center;
}

.pin-offline-banner.red {
  border-color: rgba(248, 113, 113, 0.8);
  background: rgba(248, 113, 113, 0.18);
  color: #fecaca;
}

.pin-offline-banner.hidden {
  display: none;
}
