/* Zahra — Mobile-first web styles
 *
 * Mirrors the Flutter app's design system exactly.
 * iPhone-first: max-width 430px centered, safe area padding.
 * Colors, typography, radii, shadows all match the spec.
 */

@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap');

/* ── Reset + iPhone optimizations ─────────────────────────────────── */
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
body {
  margin: 0;
  font-family: 'Outfit', 'DM Sans', system-ui, sans-serif;
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  overscroll-behavior: none; /* prevent rubber-band bounce */
}
input, select, button, textarea {
  font-family: inherit;
  outline: none;
  font-size: 16px; /* prevent iOS zoom on input focus */
}
/* Strip native styling on text-like controls only — checkboxes & radios need
   their native rendering so users can SEE that they're checked. The previous
   blanket `input { -webkit-appearance: none }` nuked the checkmark on every
   native checkbox (e.g. Inspectors → "Services they do" on /web/admin/scheduling
   and the homeowner public-request services list), so clicks toggled the state
   invisibly. Pages with custom-styled toggles (.remember-checkbox in login,
   .plan-card input in signup) already hide the native input via
   `opacity:0 + position:absolute`, so they're unaffected by this change. */
input:not([type="checkbox"]):not([type="radio"]),
select,
button,
textarea {
  -webkit-appearance: none;
}
select { font-size: 16px; }
::-webkit-scrollbar { display: none; }

/* iOS safe area padding — respects notch, home indicator, status bar */
.safe-top { padding-top: env(safe-area-inset-top, 20px); }
.safe-bottom { padding-bottom: env(safe-area-inset-bottom, 20px); }
.safe-x { padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); }

/* ── Brand tokens ─────────────────────────────────────────────────── */
:root {
  --nest: #2D2B27;
  --beak: #B45F34;
  --terra: #C3A88A;
  --linen: #E5DBC5;
  --leaf: #4A6741;
  --down: #EDEADF;
  --bg: #F3F0EA;
  --card: #FAFAF7;
  --dark: #080808;
  --red: #B5362A;
  --amber: #C67B25;
  --white: #FFFFFF;
  --glass: rgba(255,255,255,0.04);
  --glass-border: rgba(255,255,255,0.06);
  --rpt-4pt: #5B7FA5;
  --rpt-wm: #8B6BAE;
  --rpt-full: #6B8E5B;
  --rpt-rc: #C4823E;
  --rpt-pool: #4A9B8E;
  --bg-soft: rgba(195,168,138,0.08);
  --focus-ring: 0 0 0 2px rgba(180,95,52,0.45);
  /* Chrome (sidebar + topbar) tokens — light defaults. The dark-mode
     blocks below flip these so the chrome follows the OS theme rather
     than being permanently dark. The sidebar used to be hardcoded
     dark-on-light, which read as a brand decision but actually came
     from never wiring the chrome into the same light/dark token system
     the rest of the page uses. */
  --chrome-bg: #FFFFFF;
  --chrome-text-muted: rgba(45,43,39,0.55);
  --chrome-text-hover: rgba(45,43,39,0.85);
  --chrome-text-active: var(--nest);
  --chrome-active-bg: rgba(180,95,52,0.08);
  --chrome-hover-bg: rgba(45,43,39,0.04);
  --chrome-divider: rgba(45,43,39,0.08);
  --chrome-logo: var(--beak);
}

/* ── Dark mode ────────────────────────────────────────────────────── */
/* Tokens apply in two modes:
   1. System default — @media (prefers-color-scheme: dark) follows the OS,
      but only when the user hasn't explicitly overridden via settings.
   2. Explicit override — html.theme-dark (always dark) or html.theme-light
      (always light) set by the Appearance section in settings.ejs. The
      class-based selectors are scoped under `html.theme-dark` so they
      win over the @media query's `:root` cascade by specificity (0,1,1
      vs 0,0,1). Component overrides still live at the bottom of the file
      where they stay higher in source order than base rules. */
@media (prefers-color-scheme: dark) {
  html:not(.theme-light) :root,
  html:not(.theme-light) {
    --bg: #121212;
    --card: #1A1A1A;
    --down: #2A2A2A;
    --nest: #E5DBC5;
    --linen: #333;
    --terra: #9A8B75;
    --chrome-bg: #1A1A1A;
    --chrome-text-muted: rgba(255,255,255,0.45);
    --chrome-text-hover: rgba(255,255,255,0.7);
    --chrome-text-active: #FFFFFF;
    --chrome-active-bg: rgba(255,255,255,0.05);
    --chrome-hover-bg: rgba(255,255,255,0.03);
    --chrome-divider: rgba(255,255,255,0.06);
  }
}

html.theme-dark :root,
html.theme-dark {
  --bg: #121212;
  --card: #1A1A1A;
  --down: #2A2A2A;
  --nest: #E5DBC5;
  --linen: #333;
  --terra: #9A8B75;
  --bg-soft: rgba(229,219,197,0.06);
  --chrome-bg: #1A1A1A;
  --chrome-text-muted: rgba(255,255,255,0.45);
  --chrome-text-hover: rgba(255,255,255,0.7);
  --chrome-text-active: #FFFFFF;
  --chrome-active-bg: rgba(255,255,255,0.05);
  --chrome-hover-bg: rgba(255,255,255,0.03);
  --chrome-divider: rgba(255,255,255,0.06);
}

