:root{
  --blue:#1F7CAD;--blue-mid:#2A8EC2;--blue-light:#D6EEF7;--blue-xlight:#EBF6FB;
  --green:#5DB520;--green-mid:#6DC929;--green-light:#E5F5D0;
  --amber:#E8960F;--amber-light:#FEF3D8;
  --red:#D63B3B;--red-light:#FDEAEA;
  --purple:#7C5CBF;--purple-light:#EDE8F8;
  --bg:#0f1117;--bg2:#1a1e2a;--bg3:#232838;--bg4:#2d3348;
  --card:#1e2330;--card2:#252c3d;
  --text:#f0f4ff;--text2:#8892a4;--text3:#5c6478;
  --border:rgba(255,255,255,.08);
  --font:'DM Sans',sans-serif;--mono:'IBM Plex Mono',monospace;
  --r:12px;
  --safe-top:env(safe-area-inset-top,0px);
  --safe-bottom:env(safe-area-inset-bottom,16px);
}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent;}
/* `100%` is the conservative-correct height for html/body — matches the
   viewport in nearly every browser regardless of chrome state. The .app
   uses `100dvh` (with `100vh` fallback) to track the *dynamic* visible
   viewport, which excludes iOS Safari's collapsible address bar. Earlier
   PR #224 set both html/body AND .app to dvh, which caused mismatched
   stacking contexts on iPhone in-browser mode. */
html,body{width:100%;height:100%;background:var(--bg);color:var(--text);font-family:var(--font);font-size:15px;overflow:hidden;}
/* body is a flex COLUMN so anything injected as a sibling of .app shares the
   viewport height instead of pushing .app down.
   Specifically: config.js injects the DEV/LOCAL environment banner as
   document.body.firstChild — OUTSIDE .app. While .app was a hard 100dvh, banner
   height + 100dvh exceeded the viewport and body's `overflow:hidden` clipped the
   difference off the BOTTOM, taking the bottom nav with it. That is why the menu
   bar was off-screen on dev (and only on dev — prod injects no banner).
   .app now flexes to whatever height is left, with or without a banner. */
body{display:flex;flex-direction:column;}
.app{width:100%;flex:1 1 auto;min-height:0;display:flex;flex-direction:column;max-width:430px;margin:0 auto;position:relative;}

/* STATUS BAR AREA */
/* `padding-top` reserves space for the iOS notch / dynamic island in
   standalone PWA mode. Without this, the LIVE chip + clock are hidden
   under the system status bar and the whole layout shifts up ~47px,
   which pushes the bottom of `.scroll` behind the bottom nav. */
.status-bar{background:var(--bg);padding:calc(12px + var(--safe-top)) 20px 0;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;}
/* When config.js has injected the DEV/LOCAL banner above .app, THAT element
   carries the notch inset (see portal/config.js). Without this the inset would be
   counted twice and dev would get ~68px of dead space above the greeting. */
#ecm-env-banner + .app .status-bar{padding-top:12px;}
.sb-time{font-size:13px;font-weight:600;color:var(--text2);font-family:var(--mono);}
.sb-right{display:flex;align-items:center;gap:8px;}
.sb-live{display:flex;align-items:center;gap:5px;font-size:11px;font-weight:600;color:var(--green);}
.sb-live-dot{width:7px;height:7px;border-radius:50%;background:var(--green);animation:pulse 2s ease infinite;}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.6;transform:scale(1.2)}}