@media (prefers-color-scheme: dark) {
  html:not(.theme-light) :root,
  html:not(.theme-light) {
    --bg-soft: rgba(229,219,197,0.06);
  }
}

/* ── Animations ───────────────────────────────────────────────────── */
@keyframes breathe { 0%,100%{opacity:1} 50%{opacity:0.4} }
@keyframes slideUp { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
@keyframes fadeIn { from{opacity:0} to{opacity:1} }

/* Honour OS-level "reduce motion" — disables decorative animations
   (slideUp message entries, the breathing recording dot, fadeIn cards)
   but EXEMPTS load spinners. A frozen spinner reads as a hung app, the
   exact opposite of "respectful". Spinners convey progress; without
   motion the user has no signal that work is happening. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.05s !important;
    scroll-behavior: auto !important;
  }
  .spinner, .login-spinner {
    animation-duration: 0.6s !important;
    animation-iteration-count: infinite !important;
  }
}

/* ── Focus rings (keyboard a11y) ──────────────────────────────────── */
/* Browser default outlines were stripped by the iOS reset above, so we
   re-add a visible ring scoped to keyboard focus only — never on touch
   or mouse — using :focus-visible. Use `outline` rather than box-shadow
   so we don't clobber the lift shadow on .btn-primary or other shadowed
   elements. `outline-offset` lifts the ring off rounded corners so it
   reads as a halo, not a square crop. */
:focus-visible { outline: 2px solid var(--beak); outline-offset: 2px; }
.btn-primary:focus-visible {
  outline: 2px solid var(--nest);
  outline-offset: 2px;
}
.input:focus-visible, .chat-input:focus-visible {
  outline: 2px solid var(--beak);
  outline-offset: 0;
}

/* ── Responsive shell ─────────────────────────────────────────────── */
/* iPhone-first (430px), grows on tablet/desktop with sensible max widths */
.app-shell {
  max-width: 430px;
  margin: 0 auto;
  min-height: 100vh;
  min-height: 100dvh;
  position: relative;
}

.app-shell-workspace {
  max-width: none !important;
  margin: 0;
  min-height: 100vh;
  min-height: 100dvh;
  border: none !important;
}

/* Tablet (iPad): wider shell, comfortable reading width */
@media (min-width: 768px) {
  .app-shell { max-width: 600px; }
  .header-bar { padding-top: 20px; }
}

/* Desktop: centered with side margins, no wider than needed */
@media (min-width: 1024px) {
  .app-shell { max-width: 720px; }
  body { background: #E8E4DC; }
  .app-shell { border-left: 1px solid var(--linen); border-right: 1px solid var(--linen); }
}
@media (min-width: 1024px) and (prefers-color-scheme: dark) {
  html:not(.theme-light) body { background: #0A0A0A; }
}
@media (min-width: 1024px) {
  html.theme-dark body { background: #0A0A0A; }
}

/* ── Page backgrounds ─────────────────────────────────────────────── */
.page-light { background: var(--bg); }
.page-dark { background: var(--dark); }
.page-transition { transition: background 0.4s ease; }

/* ── Typography ───────────────────────────────────────────────────── */
.t-display { font-weight: 300; font-size: 32px; color: var(--nest); letter-spacing: -0.3px; }
.t-title { font-weight: 300; font-size: 28px; color: var(--nest); letter-spacing: -0.3px; }
.t-section { font-weight: 600; font-size: 15px; color: var(--nest); }
.t-body { font-weight: 400; font-size: 14px; color: var(--nest); }
.t-label { font-weight: 500; font-size: 10px; color: var(--terra); text-transform: uppercase; letter-spacing: 1px; }
.t-small { font-weight: 400; font-size: 12px; color: var(--terra); }
.t-tiny { font-weight: 400; font-size: 10px; color: var(--terra); }

/* ── Input fields ─────────────────────────────────────────────────── */
.input {
  font-size: 16px; font-weight: 400; color: var(--nest);
  background: var(--white); border: 1px solid var(--linen);
  border-radius: 12px; padding: 14px 16px; width: 100%;
}
.input:focus { border-color: var(--beak); }
.input::placeholder { color: var(--terra); }

.input-icon-shell {
  position: relative;
  width: 100%;
}

.input-icon-shell .input {
  padding-right: 44px;
}

.input-icon-shell .z-icon {
  position: absolute;
  top: 50%;
  right: 16px;
  width: 16px;
  height: 16px;
  transform: translateY(-50%);
  color: var(--terra);
  pointer-events: none;
}

.input-date-shell input[type="date"]::-webkit-calendar-picker-indicator {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}

.input-date-shell input[type="date"]::-webkit-inner-spin-button,
.input-date-shell input[type="date"]::-webkit-clear-button {
  display: none;
}

/* ── Buttons ──────────────────────────────────────────────────────── */
.btn-primary {
  font-size: 16px; font-weight: 600; color: var(--white);
  background: var(--beak); border: none; border-radius: 28px;
  padding: 16px; width: 100%; cursor: pointer;
  box-shadow: 0 4px 20px rgba(180,95,52,0.3);
  letter-spacing: 0.3px; transition: transform 0.15s;
}
.btn-primary:active { transform: scale(0.98); }
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }

.btn-secondary {
  font-size: 13px; font-weight: 500; color: var(--beak);
  background: none; border: none; cursor: pointer; padding: 0;
}
.btn-secondary:disabled { opacity: 0.5; cursor: not-allowed; }
.input:disabled { opacity: 0.6; cursor: not-allowed; background: var(--down); }

/* Outlined secondary action — used where a tonal button needs
   visible affordance but shouldn't compete with the primary CTA.
   Replaces inline `border:1.5px solid var(--linen);border-radius:12px`
   patterns scattered across deliver/team/setup. */
.btn-outline {
  font-size: 13px; font-weight: 500; color: var(--nest);
  background: var(--card); border: 1.5px solid var(--linen);
  border-radius: 12px; padding: 10px 14px; cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
  min-height: 40px;
}
.btn-outline:hover { border-color: var(--terra); }
.btn-outline:disabled { opacity: 0.5; cursor: not-allowed; }

/* Inline banner blocks used for warnings/success/info. Tokens-based
   so they adapt to dark mode automatically — replaces the hardcoded
   `#FFF4E5`/`#C67B25`/`#6B4410` blocks in deliver and client-report. */
.banner {
  padding: 12px 14px; border-radius: 12px; border-left: 3px solid var(--terra);
  background: var(--bg-soft); color: var(--nest); font-size: 13px; line-height: 1.5;
}
.banner-warning { border-left-color: var(--amber); background: rgba(198,123,37,0.10); }
.banner-success { border-left-color: var(--leaf); background: rgba(74,103,65,0.10); }
.banner-error   { border-left-color: var(--red);  background: rgba(181,54,42,0.10); }

/* ── Cards ────────────────────────────────────────────────────────── */
.card {
  background: var(--card); border: 1px solid var(--linen);
  border-radius: 12px; padding: 14px 16px;
}

/* ── Chips (scope selector, answer chips) ─────────────────────────── */
.chip {
  display: inline-block; font-size: 13px; font-weight: 500;
  border-radius: 24px; padding: 8px 16px; cursor: pointer;
  transition: all 0.2s; border: 1.5px solid var(--linen);
  color: rgba(45,43,39,0.9); background: transparent;
}
.chip.active { color: var(--white); }

/* ── Progress bar ─────────────────────────────────────────────────── */
.progress-bar {
  height: 2px; border-radius: 1px; overflow: hidden;
}
.progress-bar-bg { background: rgba(255,255,255,0.06); }
.progress-bar-fill { height: 100%; transition: width 0.5s ease; border-radius: 1px; }

/* ── Header bar (inspection / conversation workspaces) ───────────── */
/* Intentionally stays dark in both light and dark modes. The
   inspection screen is a focus/recording workspace — the timer,
   progress bar, capture-policy pill and tab labels rely on a dark
   surface for legibility, and most of those colors live as hardcoded
   inline styles in inspect.ejs / conversation_v2.ejs. Flipping the
   header background to the chrome tokens (which the sidebar uses to
   follow OS theme) would orphan all those inline white-tint styles
   on a light surface. The sidebar is a navigation chrome and follows
   the system theme; this header is a workspace surface and doesn't.
   If we ever want a light-mode workspace header, the migration is
   the inline styles → tokens, not the other way around. */
.header-bar {
  position: sticky; top: 0; z-index: 30;
  background: var(--nest); padding: max(48px, env(safe-area-inset-top, 48px)) 16px 0;
}

/* ── Tab bar ──────────────────────────────────────────────────────── */
.tab-bar { display: flex; }
.tab-btn {
  flex: 1; background: none; border: none; padding: 10px 0 9px;
  font-size: 13px; cursor: pointer; transition: all 0.2s;
  border-bottom: 2px solid transparent;
}
.tab-btn.active { font-weight: 600; color: var(--white); border-bottom-color: var(--beak); }
.tab-btn:not(.active) { font-weight: 400; color: rgba(255,255,255,0.3); }

/* ── Chat messages ────────────────────────────────────────────────── */
.msg { margin-bottom: 12px; display: flex; animation: slideUp 0.3s ease; }
.msg.user { justify-content: flex-end; }
.msg.ai { justify-content: flex-start; }
/* During SSE streaming the renderer rewrites all bubbles on every
   chunk; replaying slideUp on every paint flickers the whole list.
   The .is-streaming flag (set in inspect.page.js) opts out of the
   entry animation while a stream is in flight, then is removed after
   the final `done` event so new messages animate normally again. */
#messages.is-streaming .msg { animation: none; }
.msg-bubble {
  max-width: 82%; padding: 12px 16px;
  font-size: 13px; line-height: 1.55; white-space: pre-wrap;
  word-wrap: break-word; overflow-wrap: anywhere;
}
.msg.user .msg-bubble {
  background: var(--nest); color: rgba(255,255,255,0.95);
  border-radius: 18px 18px 4px 18px;
}
.msg.ai .msg-bubble {
  background: var(--card); border: 1px solid var(--linen);
  color: var(--nest); border-radius: 18px 18px 18px 4px;
}
.msg-bubble strong, .msg-bubble b { font-weight: 600; }
.msg-bubble em, .msg-bubble i { font-style: italic; }
.msg-bubble code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92em; background: rgba(127,127,127,0.18);
  padding: 1px 5px; border-radius: 4px;
}

/* Thinking indicator: the brief moment after the user hits send, before
   Claude's first SSE chunk arrives. Three pulsing dots inside a normal
   AI bubble read as "working" instead of a tiny empty rounded stub. */
.msg-bubble-thinking {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 14px 16px;
}
.thinking-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--terra); opacity: 0.4;
  animation: thinkingPulse 1.2s ease-in-out infinite;
}
.thinking-dot:nth-child(2) { animation-delay: 0.15s; }
.thinking-dot:nth-child(3) { animation-delay: 0.3s; }
@keyframes thinkingPulse {
  0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
  30% { opacity: 0.9; transform: translateY(-2px); }
}

/* ── Answer chips ─────────────────────────────────────────────────── */
.answer-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.answer-chip {
  font-size: 13px; font-weight: 500; color: var(--nest);
  background: var(--card); border: 1px solid var(--linen);
  border-radius: 22px; padding: 10px 18px; cursor: pointer;
  transition: all 0.15s;
  /* Long chip labels (e.g. "Asphalt shingle — 20+ years") used to
     either overflow narrow phones or stretch a chip across the full
     row. Cap the width and let long strings wrap inside the pill so
     the layout stays predictable. text-align centers wrapped lines. */
  max-width: 100%;
  word-break: break-word;
  text-align: center;
  line-height: 1.3;
  /* iOS HIG / Apple-pencil-friendly tap target. The previous 12px font
     × 7px+16px padding rendered a ~23px tall chip — well below the
     44pt minimum that surfaces in WCAG / Apple guidelines. Inspectors
     reported "missing on the first tap, especially with gloves" on
     site visits. Bumping font + padding + min-height keeps the chip
     visually compact while ensuring the button is at least 44px in
     both dimensions for touch. min-width: 44px guards against
     single-character chips like "1" or "5" that would otherwise be
     too narrow. */
  min-height: 44px;
  min-width: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.answer-chip:hover { background: var(--down); }
.answer-chip:active { transform: scale(0.97); }

/* ── Chat input bar ───────────────────────────────────────────────── */
.chat-input-bar {
  padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 20px));
  display: flex; gap: 6px; align-items: center;
  background: var(--bg);
  border-top: 1px solid var(--linen);
}
.chat-input {
  flex: 1; font-size: 16px; color: var(--nest);
  background: var(--card); border: 1px solid var(--linen);
  border-radius: 24px; padding: 11px 16px; caret-color: var(--beak);
  min-height: 44px;
}
.chat-input::placeholder { color: var(--terra); }
.chat-input:focus { border-color: var(--beak); }
.circle-btn {
  width: 44px; height: 44px; border-radius: 22px;
  display: flex; align-items: center; justify-content: center;
  cursor: pointer; flex-shrink: 0; border: none;
  color: var(--nest);
}
/* On mobile/light mode the chat-input-bar sits on the page bg, so
   `--glass` (rgba white) is invisible. Keep the soft glass look on the
   dark inspection header where it shipped, but on the light input bar
   use a tinted neutral so Photo/Video labels actually read. */