/* HEADER */
.header{padding:16px 20px 12px;flex-shrink:0;}
.header-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:2px;}
.greeting{font-size:22px;font-weight:700;color:var(--text);}
.header-sub{font-size:13px;color:var(--text2);}
.avatar-btn{width:42px;height:42px;border-radius:50%;background:linear-gradient(135deg,var(--blue),var(--purple));display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;color:#fff;border:none;cursor:pointer;flex-shrink:0;}

/* SCROLL AREA */
/* `padding-top` gives the today-card's rounded top corners visible
   breathing room from the page header. Without it, iPhone Safari/Edge
   in browser mode renders the top corners as if clipped — likely an
   interaction between the today-card's `position:relative;
   overflow:hidden` and its absolutely-positioned `::before` pseudo-element
   at `top:-30px`. The explicit gap also makes the visual hierarchy
   cleaner regardless of the browser quirk. */
.scroll{flex:1;overflow-y:auto;overflow-x:hidden;padding:12px 16px 0;-webkit-overflow-scrolling:touch;min-height:0;}
.scroll::-webkit-scrollbar{display:none;}

/* TODAY CARD */
/* `isolation:isolate` creates a new stacking context so the
   rounded-corner clipping of the decorative `::before` / `::after`
   pseudo-elements behaves consistently across iOS Safari/Edge. Without
   it, those engines had been mis-painting the today-card's rounded TOP
   corners (visible as a "clipped top edge") in browser mode. */
.today-card{background:linear-gradient(135deg,var(--blue) 0%,#1a5f8a 100%);border-radius:20px;padding:20px;margin-bottom:14px;margin-top:0;position:relative;overflow:hidden;isolation:isolate;flex-shrink:0;}
.today-card::before{content:'';position:absolute;top:-30px;right:-30px;width:120px;height:120px;border-radius:50%;background:rgba(255,255,255,.06);z-index:-1;}
/* The today-card's "Get Directions" button. Was an inline style attribute plus
   an onmouseover/onmouseout pair that assigned this.style.background directly.
   The hover belongs in CSS, not in two JS handlers — and on a touch device
   those two never fired at all, so this is also the first time the transition
   is reachable for the field fleet. */
.today-dir-btn{width:100%;margin-top:12px;padding:11px;border-radius:10px;border:none;background:rgba(255,255,255,.15);color:#fff;font-size:13px;font-weight:600;font-family:var(--font);cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:background .15s;}
.today-dir-btn:hover{background:rgba(255,255,255,.22);}
.today-dir-btn:active{background:rgba(255,255,255,.26);}
.today-card::after{content:'';position:absolute;bottom:-20px;left:20px;width:80px;height:80px;border-radius:50%;background:rgba(255,255,255,.04);z-index:-1;}
.tc-badge{display:inline-flex;align-items:center;gap:5px;background:rgba(255,255,255,.15);border-radius:20px;padding:3px 10px;font-size:11px;font-weight:600;color:rgba(255,255,255,.9);margin-bottom:10px;}
.tc-location{font-size:20px;font-weight:700;color:#fff;margin-bottom:4px;line-height:1.2;}
.tc-sub{font-size:13px;color:rgba(255,255,255,.7);margin-bottom:14px;}
.tc-row{display:flex;gap:10px;}
.tc-chip{background:rgba(255,255,255,.12);border-radius:8px;padding:8px 12px;flex:1;text-align:center;}
.tc-chip-val{font-size:14px;font-weight:700;color:#fff;font-family:var(--mono);}
.tc-chip-lbl{font-size:10px;color:rgba(255,255,255,.6);margin-top:1px;}

/* PUNCH BUTTON */
.punch-wrap{margin-bottom:14px;}
.punch-btn{width:100%;border-radius:16px;border:none;padding:18px;font-size:18px;font-weight:700;cursor:pointer;font-family:var(--font);display:flex;align-items:center;justify-content:center;gap:10px;transition:all .2s;position:relative;overflow:hidden;}
.punch-btn.punch-in{background:linear-gradient(135deg,var(--green),#3d8c15);color:#fff;box-shadow:0 8px 24px rgba(93,181,32,.35);}
.punch-btn.punch-out{background:linear-gradient(135deg,var(--red),#a82a2a);color:#fff;box-shadow:0 8px 24px rgba(214,59,59,.35);}
.punch-btn.punched{background:var(--card);color:var(--text2);box-shadow:none;cursor:default;}
/* Boot state, before restorePunchState() has answered. Deliberately inert-looking
   — no gradient, no shadow, no green — so it never reads as "ready, tap me"
   while the app still doesn't know whether this worker is on an open shift. */
.punch-btn.punch-loading{background:var(--card);color:var(--text2);box-shadow:none;cursor:default;opacity:.6;}
.punch-btn:active{transform:scale(.97);}
/* .punch-btn:active is unconditional above; without this the disabled boot
   button still depresses on touch, which reads as an accepted tap. */
.punch-btn:disabled{cursor:default;}
.punch-btn:disabled:active{transform:none;}
.punch-icon{font-size:22px;}
.punch-time{font-size:12px;font-weight:500;opacity:.8;font-family:var(--mono);}

/* GPS STATUS */
.gps-status{display:flex;align-items:center;gap:8px;padding:10px 14px;border-radius:10px;margin-bottom:14px;font-size:12px;font-weight:500;}
.gps-status.ok{background:rgba(93,181,32,.12);border:1px solid rgba(93,181,32,.2);color:var(--green);}
.gps-status.warn{background:rgba(232,150,15,.12);border:1px solid rgba(232,150,15,.2);color:var(--amber);}
.gps-status.err{background:rgba(214,59,59,.12);border:1px solid rgba(214,59,59,.2);color:var(--red);}
.gps-icon{font-size:16px;}

/* SECTION HEADER */
.sec-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;margin-top:4px;}
.sec-title{font-size:13px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.06em;}
.sec-link{font-size:12px;color:var(--blue-mid);font-weight:600;}

/* WEEK STRIP */
.week-strip{display:flex;gap:6px;margin-bottom:16px;overflow-x:auto;padding-bottom:4px;}
.week-strip::-webkit-scrollbar{display:none;}
.day-pill{flex-shrink:0;width:52px;border-radius:12px;padding:10px 6px;text-align:center;cursor:pointer;transition:all .15s;border:1.5px solid transparent;}
.day-pill.working{background:rgba(93,181,32,.12);border-color:rgba(93,181,32,.3);}
.day-pill.float{background:rgba(124,92,191,.12);border-color:rgba(124,92,191,.3);}
.day-pill.pto{background:rgba(8,145,178,.12);border-color:rgba(8,145,178,.3);}
.day-pill.off{background:var(--card);border-color:var(--border);}
.day-pill.today{box-shadow:0 0 0 2px var(--blue);}
.day-pill.selected{transform:scale(1.05);}
.dp-name{font-size:10px;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:.04em;}
.dp-date{font-size:16px;font-weight:700;color:var(--text);margin:2px 0;}
.day-pill.working .dp-date{color:var(--green);}
.day-pill.float .dp-date{color:var(--purple);}
.day-pill.pto .dp-date{color:#0891B2;}
.day-pill.off .dp-date{color:var(--text3);}
.dp-dot{width:6px;height:6px;border-radius:50%;margin:0 auto;}
.day-detail-card{background:var(--card);border-radius:16px;padding:16px;border:1px solid var(--border);animation:slideDown .2s ease;}
@keyframes slideDown{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
.ddc-header{display:flex;align-items:center;gap:12px;margin-bottom:12px;}
.ddc-av{width:44px;height:44px;border-radius:10px;display:flex;flex-direction:column;align-items:center;justify-content:center;flex-shrink:0;}
.ddc-title{font-size:16px;font-weight:700;color:var(--text);}
.ddc-sub{font-size:12px;color:var(--text2);margin-top:1px;}
.ddc-rows{display:flex;flex-direction:column;gap:8px;}
.ddc-row{display:flex;align-items:center;gap:10px;font-size:13px;}
.ddc-lbl{color:var(--text2);min-width:80px;font-size:12px;}
.ddc-val{color:var(--text);font-weight:500;}
.ddc-dir-btn{width:100%;margin-top:12px;padding:11px;border-radius:10px;border:none;background:rgba(31,124,173,.2);color:var(--blue-mid);font-size:13px;font-weight:600;font-family:var(--font);cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;}

/* QUICK CARDS */
.quick-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px;}
.quick-card{background:var(--card);border-radius:14px;padding:14px;border:1px solid var(--border);cursor:pointer;transition:all .15s;active:scale(.97);}
.quick-card:active{transform:scale(.97);}
.qc-icon{font-size:24px;margin-bottom:8px;}
.qc-title{font-size:13px;font-weight:600;color:var(--text);margin-bottom:2px;}
.qc-sub{font-size:11px;color:var(--text2);}

/* CONTACTS */
.contact-card{background:var(--card);border-radius:14px;padding:14px 16px;border:1px solid var(--border);display:flex;align-items:center;gap:12px;margin-bottom:8px;cursor:pointer;transition:all .15s;}
.contact-card:active{transform:scale(.98);}
.cc-av{width:42px;height:42px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:#fff;flex-shrink:0;}
.cc-info{flex:1;}
.cc-name{font-size:14px;font-weight:600;color:var(--text);}
.cc-role{font-size:11px;color:var(--text2);}
.cc-actions{display:flex;gap:8px;}
.cc-btn{width:36px;height:36px;border-radius:50%;border:none;display:flex;align-items:center;justify-content:center;font-size:16px;cursor:pointer;transition:all .15s;}
.cc-btn:active{transform:scale(.9);}
.cc-call{background:rgba(93,181,32,.15);color:var(--green);}
.cc-msg{background:rgba(31,124,173,.15);color:var(--blue-mid);}

/* BOTTOM NAV */
.bottom-nav{background:var(--bg2);border-top:1px solid var(--border);display:flex;padding:8px 0;padding-bottom:calc(8px + var(--safe-bottom));flex-shrink:0;}
.bn-item{flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;cursor:pointer;padding:6px 4px;border:none;background:none;border-radius:12px;transition:all .15s;margin:4px 3px;}
.bn-icon{font-size:22px;transition:transform .15s;}
.bn-label{font-size:10px;font-weight:500;color:var(--text3);}
.bn-item.active{background:rgba(31,124,173,.18);}
.bn-item.active .bn-icon{transform:scale(1.1);}
.bn-item.active .bn-label{color:var(--blue-mid);font-weight:700;}
.bn-badge{position:absolute;top:0;right:0;width:16px;height:16px;border-radius:50%;background:var(--red);color:#fff;font-size:9px;font-weight:700;display:flex;align-items:center;justify-content:center;}

/* PAGES */
/* `min-height:0` is load-bearing — without it the `.scroll` child's
   `overflow-y:auto` is ineffective because the page can grow taller than
   its flex slot. (Originally PR #224 also set `overflow:hidden` here as a
   belt-and-suspenders measure; reverted because it interacted badly with
   `.scroll`'s top in iPhone in-browser mode, clipping the today-card's
   rounded top corners.) */
.page{display:none;flex-direction:column;flex:1;min-height:0;}
.page.active{display:flex;}

/* NOTIFICATION PANEL */
.notif-list{display:flex;flex-direction:column;gap:8px;margin-bottom:14px;}
.notif-item{background:var(--card);border-radius:12px;padding:12px 14px;border:1px solid var(--border);border-left:3px solid var(--blue);cursor:pointer;transition:background .12s;}
.notif-item:active{background:rgba(255,255,255,.04);}
.notif-item.warn{border-left-color:var(--amber);}
.notif-item.urgent{border-left-color:var(--red);}
.notif-item.read{opacity:.62;}
.notif-item.read .ni-title{font-weight:500;}
.ni-title{font-size:13px;font-weight:700;color:var(--text);margin-bottom:2px;}
.ni-body{font-size:12px;color:var(--text2);}
.ni-time{font-size:10px;color:var(--text3);margin-top:4px;}

/* PUNCH CONFIRMATION SHEET */
.sheet-bg{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:100;display:none;align-items:flex-end;justify-content:center;}
.sheet-bg.open{display:flex;}
.sheet{background:var(--bg2);border-radius:24px 24px 0 0;padding:20px 20px calc(20px + var(--safe-bottom));width:100%;max-width:430px;border-top:1px solid var(--border);}
.sheet-handle{width:36px;height:4px;border-radius:2px;background:var(--text3);margin:0 auto 20px;}
.sheet-title{font-size:18px;font-weight:700;color:var(--text);margin-bottom:6px;}
.sheet-sub{font-size:13px;color:var(--text2);margin-bottom:20px;}
.sheet-info{background:var(--card);border-radius:12px;padding:14px;margin-bottom:16px;display:flex;flex-direction:column;gap:8px;}
.si-row{display:flex;justify-content:space-between;align-items:center;font-size:13px;}
.si-lbl{color:var(--text2);}
.si-val{font-weight:600;color:var(--text);font-family:var(--mono);}
.si-val.green{color:var(--green);}
.si-val.amber{color:var(--amber);}
.sheet-btn{width:100%;padding:16px;border-radius:14px;border:none;font-size:16px;font-weight:700;cursor:pointer;font-family:var(--font);margin-bottom:10px;}
.sheet-btn.confirm{background:var(--green);color:#fff;box-shadow:0 6px 20px rgba(93,181,32,.3);}
.sheet-btn.confirm-out{background:var(--red);color:#fff;box-shadow:0 6px 20px rgba(214,59,59,.3);}
.sheet-btn.cancel{background:var(--card);color:var(--text2);}
.gps-confirm{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--green);margin-bottom:16px;}

/* TOAST */
.toast{position:fixed;bottom:calc(80px + var(--safe-bottom));left:50%;transform:translateX(-50%) translateY(20px);background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:10px 18px;font-size:13px;font-weight:500;color:var(--text);white-space:nowrap;z-index:200;opacity:0;transition:all .3s;box-shadow:0 8px 24px rgba(0,0,0,.4);}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
.toast.success{border-color:rgba(93,181,32,.4);color:var(--green);}
/* The punch-save failure toast is the only caller and it must not look routine.
   `white-space:nowrap` on .toast would push a long message off both edges of a
   phone, so the error variant wraps.
   `width`, not `max-width`: .toast is position:fixed with left:50% and no right,
   so its shrink-to-fit width is capped at 50vw (~187px on a 375px phone) and a
   max-width above that is inert — the message wrapped to 5 cramped lines. An
   explicit width is what translateX(-50%) then centers. */
.toast.error{border-color:rgba(214,59,59,.5);color:var(--red);white-space:normal;width:min(86vw,340px);text-align:center;line-height:1.35;}

/* OFFLINE BANNER */
/* padding-top carries the notch inset. This is `position:fixed;top:0`, so under
   `viewport-fit=cover` (added 2026-08-03 so the bottom nav gets home-indicator
   clearance) the layout now extends INTO the status-bar area — without this the
   "No connection" text renders behind the clock and is invisible, which is the
   one message that must not be. */
.offline-banner{position:fixed;top:0;left:0;right:0;z-index:999;background:#B45309;color:#fff;font-size:12px;font-weight:600;padding:calc(8px + var(--safe-top)) 16px 8px;display:flex;align-items:center;justify-content:center;gap:8px;transform:translateY(-100%);transition:transform .3s;max-width:430px;margin:0 auto;}
.offline-banner.show{transform:translateY(0);}
.offline-banner.cached{background:#065F46;}

/* CERT BADGE extra */
.cert-badge{font-size:10px;font-weight:600;padding:2px 8px;border-radius:6px;}
.profile-header{background:linear-gradient(135deg,var(--bg2),var(--bg3));border-radius:20px;padding:24px;margin-bottom:14px;text-align:center;border:1px solid var(--border);}
.ph-av{width:72px;height:72px;border-radius:50%;background:linear-gradient(135deg,var(--blue),var(--purple));display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;color:#fff;margin:0 auto 12px;}
.ph-name{font-size:20px;font-weight:700;color:var(--text);}
.ph-role{font-size:13px;color:var(--text2);margin-top:2px;}
.ph-id{font-size:11px;color:var(--text3);font-family:var(--mono);margin-top:4px;}
.stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:14px;}
.stat-tile{background:var(--card);border-radius:12px;padding:14px;border:1px solid var(--border);text-align:center;}
.st-val{font-size:22px;font-weight:700;font-family:var(--mono);color:var(--text);}
.st-lbl{font-size:10px;color:var(--text2);margin-top:3px;text-transform:uppercase;letter-spacing:.05em;}
.cert-row{display:flex;align-items:center;gap:10px;padding:12px 14px;background:var(--card);border-radius:12px;border:1px solid var(--border);margin-bottom:8px;}
.cert-icon{font-size:20px;}
.cert-info{flex:1;}
.cert-name{font-size:13px;font-weight:600;color:var(--text);}
.cert-exp{font-size:11px;color:var(--text2);}
.cert-badge{font-size:10px;font-weight:600;padding:2px 8px;border-radius:6px;}