.circle-btn.glass {
  background: var(--bg-soft);
  border: 1px solid var(--linen);
  color: var(--nest);
}
.circle-btn.beak { background: var(--beak); color: var(--white); }
.circle-btn.beak:disabled { opacity: 0.5; cursor: not-allowed; }
.circle-btn.mic-active { background: rgba(74,103,65,0.15); border: 1px solid rgba(74,103,65,0.4); color: var(--leaf); }
/* Voice-note recording state — the web audio-recorder sets this
   while MediaRecorder is actively capturing. Gentle pulse signals
   "live mic open" without distracting motion; respects
   prefers-reduced-motion. */
.circle-btn.is-recording {
  background: rgba(192,48,32,0.14);
  border: 1px solid rgba(192,48,32,0.45);
  color: #c03020;
  animation: zh-mic-pulse 1.8s ease-in-out infinite;
}
@keyframes zh-mic-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(192,48,32,0.35); }
  50%      { box-shadow: 0 0 0 6px rgba(192,48,32,0);    }
}
@media (prefers-reduced-motion: reduce) {
  .circle-btn.is-recording { animation: none; }
}

/* Conversation-mode active state — the toggle button at
   #chat-conversation-btn flips this class on. Distinct from
   .is-recording (which is the mic itself); this pill says
   "hands-free conversation mode is engaged" so the inspector can tell
   at a glance whether their next utterance will auto-send and the AI
   will speak back. Brand orange, no pulse — pulsing two adjacent
   buttons would be visual noise. */
.circle-btn.is-conversation-active {
  background: rgba(180,95,52,0.16);
  border: 1px solid rgba(180,95,52,0.55);
  color: var(--beak);
}
/* TTS-speaking state on the mic button — surfaces the "tap to
   interrupt" affordance while the assistant reply is being read aloud.
   Web doesn't run mic + speechSynthesis concurrently (Web Speech API
   can't stream while speechSynthesis is speaking on iOS Safari), so
   the only barge-in path is a manual tap. The pulse mirrors
   .is-recording's so the inspector reads it as "this button is the
   thing to tap right now." */
.circle-btn.tts-interruptible {
  background: rgba(74,103,65,0.14);
  border: 1px solid rgba(74,103,65,0.45);
  color: var(--leaf);
  animation: zh-mic-pulse 1.8s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .circle-btn.tts-interruptible { animation: none; }
}
/* Mic button when conversation mode is on but the recorder isn't
   actively chunking yet — communicates "I'm armed, just speak."
   Subtle beak-colored ring (no pulse) so it doesn't compete with
   .is-recording's pulse when actually capturing audio. Visually
   couples the conversation toggle and the mic so the inspector
   reads them as one feature. */
.circle-btn.is-conversation-armed:not(.is-recording) {
  background: rgba(180,95,52,0.10);
  border: 1px solid rgba(180,95,52,0.45);
  color: var(--beak);
}

.circle-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* ── Report tab (light mode sections) ─────────────────────────────── */
.jump-bar { display: flex; gap: 5px; overflow-x: auto; padding: 4px 0 14px; -webkit-overflow-scrolling: touch; }
.jump-pill {
  font-size: 11px; font-weight: 500; border-radius: 20px;
  padding: 5px 14px; flex-shrink: 0; white-space: nowrap;
  cursor: pointer; transition: all 0.2s; border: 1.5px solid var(--linen);
  color: var(--terra); background: transparent;
}
.jump-pill.active { background: var(--nest); color: var(--white); border-color: var(--nest); }
.jump-pill.complete { color: var(--leaf); border-color: rgba(74,103,65,0.25); }

.section-card { margin-bottom: 8px; animation: fadeIn 0.3s ease; }
.section-header {
  background: var(--card); border: 1px solid var(--linen);
  padding: 14px 16px; cursor: pointer; transition: all 0.2s;
}
.section-header.rounded { border-radius: 12px; }
.section-header.top-only { border-radius: 12px 12px 0 0; }
.section-body {
  background: var(--card); border: 1px solid var(--linen);
  border-top: none; border-radius: 0 0 12px 12px; padding: 6px 16px 16px;
  animation: slideUp 0.2s ease;
}

/* ── Observation inline card ──────────────────────────────────────── */
.obs-inline {
  margin-top: 6px; padding: 6px 10px;
  border-radius: 0 8px 8px 0; font-size: 11px; font-weight: 500;
}

/* ── Deliver screen ───────────────────────────────────────────────── */
.report-card {
  background: var(--card); border: 1px solid var(--linen);
  border-radius: 0 14px 14px 0; padding: 16px; margin-bottom: 10px;
  animation: slideUp 0.3s ease;
}

/* ── Loading spinner ──────────────────────────────────────────────── */
.spinner {
  width: 24px; height: 24px; border: 2px solid var(--linen);
  border-top-color: var(--beak); border-radius: 50%;
  animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ── Authed-image loading shimmer ─────────────────────────────────────
   <img data-authed-src="…"> elements have no real `src` until
   hydrateAuthedImages mints an object URL from a bearer-auth fetch.
   Until that resolves, the slot used to render as a flat grey
   rectangle with no signal that something was loading. While the
   fetch is in flight, hydrateAuthedImages adds [data-authed-loading]
   so we can overlay a shimmer + spinner. Removed once the blob
   resolves and the real image paints. Honours prefers-reduced-motion
   below (block #143) by falling back to a flat tint. */
img[data-authed-loading="true"],
video[data-authed-loading="true"],
audio[data-authed-loading="true"] {
  background:
    linear-gradient(
      100deg,
      rgba(255,255,255,0) 30%,
      rgba(255,255,255,0.55) 50%,
      rgba(255,255,255,0) 70%
    ) var(--down);
  background-size: 220% 100%;
  animation: authedImgShimmer 1.2s ease-in-out infinite;
}
@keyframes authedImgShimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -100% 0; }
}

/* ── App layout (sidebar + main content) ──────────────────────────── */
.app-layout {
  display: flex;
  min-height: 100vh; min-height: 100dvh;
}

.sidebar-nav {
  display: none;
  width: 220px; flex-shrink: 0;
  background: var(--chrome-bg); color: var(--chrome-text-active);
  padding: 24px 0; position: fixed; top: 0; left: 0; bottom: 0;
  overflow-y: auto; z-index: 40;
  flex-direction: column;
  border-right: 1px solid var(--chrome-divider);
}
.sidebar-logo {
  font-size: 18px; font-weight: 600; letter-spacing: 0.5px;
  padding: 0 20px 20px; color: var(--chrome-logo);
}
.sidebar-links { list-style: none; margin: 0; padding: 0; }
.sidebar-links a {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 20px; font-size: 13px; font-weight: 400;
  color: var(--chrome-text-muted); text-decoration: none;
  transition: all 0.15s; border-left: 3px solid transparent;
}
.sidebar-links a:hover { color: var(--chrome-text-hover); background: var(--chrome-hover-bg); }
.sidebar-links a.active {
  color: var(--chrome-text-active); font-weight: 500;
  border-left-color: var(--beak); background: var(--chrome-active-bg);
}
.sidebar-links .nav-icon {
  display: inline-block; width: 18px; height: 18px; vertical-align: middle;
  background-color: currentColor;
  -webkit-mask-size: contain; mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
}
.nav-icon-dashboard { -webkit-mask-image: url('/assets/icons/svg/Zhub_NestBlack.svg'); mask-image: url('/assets/icons/svg/Zhub_NestBlack.svg'); }
.nav-icon-calendar { -webkit-mask-image: url('/assets/icons/svg/Calendar_NestBlack.svg'); mask-image: url('/assets/icons/svg/Calendar_NestBlack.svg'); }
.nav-icon-billing { -webkit-mask-image: url('/assets/icons/svg/Finances_NestBlack.svg'); mask-image: url('/assets/icons/svg/Finances_NestBlack.svg'); }
.nav-icon-templates { -webkit-mask-image: url('/assets/icons/svg/Documents_NestBlack.svg'); mask-image: url('/assets/icons/svg/Documents_NestBlack.svg'); }
.nav-icon-analytics { -webkit-mask-image: url('/assets/icons/svg/Activity_NestBlack.svg'); mask-image: url('/assets/icons/svg/Activity_NestBlack.svg'); }
.nav-icon-team { -webkit-mask-image: url('/assets/icons/svg/Vendors_NestBlack.svg'); mask-image: url('/assets/icons/svg/Vendors_NestBlack.svg'); }
.nav-icon-settings { -webkit-mask-image: url('/assets/icons/svg/More_NestBlack.svg'); mask-image: url('/assets/icons/svg/More_NestBlack.svg'); }
.nav-icon-tasks { -webkit-mask-image: url('/assets/icons/svg/Tasks_NestBlack.svg'); mask-image: url('/assets/icons/svg/Tasks_NestBlack.svg'); }
.nav-icon-notifications { -webkit-mask-image: url('/assets/icons/svg/Notifications_NestBlack.svg'); mask-image: url('/assets/icons/svg/Notifications_NestBlack.svg'); }
.nav-icon-plus { -webkit-mask-image: url('/assets/icons/svg/Plus_NestBlack.svg'); mask-image: url('/assets/icons/svg/Plus_NestBlack.svg'); }
.nav-icon-overrides { -webkit-mask-image: url('/assets/icons/svg/maintenance-icon.svg'); mask-image: url('/assets/icons/svg/maintenance-icon.svg'); }
.nav-icon-profiles { -webkit-mask-image: url('/assets/icons/svg/Tasks_NestBlack.svg'); mask-image: url('/assets/icons/svg/Tasks_NestBlack.svg'); }
.nav-icon-platform-ops { -webkit-mask-image: url('/assets/icons/svg/Notifications_NestBlack.svg'); mask-image: url('/assets/icons/svg/Notifications_NestBlack.svg'); }
.nav-icon-contacts { -webkit-mask-image: url('/assets/icons/svg/Profile_NestBlack.svg'); mask-image: url('/assets/icons/svg/Profile_NestBlack.svg'); }
/* Distinct icons so nav items stop sharing glyphs (2026-05-29):
   Portal (was reusing Overrides' wrench), Scheduling (was reusing the
   Calendar grid), Help (was reusing Settings' dots). */
.nav-icon-portal { -webkit-mask-image: url('/assets/icons/svg/portal-icon.svg'); mask-image: url('/assets/icons/svg/portal-icon.svg'); }
.nav-icon-scheduling { -webkit-mask-image: url('/assets/icons/svg/clock-icon.svg'); mask-image: url('/assets/icons/svg/clock-icon.svg'); }
.nav-icon-help { -webkit-mask-image: url('/assets/icons/svg/help-icon.svg'); mask-image: url('/assets/icons/svg/help-icon.svg'); }

/* Generic branded icon (use outside of nav) */
.z-icon {
  display: inline-block; background-color: currentColor;
  -webkit-mask-size: contain; mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
}
.z-icon-right-arrow { -webkit-mask-image: url('/assets/icons/svg/Right-Arrow_NestBlack.svg'); mask-image: url('/assets/icons/svg/Right-Arrow_NestBlack.svg'); }
.z-icon-left-arrow { -webkit-mask-image: url('/assets/icons/svg/Left-Arrow_NestBlack.svg'); mask-image: url('/assets/icons/svg/Left-Arrow_NestBlack.svg'); }
.z-icon-down-arrow { -webkit-mask-image: url('/assets/icons/svg/Down-Arrow_NestBlack.svg'); mask-image: url('/assets/icons/svg/Down-Arrow_NestBlack.svg'); }
.z-icon-up-arrow { -webkit-mask-image: url('/assets/icons/svg/Up-Arrow_NestBlack.svg'); mask-image: url('/assets/icons/svg/Up-Arrow_NestBlack.svg'); }
.z-icon-check { -webkit-mask-image: url('/assets/icons/svg/check.svg'); mask-image: url('/assets/icons/svg/check.svg'); }
.z-icon-calendar { -webkit-mask-image: url('/assets/icons/svg/Calendar_NestBlack.svg'); mask-image: url('/assets/icons/svg/Calendar_NestBlack.svg'); }
.z-icon-plus { -webkit-mask-image: url('/assets/icons/svg/Plus_NestBlack.svg'); mask-image: url('/assets/icons/svg/Plus_NestBlack.svg'); }
.sidebar-user {
  margin-top: auto;
  padding: 16px 20px; border-top: 1px solid var(--chrome-divider);
  font-size: 12px; color: var(--chrome-text-muted);
}
.sidebar-user .name { font-size: 13px; font-weight: 500; color: var(--chrome-text-hover); margin-bottom: 4px; }
.sidebar-signout { color: var(--chrome-text-muted); text-decoration: none; font-size: 11px; transition: color 0.15s; }
.sidebar-signout:hover { color: var(--chrome-text-hover); }

.app-main { flex: 1; min-height: 100vh; min-height: 100dvh; }

.bottom-nav {
  display: flex; position: fixed; bottom: 0; left: 0; right: 0;
  background: var(--card); border-top: 1px solid var(--linen);
  padding: 6px 0 env(safe-area-inset-bottom, 8px); z-index: 40;
}
.bottom-nav a {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px;
  font-size: 10px; font-weight: 400; color: var(--terra);
  text-decoration: none; padding: 4px 0; transition: color 0.15s;
}
.bottom-nav a.active { color: var(--beak); font-weight: 500; }
.bottom-nav .nav-icon {
  display: inline-block; width: 20px; height: 20px;
  background-color: currentColor;
  -webkit-mask-size: contain; mask-size: contain;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-position: center; mask-position: center;
}

.page-container {
  /* Mobile: tight horizontal padding so cards run nearly edge-to-
     edge — wide horizontal margins on a phone screen waste a chunk
     of the viewport that inspectors need for one-glance scanning.
     Inner cards keep their own padding so text doesn't kiss the
     screen edge. Tablet/desktop bumps padding up via the @media
     blocks below. */
  padding: 12px 8px 80px;
  width: 100%;
  max-width: 960px;
  margin: 0 auto;
}

.page-container-wide { max-width: 1180px; }
.page-container-form { max-width: 760px; }
.page-container-settings { max-width: 1040px; }

.content-grid {
  display: grid; gap: 8px;
  grid-template-columns: 1fr;
}

@media (min-width: 768px) {
  .sidebar-nav { display: flex; }
  .bottom-nav { display: none; }
  .app-main { margin-left: 220px; }
  .page-container { padding: 28px 32px; padding-bottom: 32px; }
  .content-grid { grid-template-columns: repeat(2, 1fr); gap: 12px; }
  .app-layout .app-shell { max-width: none; border: none; }
}

@media (min-width: 1200px) {
  .content-grid { grid-template-columns: repeat(3, 1fr); }
  .page-container { padding: 32px 40px; }
}

.summary-row {
  display: grid; gap: 12px; margin-bottom: 20px;
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 768px) { .summary-row { grid-template-columns: repeat(4, 1fr); } }

.summary-card {
  background: var(--card); border: 1px solid var(--linen);
  border-radius: 12px; padding: 14px 16px;
}
.summary-card .label { font-size: 10px; font-weight: 500; color: var(--terra); text-transform: uppercase; letter-spacing: 0.5px; }
.summary-card .value { font-size: 22px; font-weight: 600; color: var(--nest); margin-top: 4px; }
.summary-card .sub { font-size: 11px; color: var(--terra); margin-top: 2px; }

.pill {
  display: inline-block; font-size: 11px; font-weight: 500;
  padding: 3px 10px; border-radius: 8px;
}
.pill-paid { color: var(--leaf); background: rgba(74,103,65,0.1); }
.pill-pending { color: var(--red); background: rgba(181,54,42,0.1); }
.pill-sent { color: var(--amber); background: rgba(198,123,37,0.1); }
.pill-unpaid { color: var(--red); background: rgba(181,54,42,0.1); }
.pill-due { color: var(--amber); background: rgba(198,123,37,0.1); }
.pill-overdue { color: var(--red); background: rgba(181,54,42,0.18); }
.pill-settled { color: var(--leaf); background: rgba(74,103,65,0.1); }
.pill-setup { color: var(--amber); background: rgba(198,123,37,0.1); }
.pill-active { color: var(--beak); background: rgba(180,95,52,0.1); }
.pill-delivered { color: var(--leaf); background: rgba(74,103,65,0.1); }

.filter-tabs { display: flex; gap: 6px; margin-bottom: 16px; flex-wrap: wrap; }
.filter-tab {
  font-size: 12px; font-weight: 500; padding: 6px 14px;
  border-radius: 20px; cursor: pointer; border: 1.5px solid var(--linen);
  color: var(--terra); background: transparent; transition: all 0.15s;
}
.filter-tab.active { background: var(--nest); color: var(--white); border-color: var(--nest); }

.modal-overlay {
  display: none; position: fixed; inset: 0; z-index: 100;
  background: rgba(0,0,0,0.4); align-items: center; justify-content: center;
}
.modal-overlay.show { display: flex; }
.modal-box {
  background: var(--card); border-radius: 16px; padding: 24px;
  width: 90%; max-width: 440px; max-height: 85vh; overflow-y: auto;
  animation: slideUp 0.2s ease;
}

/* ── Utility ──────────────────────────────────────────────────────── */
.flex { display: flex; }
.flex-col { display: flex; flex-direction: column; }
.gap-6 { gap: 6px; }
.gap-8 { gap: 8px; }
.flex-1 { flex: 1; }
.mb-6 { margin-bottom: 6px; }
.mb-8 { margin-bottom: 8px; }
.mb-16 { margin-bottom: 16px; }
.mb-20 { margin-bottom: 20px; }
.mb-24 { margin-bottom: 24px; }
.mb-32 { margin-bottom: 32px; }
.mt-8 { margin-top: 8px; }
.mt-16 { margin-top: 16px; }
.mt-24 { margin-top: 24px; }
.p-20 { padding: 20px; }
.text-center { text-align: center; }
.hidden { display: none !important; }
/* Visually hidden but readable by assistive tech — used for form labels
   that pair with placeholder-driven inputs. */
.visually-hidden {
  position: absolute !important; width: 1px !important; height: 1px !important;
  padding: 0 !important; margin: -1px !important; overflow: hidden !important;
  clip: rect(0,0,0,0) !important; white-space: nowrap !important; border: 0 !important;
}
/* Comfortable touch target — used by inline action buttons that need
   ≥44×44 hit area without growing visually. Keeps the rendered glyph
   tiny while expanding the tap surface and pulling layout back with
   negative margin so the surrounding row doesn't bloat. */
.tap-target {
  min-width: 44px; min-height: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  touch-action: manipulation; -webkit-tap-highlight-color: transparent;
}

/* ── Responsive fixes ────────────────────────────────────────────── */

/* Fixed bottom button: above bottom nav on mobile, scale with content on tablet/desktop */
.fixed-bottom-btn {
  position: fixed; bottom: calc(60px + env(safe-area-inset-bottom, 8px));
  right: 24px; left: 24px;
  max-width: 430px; margin: 0 auto; z-index: 35;
}
@media (min-width: 768px) {
  .fixed-bottom-btn { bottom: 24px; left: 244px; right: 24px; max-width: 536px; margin: 0 auto; }
}
@media (min-width: 1200px) {
  .fixed-bottom-btn { max-width: 600px; }
}

/* Ensure body doesn't overflow horizontally.
 *
 * Use `overflow-x: clip` rather than `hidden`. Per CSS spec, setting
 * `overflow-x: hidden` on the `html` element forces `overflow-y` to
 * compute to `auto`, turning html into a scroll container. That breaks
 * the viewport's natural scroll on desktop browsers (notably Chrome/
 * Edge on macOS with trackpad) and on some iOS Safari combinations,
 * especially with `body { overscroll-behavior: none }` above and the
 * scrollbar-display:none rule on the global reset. `clip` hides
 * horizontal overflow without establishing a scroll container, so the
 * viewport keeps its default vertical scrolling.
 *
 * Compat: Chrome 90+, Edge 90+, Firefox 81+, Safari 16+ (iOS 16+).
 * Matches the app's iOS 16 deployment target and modern-browser-only
 * staff web support.
 */
html, body { overflow-x: clip; }

/* ── Auth pages (login, forgot-password, reset, 2fa) ────────────── */
.auth-page {
  position: relative; min-height: 100vh; min-height: 100dvh;
  background:
    radial-gradient(ellipse 70% 50% at 20% 30%, rgba(255,252,242,0.5) 0%, transparent 70%),
    radial-gradient(ellipse 60% 50% at 80% 70%, rgba(180,95,52,0.08) 0%, transparent 60%),
    #E2D8C8;
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
}
.auth-card {
  background: rgba(255,252,242,0.72);
  -webkit-backdrop-filter: blur(16px) saturate(150%);
  backdrop-filter: blur(16px) saturate(150%);
  border: 1px solid rgba(255,255,255,0.45);
  border-radius: 20px; padding: 32px 40px;
  display: flex; flex-direction: column; align-items: center; text-align: center;
  box-shadow: 0 8px 40px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.04);
  position: relative; overflow: hidden;
  width: 100%; max-width: 440px;
}
.auth-card::before {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(135deg, rgba(255,255,255,0.35) 0%, rgba(255,255,255,0) 50%);
  pointer-events: none; border-radius: inherit;
}
/* Break out of .app-shell for full-bleed auth backgrounds */
.app-shell:has(.auth-page) { max-width: none !important; border: none !important; margin: 0; }
/* Auth page inputs inherit the underline style from login */
.auth-card .input {
  background: transparent; border: none; border-bottom: 1px solid #000;
  border-radius: 0; padding: 12px 5px 8px; font-size: 16px; color: #000;
}
.auth-card .input:focus { border-color: var(--beak); border-bottom-width: 1.5px; }
.auth-card .input::placeholder { color: #B8B5AA; }
.auth-card .btn-primary {
  background: var(--beak); border: 1px solid #2D2B27; border-radius: 100px;
  color: #2D2B27; font-size: 18px; height: 49px; max-width: 348px;
}
.auth-card .btn-secondary { color: #2D2B27; }
.auth-card .btn-secondary:hover { color: var(--beak); }
.auth-card a { color: #2D2B27; }
.auth-card a:hover { color: var(--beak); }
@media (max-width: 480px) {
  .auth-card { padding: 24px 22px; }
  .auth-card .btn-primary { font-size: 16px; height: 42px; }
}

/* ── Dark mode component overrides ───────────────────────────────── */
/* Placed AFTER all base rules so source-order wins at equal specificity.
   Applied in two modes: system (OS dark + user hasn't chosen light) and
   explicit `html.theme-dark`. Selector lists ensure each rule fires for
   either trigger without duplicating the whole block. */
/* Sidebar now follows the chrome-* tokens in :root and the dark-mode
   token block above, so it switches automatically with the OS theme —
   no more hardcoded #1A1A1A here. The .header-bar is intentionally
   left out of the chrome system; it stays dark in both modes (see
   the comment on the .header-bar rule for rationale). */
@media (prefers-color-scheme: dark) {
  html:not(.theme-light) .input { background: var(--card); color: var(--nest); border-color: var(--linen); }
  html:not(.theme-light) .header-bar { background: #1A1A1A; }
  html:not(.theme-light) .msg.user .msg-bubble { background: #333; }
  html:not(.theme-light) .chip { color: var(--nest); }
  html:not(.theme-light) .filter-tab.active,
  html:not(.theme-light) .jump-pill.active { background: #E5DBC5; color: #121212; border-color: #E5DBC5; }
}
html.theme-dark .input { background: var(--card); color: var(--nest); border-color: var(--linen); }
html.theme-dark .header-bar { background: #1A1A1A; }
html.theme-dark .msg.user .msg-bubble { background: #333; }
html.theme-dark .chip { color: var(--nest); }
html.theme-dark .filter-tab.active,
html.theme-dark .jump-pill.active { background: #E5DBC5; color: #121212; border-color: #E5DBC5; }
