/* ═══════════════════════════════════════════════════════════════
   FLOW — COMPONENTS
   Reusable UI patterns. Every component here is used in 2+ places.
   If a style only appears once, it belongs in pages.css.
   ═══════════════════════════════════════════════════════════════ */

/* ── SCROLLBAR ── */
/* Reduced from 5px (2026-07-28 feedback: "still feel very large") — Firefox's
   scrollbar-width below is already at its thinnest supported preset
   ('thin'; Firefox has no numeric control), so this width cut only affects
   Chromium/Safari's own scrollbar pseudo-elements. */
::-webkit-scrollbar { width: 3px; height: 3px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
/* Standard equivalent of the 3 rules above (batch feedback: "massive scroll
   bars" appearing in the sidebar) — ::-webkit-scrollbar only styles
   Chromium/Safari's own scrollbar pseudo-elements; a browser/engine that
   doesn't support it (or a webview that ignores it) silently falls back to
   its native default-width scrollbar, which reads huge next to Flow's thin
   5px bars everywhere else. scrollbar-width/scrollbar-color are the
   standard CSS4 properties every modern engine (incl. Firefox) honors, and
   both are inherited, so setting them once on html covers every scrollable
   element in the app, including #sidebar. */
html { scrollbar-width: thin; scrollbar-color: var(--border2) transparent; }

/* ═══ TYPOGRAPHY ═══════════════════════════════════════════════ */

/* Page-level section header — used in all settings panels and card groups */
.section-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-4);
}
.section-title {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: 700;
}

/* Eyebrow label — tiny all-caps label above a value or group */
.eyebrow {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 1.2px;
  color: var(--text3);
}

/* Muted helper text — beneath a label or field */
.hint {
  font-size: var(--text-sm);
  color: var(--text3);
  line-height: 1.5;
}

/* Accent-colored inline text */
.text-accent { color: var(--accent-bright); font-weight: 600; }
.text-success { color: var(--success); }
.text-danger  { color: var(--danger);  }
.text-warn    { color: var(--warn);    }
.text-muted   { color: var(--text3);   }

/* ═══ LAYOUT HELPERS ═══════════════════════════════════════════ */
.flex        { display: flex; }
.flex-col    { display: flex; flex-direction: column; }
.items-center{ align-items: center; }
.justify-between { justify-content: space-between; }
.gap-1 { gap: var(--space-1); }
.gap-2 { gap: var(--space-2); }
.gap-3 { gap: var(--space-3); }
.gap-4 { gap: var(--space-4); }
.flex-1 { flex: 1; }
.min-w-0 { min-width: 0; }

.grid-2 { display: grid; grid-template-columns: 1fr 1fr;           gap: var(--space-4); }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr;       gap: var(--space-4); }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr);    gap: var(--space-4); }

/* ═══ BUTTONS ══════════════════════════════════════════════════ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  height: var(--control-height);
  padding: 0 var(--control-padding-x);
  border-radius: var(--control-radius);
  border: none;
  cursor: pointer;
  font-size: var(--control-font-size);
  font-family: var(--control-font-family);
  font-weight: var(--control-font-weight);
  transition: all var(--transition);
  white-space: nowrap;
  text-decoration: none;
  line-height: 1;
}
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* Sizes */
.btn-lg { padding: 10px 20px; font-size: var(--text-base); border-radius: var(--radius); }
.btn-sm { padding: 4px 10px;  font-size: 11px; border-radius: 5px; }
.btn-xs { padding: 2px 7px;   font-size: 10px; border-radius: 4px; }

/* Shapes */
.btn-pill { border-radius: var(--radius-pill); }
.btn-icon {
  width: 32px; height: 32px; padding: 0;
  border-radius: var(--radius-sm);
}
.btn-icon-lg { width: 38px; height: 38px; }

/* Variants */
.btn-primary   { background: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent-light); }

.btn-secondary {
  background: var(--surface2);
  color: var(--text2);
  border: 1px solid var(--border2);
}
.btn-secondary:hover { background: var(--surface3); color: var(--text); }

.btn-ghost {
  background: transparent;
  color: var(--text3);
  border: 1px solid var(--border);
}
.btn-ghost:hover { background: var(--surface2); color: var(--text2); }

.btn-danger  { background: var(--danger);  color: #fff; }
.btn-success { background: var(--success); color: #fff; }
.btn-warn    { background: var(--warn);    color: #000; }

/* ═══ CARDS ════════════════════════════════════════════════════ */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4) 18px;
}

.card-sm { padding: var(--space-3) var(--space-4); }
.card-lg { padding: var(--space-6); }

/* Card with accent left border */
.card-accent { border-left: 3px solid var(--accent); }

/* Card that glows with accent bg — AI panel, rewards, clock widget etc */
.card-glow {
  background: linear-gradient(135deg, #1a0a2e 0%, var(--surface) 65%);
  border: 1px solid var(--accent-border);
  border-radius: var(--radius);
  padding: var(--space-4) 18px;
  position: relative;
  overflow: hidden;
}
.card-glow::before {
  content: '';
  position: absolute;
  top: -50px; right: -50px;
  width: 180px; height: 180px;
  background: radial-gradient(circle, rgba(93,39,141,0.2) 0%, transparent 70%);
  pointer-events: none;
}

/* Card title — eyebrow style */
.card-title {
  font-size: var(--text-xs);
  color: var(--text3);
  text-transform: uppercase;
  letter-spacing: 1.2px;
  font-weight: 700;
  margin-bottom: var(--space-3);
}

/* ═══ STAT CARDS ════════════════════════════════════════════════ */
.stat-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4) 18px;
}
.stat-label { font-size: var(--text-xs); color: var(--text3); text-transform: uppercase; letter-spacing: 1px; font-weight: 600; }
.stat-value { font-family: var(--font-display); font-size: var(--text-2xl); font-weight: 700; margin-top: var(--space-1); line-height: 1; }
.stat-sub   { font-size: var(--text-sm); color: var(--text3); margin-top: var(--space-1); }

/* Stat value color modifiers */
.sv-purple { color: var(--accent-bright); }
.sv-silver { color: var(--silver); }
.sv-green  { color: var(--success); }
.sv-white  { color: #fff; }

/* ═══ BADGES ════════════════════════════════════════════════════ */
.badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: 600;
  white-space: nowrap;
}

/* Color variants */
.b-purple { background: rgba(93,39,141,0.25);  color: var(--accent-bright); }
.b-silver { background: rgba(192,192,192,0.15); color: var(--silver); }
.b-green  { background: var(--success-bg);      color: var(--success); }
.b-red    { background: var(--danger-bg);        color: var(--danger); }
.b-yellow { background: var(--warn-bg);          color: var(--warn); }
.b-blue   { background: var(--info-bg);          color: var(--info); }
.b-gray   { background: rgba(102,102,102,0.2);   color: var(--text3); }
.b-white  { background: rgba(255,255,255,0.08);  color: #fff; }
.b-teal   { background: rgba(20,184,166,0.15);   color: #14b8a6; }
.b-orange { background: rgba(249,115,22,0.15);   color: #f97316; }
.b-pink   { background: rgba(236,72,153,0.15);   color: #ec4899; }
.b-brown  { background: rgba(180,113,71,0.18);   color: #b47147; }
.b-navy   { background: rgba(59,111,176,0.18);   color: #6ea3e0; }
.b-lime   { background: rgba(163,214,63,0.18);   color: #a3d63f; }

/* Role badges */
.role-badge    { display: inline-flex; align-items: center; padding: 2px 9px; border-radius: var(--radius-pill); font-size: 10px; font-weight: 700; }
.role-owner    { background: rgba(93,39,141,0.25);  color: var(--accent-bright); }
.role-manager  { background: var(--info-bg);         color: #60a5fa; }
.role-tech     { background: var(--success-bg);      color: #4ade80; }
.role-social   { background: var(--warn-bg);         color: #fbbf24; }
.role-sales    { background: rgba(192,192,192,0.12); color: #d1d5db; }

/* Status pills — job queue, PTO, etc */
.status-pill {
  display: inline-flex;
  align-items: center;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
}
.sp-scheduled   { background: rgba(192,192,192,0.15); color: var(--silver); }
.sp-started     { background: var(--warn-bg);          color: var(--warn); }
.sp-inprogress  { background: var(--info-bg);          color: var(--info); }
.sp-qc          { background: rgba(93,39,141,0.2);     color: var(--accent-bright); }
.sp-complete    { background: var(--success-bg);       color: var(--success); }
.sp-overdue     { background: var(--danger-bg);        color: var(--danger); }
.sp-pending     { background: var(--warn-bg);          color: var(--warn); }
.sp-approved    { background: var(--success-bg);       color: var(--success); }
.sp-denied      { background: var(--danger-bg);        color: var(--danger); }

/* ═══ TAGS ══════════════════════════════════════════════════════ */
.tag {
  display: inline-flex;
  align-items: center;
  font-size: var(--text-xs);
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  background: var(--accent-glow);
  color: var(--accent-bright);
  border: 1px solid var(--accent-border);
  font-weight: 500;
  white-space: nowrap;
}

.tag-row { display: flex; flex-wrap: wrap; gap: 5px; }

/* Tag pill input system */
.tag-input-wrap {
  min-height: 38px;
  background: var(--surface2);
  border: 1px solid var(--border2);
  border-radius: 8px;
  padding: 5px 8px;
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  align-items: center;
  cursor: text;
  transition: border-color var(--transition);
}
.tag-input-wrap:focus-within { border-color: var(--accent); }
/* Applied tags now render below a plain tag-input field instead of sharing
   one bordered box with it (2026-07-22 feedback: "remove that card encasing
   all the tags applied to the contact and the tag field bar") — this is just
   a plain flex-wrap row, no background/border of its own, so only the
   individual pills (already self-contained boxes via .tag-pill) show. */
.tag-pills-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}
.tag-pills-row:empty { display: none; }
.tag-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: var(--accent-glow);
  border: 1px solid var(--accent-border);
  border-radius: var(--radius-pill);
  padding: 2px 8px 2px 10px;
  font-size: 12px;
  font-weight: 500;
  color: var(--accent-bright);
  white-space: nowrap;
  user-select: none;
}
.tag-pill-x {
  cursor: pointer;
  font-size: 11px;
  color: rgba(192,152,252,0.7);
  line-height: 1;
  padding: 0 1px;
  border-radius: 50%;
  transition: all 0.12s;
}
.tag-pill-x:hover { color: var(--danger); background: var(--danger-bg); }

/* Custom-field "Card" type — visual single-choice tiles, functionally like radio */
.field-card-opt {
  padding: 8px 14px;
  border: 1px solid var(--border2);
  border-radius: var(--radius);
  font-size: 13px;
  cursor: pointer;
  user-select: none;
  transition: border-color var(--transition), background var(--transition);
}
.field-card-opt:hover { border-color: var(--accent); }
.field-card-opt.selected { border-color: var(--accent); background: var(--accent-glow); color: var(--accent-bright); font-weight: 600; }
.tag-input-field {
  border: none;
  background: transparent;
  color: var(--text);
  font-size: var(--text-base);
  font-family: var(--font-body);
  outline: none;
  flex: 1;
  min-width: 100px;
  padding: 2px;
}

/* ═══ FORM ELEMENTS ════════════════════════════════════════════
   Same control tokens as .btn (font-family/size/weight, height) — every
   button, dropdown, and single-line field reads at the same visual scale
   site-wide instead of each having its own hardcoded numbers. Textarea
   keeps growing (no fixed height) but still uses the same font metrics. */
/* :not([type=checkbox]):not([type=radio]) — those are native small square
   controls (or hidden entirely behind the .toggle-wrap switch pattern),
   not text-sized fields; forcing this height/padding onto them would
   break their rendering instead of standardizing anything. */
input:not([type="checkbox"]):not([type="radio"]), select, textarea {
  background: var(--surface2);
  border: 1px solid var(--border2);
  border-radius: var(--control-radius);
  color: var(--text);
  padding: 0 var(--control-padding-x);
  font-family: var(--control-font-family);
  font-size: var(--control-font-size);
  font-weight: var(--control-font-weight);
  outline: none;
  transition: border-color var(--transition);
  width: 100%;
  height: var(--control-height);
}
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
/* 2026-07-15 "dropdown color standard": every select dims to var(--text2)
   by default, everywhere. Briefly reverted app-wide in 2026-08 (batch
   feedback: modal dropdowns read duller than sibling inputs), but that
   reversal was scoped too broadly — it also brightened non-modal selects
   (list-toolbar filters without the .list-toolbar-select class, calendar
   toolbar dropdowns, etc.) that were never meant to change. Re-scoped
   correctly below: the dim default stays app-wide, and only selects living
   inside a modal are bright (.modal select) — matching the actual ask
   ("in the modals only"). */
select { color: var(--text2); }
.modal select { color: var(--text); }
.list-toolbar-select { color: var(--text2); }
input::placeholder, textarea::placeholder { color: var(--text3); }
/* Belt-and-suspenders alongside :root's color-scheme:dark (variables.css)
   for the rare browser that still renders a <select>'s own option-list
   popup with light colors regardless of that hint. */
option { background: var(--surface2); color: var(--text); }

/* Native date-input calendar glyph (2026-07-09 feedback: "match Flow's
   style") — the input BOX itself already matches every other field via the
   generic rule above; the one piece of unstyled browser-default chrome is
   this little icon, which Chrome renders dark/black by default and is
   nearly invisible against --surface2. Inverted to read as light on dark,
   consistent with every input field's white text. Flow is dark-theme-only
   today (no real light-mode CSS exists yet — see set-light-mode's
   placeholder-only toggle in Settings), so no light-theme counter-rule is
   needed here. */
input[type="date"]::-webkit-calendar-picker-indicator {
  filter: invert(1);
  opacity: 0.7;
  cursor: pointer;
}
input[type="date"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }
textarea {
  resize: vertical;
  min-height: 72px;
  height: auto;
  padding: 8px var(--control-padding-x);
}
select { cursor: pointer; }

/* Form layout */
.form-row       { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); margin-bottom: var(--space-3); }
.form-row.full  { grid-template-columns: 1fr; }
.form-row.three { grid-template-columns: 1fr 1fr 1fr; }
.form-row.four  { grid-template-columns: 1fr 1fr 1fr 1fr; }

/* Section divider between grouped rows in a long modal (Item/Service edit) */
.form-section-sep { border-top: 1px solid var(--border); margin: 14px 0; }

/* Checkbox + its own label text, sized/weighted to match .form-group label
   (11px/600/var(--text2)) instead of inheriting the surrounding body font —
   used in place of a bare <span> so checkbox titles never render bigger
   than the rest of a modal's field labels. */
.check-inline {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text2);
  cursor: pointer;
}
.form-group     { display: flex; flex-direction: column; gap: 5px; }
.form-group label { font-size: 11px; color: var(--text2); font-weight: 600; }
/* .select-label-hidden's actual display:none rule now lives mobile-only in
   css/layout.css's mobile media query (2026-07-18 feedback: keep dropdown
   labels visible on desktop, only mobile hides them) — the class is still
   applied app-wide by standardizeSelectPlaceholder() (js/ui.js), only the
   CSS effect is now scoped. */

/* Field size helpers — use on inline settings selects/inputs */
.field-xs  { width: 80px  !important; }
.field-sm  { width: 120px !important; }
.field-md  { width: 160px !important; }
.field-lg  { width: 200px !important; }
.field-xl  { width: 260px !important; }
.field-full{ width: 100%  !important; }

.search-row { display: flex; gap: var(--space-2); margin-bottom: var(--space-4); align-items: center; flex-wrap: wrap; }

/* Form hint text */
.form-hint { font-size: var(--text-xs); color: var(--text3); margin-top: 3px; }

/* Required asterisk */
.req { color: var(--accent-bright); }

/* Inline validation error */
.field-error { font-size: 11px; color: var(--danger); margin-top: 3px; display: none; }
.field-error.show { display: block; }

/* ═══ TOGGLE SWITCH ════════════════════════════════════════════ */
/* Usage: <label class="toggle-wrap"><input type="checkbox"><span class="toggle-track"></span></label> */
.toggle-wrap {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  flex-shrink: 0;
}
.toggle-wrap input[type="checkbox"] { display: none; }
.toggle-track {
  width: 36px;
  height: 20px;
  background: var(--border2);
  border-radius: var(--radius-pill);
  position: relative;
  transition: background var(--transition);
  flex-shrink: 0;
}
.toggle-track::after {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: 16px; height: 16px;
  background: #fff;
  border-radius: 50%;
  transition: transform var(--transition);
  box-shadow: var(--shadow-sm);
}
.toggle-wrap input:checked + .toggle-track { background: var(--accent); }
.toggle-wrap input:checked + .toggle-track::after { transform: translateX(16px); }

/* ═══ CHECKBOX / RADIO ROWS ════════════════════════════════════ */
/* Used in vehicle services grid, settings panels */
.check-row {
  display: flex;
  align-items: center;
  gap: 7px;
  cursor: pointer;
  font-size: var(--text-base);
  color: var(--text2);
  padding: 3px 0;
}
.check-row input[type="checkbox"],
.check-row input[type="radio"] {
  accent-color: var(--accent);
  width: 14px; height: 14px;
  flex-shrink: 0;
  width: auto;
}

/* ═══ TABLES ════════════════════════════════════════════════════ */
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
th {
  text-align: left;
  font-size: var(--text-xs);
  color: var(--text3);
  text-transform: uppercase;
  letter-spacing: 1px;
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
  font-weight: 600;
  white-space: nowrap;
  background: var(--surface2);
}
td {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  font-size: var(--text-base);
  color: var(--text2);
  vertical-align: middle;
}
tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--surface2); transition: background var(--transition); }
.est-doc-table tr:hover td { background: none; }

/* ═══ EMPTY STATE ════════════════════════════════════════════ */
.empty {
  text-align: center;
  padding: 40px 20px;
  color: var(--text3);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.empty-icon { font-size: 32px; opacity: 0.6; }
.empty-text { font-size: var(--text-base); max-width: 260px; line-height: 1.5; }
.empty-action { margin-top: var(--space-2); }

/* ═══ NOTICE / CALLOUT BOX ══════════════════════════════════ */
.notice {
  background: var(--accent-glow);
  border: 1px solid var(--accent-border);
  border-radius: 8px;
  padding: var(--space-3) var(--space-4);
  font-size: 12px;
  color: var(--text2);
  line-height: 1.6;
}
.notice strong { color: var(--accent-bright); }

.notice-success { background: var(--success-bg); border-color: rgba(34,197,94,0.3); }
.notice-success strong { color: var(--success); }
.notice-warn    { background: var(--warn-bg);    border-color: rgba(234,179,8,0.3); }
.notice-warn strong { color: var(--warn); }
.notice-danger  { background: var(--danger-bg);  border-color: rgba(239,68,68,0.3); }
.notice-danger strong { color: var(--danger); }

/* ═══ MODALS ════════════════════════════════════════════════════ */
.modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.72);
  z-index: 1000;
  align-items: center;
  justify-content: center;
  padding: var(--space-5);
}
.modal-overlay.open { display: flex; }

.modal {
  background: var(--surface);
  border: 1px solid var(--border2);
  border-radius: var(--radius-lg);
  width: 100%;
  max-width: 540px;
  max-height: 88vh;
  overflow-y: auto;
  padding: var(--space-6);
  position: relative;
}
.modal.wide   { max-width: 740px; }
.modal.xlwide { max-width: 960px; }

.modal-title {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 700;
  padding-right: 30px;
  /* Frozen while the modal body scrolls (2026-07-10) — bleeds up through
     .modal's own padding via the negative margin, then re-adds its own so
     it reads identically to before, just pinned in place. */
  position: sticky;
  top: calc(-1 * var(--space-6));
  z-index: 3;
  background: var(--surface);
  margin: calc(-1 * var(--space-6)) calc(-1 * var(--space-6)) 18px;
  padding: var(--space-6) var(--space-6) 12px;
}
.modal-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  /* The ROW must be the sticky element, not the title text nested inside it
     — a sticky child can't stick past its own parent's box once that
     parent scrolls out of view, so putting position:sticky on .modal-title
     alone let the whole header scroll away as soon as the row's own
     (unstickied) box passed the top of the scrollport. Bleeds up through
     .modal's padding exactly like the plain .modal-title rule below. */
  position: sticky;
  top: calc(-1 * var(--space-6));
  z-index: 3;
  background: var(--surface);
  margin: calc(-1 * var(--space-6)) calc(-1 * var(--space-6)) 18px;
  padding: var(--space-6) var(--space-6) 12px;
}
.modal-title-row .modal-title {
  /* Cancel the plain .modal-title rule's own sticky/bleed — the row above
     handles it now. */
  position: static;
  z-index: auto;
  background: none;
  margin: 0;
  padding: 0;
  flex: 1;
}

/* Generic full-lockdown view mode (2026-07-10) — for modals whose content
   is too dynamic (line-item pickers, add/remove buttons, custom date
   widgets) to disable field-by-field. Same blanket pointer-events approach
   already proven on the Estimate modal's accepted-lockdown, generalized to
   any modal using the .modal-title-row header convention above — blocks
   everything below the title row except the footer, in one shot. */
.modal-overlay.view-locked .modal-title-row ~ *:not(.modal-footer) {
  pointer-events: none;
  opacity: 0.65;
  user-select: none;
}
/* Info tooltips stay hoverable in view-only mode — they're read-only
   explanations, not an editable control, so the blanket pointer-events:none
   above (meant to freeze inputs/buttons) shouldn't also silence them. */
.modal-overlay.view-locked .info-tip {
  pointer-events: auto;
  opacity: 1;
}
/* Section headers stay clickable in view-only mode too — collapsing/
   expanding a section is navigation, not an edit, so it shouldn't be
   frozen by the blanket pointer-events:none above. */
.modal-overlay.view-locked .ec-section-header {
  pointer-events: auto;
  opacity: 1;
}
/* Opportunity modal (2026-07-27): "View Full Contact" is navigation, not an
   edit, and the contact name/phone above it is just read-only context — both
   should stay fully legible/clickable even while the stage-card content
   below is frozen. */
.modal-overlay.view-locked #opp-contact-info-row {
  pointer-events: auto;
  opacity: 1;
}
/* Opportunity modal (2026-07-27): Cancel/close must stay clickable in
   view-only mode too — closing isn't an edit. The generic freeze rule above
   already excludes .modal-footer by class, and #opp-modal-close-btn sits
   before .modal-title-row (so the ~ sibling selector never touches it
   either) — this is belt-and-suspenders to guarantee both explicitly
   regardless of any future markup shuffle. */
.modal-overlay.view-locked #opp-modal-footer,
.modal-overlay.view-locked #opp-modal-close-btn {
  pointer-events: auto;
  opacity: 1;
}
/* Opportunity modal (2026-07-25 feedback: "cancel is still not clickable in
   view only mode") — the fix above only covers the modal-LEVEL footer, but
   opening a specific stage from a board card pre-renders that stage's own
   edit form immediately (renderStageEditForm(), js/contacts.js), which hides
   the modal-level footer entirely and shows its own Cancel
   (cancelStageEditCard()) instead — that button was never added to this
   exception list, so the blanket freeze above still caught it. Same fix,
   same button, closing the gap.
   !important added (2026-07-26, still reported not clickable on mobile) —
   the blanket freeze rule above (.modal-overlay.view-locked .modal-title-row
   ~ *:not(.modal-footer)) actually carries HIGHER specificity than this
   class-only exception (4 class selectors vs. 3: the blanket rule's
   :not(.modal-footer) argument itself counts as a class selector) — every
   OTHER exception in this list that reliably works (#opp-modal-footer,
   #opp-contact-info-row, etc.) wins only because it's ID-scoped, which this
   button never had. Without !important, this rule could never actually have
   overridden the freeze, on any viewport, regardless of source order. */
.modal-overlay.view-locked .stage-cancel-btn {
  pointer-events: auto !important;
  opacity: 1 !important;
}
/* Opportunity modal (2026-07-25 feedback: "I just want the dimming to go
   away") — the exemption above sets .stage-cancel-btn's OWN opacity back to
   1, but that can never actually render undimmed: CSS opacity on an
   ANCESTOR creates a stacking context that composites every descendant
   through that reduced-opacity layer first, and a descendant's own
   opacity:1 cannot escape it, regardless of specificity or !important.
   #opp-stage-cards-slot (a plain sibling of .modal-title-row, so caught by
   the blanket freeze rule above) is that ancestor — the button really was
   clickable (pointer-events isn't subject to the same compositing
   limitation) but could never actually look undimmed. Real fix: stop
   dimming the whole slot and dim only the individual closed/read-only
   stage-card rows directly instead — #stage-edit-card (the one actually
   open, if any) is excluded, so its whole card, Close button included,
   renders at full opacity like "Edit" does, while every other stage still
   in the list stays visually frozen. */
.modal-overlay.view-locked #opp-stage-cards-slot {
  opacity: 1;
}
.modal-overlay.view-locked #opp-stage-cards-slot .vehicle-card:not(#stage-edit-card) {
  opacity: 0.65;
  pointer-events: none;
}
/* Contact modal/slide-in view-only mode (2026-07-11) — the real edit form
   (#ec-edit-fields) is always what's rendered; this freezes it in place
   exactly like .modal-overlay.view-locked does for every other modal,
   applied directly to the form wrapper itself since the Contacts slide-in
   panel isn't a .modal-overlay (it's a full sidebar panel, not a modal). */
#ec-edit-fields.view-locked {
  pointer-events: none;
  opacity: 0.65;
  user-select: none;
}
#ec-edit-fields.view-locked .info-tip {
  pointer-events: auto;
  opacity: 1;
}
/* Section headers stay clickable in read-only mode too (2026-07-14 fix) —
   collapsing/expanding is navigation, not an edit, same reasoning as the
   .modal-overlay.view-locked .ec-section-header exception above. This rule
   was missing here even though #ec-edit-fields uses the exact same
   .ec-section-header markup, so every section in the Contacts modal/
   slide-in was frozen shut (or open) whenever read-only mode was active. */
#ec-edit-fields.view-locked .ec-section-header {
  pointer-events: auto;
  opacity: 1;
}

.modal-close {
  position: absolute;
  top: 14px; right: 14px;
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--text3);
  border-radius: var(--radius-sm);
  width: 28px; height: 28px;
  cursor: pointer;
  font-size: 13px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all var(--transition);
}
.modal-close:hover { background: var(--surface3); color: var(--text); }

.modal-footer {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: var(--space-2);
  /* Pinned to the bottom of the scrollport (2026-07-14) — mirrors
     .modal-title-row's sticky-header trick, just upside down: bleed out
     through .modal's own bottom padding via negative margin, then re-add it
     as the footer's own padding. Without this, a long/dynamic modal (line
     items, add/remove rows) could push Close/Cancel/Save below the visible
     scroll area on some screens — pinning it removes that failure mode
     entirely instead of relying on the user scrolling exactly far enough. */
  margin: 18px calc(-1 * var(--space-6)) calc(-1 * var(--space-6));
  padding: var(--space-4) var(--space-6) var(--space-6);
  border-top: 1px solid var(--border);
  position: sticky;
  bottom: calc(-1 * var(--space-6));
  z-index: 3;
  background: var(--surface);
}

/* Read-only "view mode" for pre-existing records — shown first when an
   already-saved modal is opened, with an Edit button to reveal the normal
   editable form (2026-07-09). Shared across every modal that adopts this
   pattern, not just one page. */
.modal-view-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: var(--space-4);
  padding: 9px 0;
  border-bottom: 1px solid var(--border);
}
.modal-view-row:last-child { border-bottom: none; }
.modal-view-label {
  font-size: 11px;
  color: var(--text3);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.3px;
  flex-shrink: 0;
  padding-top: 1px;
}
.modal-view-value {
  font-size: 13px;
  color: var(--text);
  text-align: right;
}

/* Edit contact modal — special layout */
.modal-edit-contact {
  height: 80vh;
  max-width: 640px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: 0;
}
.modal-edit-contact .modal-title-row {
  /* This modal already freezes its header via a non-scrolling flex column
     (padding:0 on .modal-edit-contact itself, see below) rather than the
     sticky-bleed trick every other modal uses — cancel the row's own
     sticky/bleed and give it plain flex-shrink:0 + header padding instead.
     Must reset the base rule's full margin shorthand (not just
     margin-bottom) — its -24px top/left/right bleed assumes a parent with
     24px padding to bleed into, but .modal-edit-contact has none, so it was
     pulling the title row up past the modal's real top edge instead of
     just cancelling out to zero (2026-07-12 fix — this is what actually
     caused the title/Edit button to sit flush against the modal's border).
     position:relative (not static, 2026-07-29 fix) — .modal-close stays
     position:absolute regardless, so a plain in-flow static title row paints
     BELOW it every time (a positioned element always paints over
     unpositioned content in the same stacking context), which is exactly
     why the Edit button rendered underneath the ✕. Making this row
     positioned too (with no explicit z-index, so it still resolves by DOM
     order) puts it in the same paint bucket as .modal-close and, since it
     comes later in the markup, correctly paints on top.
     That position:relative fix only corrected paint order, though — it never
     addressed the underlying geometric overlap (2026-08 feedback: "the edit
     button is overlapping the x button still"). .modal-close is
     position:absolute at top/right:14px, width/height:28px — i.e. it
     physically occupies the 14-42px band from the top and right edges. This
     row's old right padding (24px) put the Edit button's right edge INSIDE
     that 14-42px band, so its box still visually overlapped .modal-close
     even though it now painted on top of it. Right padding bumped well past
     42px so Edit's box clears .modal-close's box with a real gap; top
     padding also increased here (2026-08 feedback: contact name sits too
     close to the top edge).
     top:0 added (2026-07-30 fix — "nothing on the header is on the same
     row, edit too high, name in the middle, X lowest"): the base
     .modal-title-row rule sets top:calc(-1 * var(--space-6)) (-24px) to
     use as its sticky-positioning stick point. Switching this row to
     position:relative above never reset that inherited `top` value — for
     position:relative, `top` isn't a no-op, it's a real offset from the
     element's normal flow position, so the whole row (title text + Edit
     button together) was still rendering 24px HIGHER than the padding
     math intended, silently cancelling out almost all of the top-padding
     clearance added above and landing it right back near .modal-close's
     own band instead of cleanly below it. Verified live via a rendered
     reproduction (getBoundingClientRect on the real markup/CSS) before
     and after this line — without it the title row's own box starts
     24px above the modal's actual top edge; with it, it starts flush,
     exactly where a plain in-flow position:relative box with margin:0
     should. */
  position: relative;
  top: 0;
  z-index: auto;
  background: none;
  padding: 32px var(--space-6) 0;
  margin: 0 0 18px;
  flex-shrink: 0;
}
/* 2026-08 fix ("name on one row, X on another, Edit on another, Call/
   Message/Internal Note on another") — the ✕ was still position:absolute
   (the base .modal-close rule, shared by every other modal in the app),
   floating independently of this row's own flex layout regardless of where
   its markup sat; that's what produced the "everything on its own row"
   effect despite the title/Edit already being flex siblings here. Moving
   the ✕ into the title row's own markup (index.html) and un-absoluting it
   here — just for this modal — puts Name/Edit/✕ in one real flex row for
   the first time, instead of one in-flow pair plus a floating box. */
.modal-edit-contact .modal-title-row .modal-close {
  position: static;
  flex-shrink: 0;
}
/* Desktop popup Edit Contact modal: no ✕ at all (2026-08 feedback) — Cancel/
   Save (or the Edit button toggling out of edit mode) already close it;
   kept on mobile, where this modal is the primary contact-detail surface
   (no slide-in panel there, see js/contacts.js's mobile branch) and the ✕
   is a more reachable close affordance than swipe-back alone. */
@media (min-width: 769px) {
  .modal-edit-contact .modal-title-row .modal-close {
    display: none;
  }
}
.modal-edit-contact #newContactForm {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--space-4) var(--space-6) var(--space-6);
}
.modal-edit-contact #editContactForm {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-height: 0;
}
.modal-edit-contact #editContactMainView,
.modal-edit-contact #editContactChangeLogView {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  min-height: 0;
  box-sizing: border-box;
}
.modal-edit-contact .modal-footer {
  /* Same reasoning as .modal-title-row's reset above — the base rule's
     position:sticky;bottom:calc(-1*var(--space-6)) bleed-through trick
     assumes a parent that itself scrolls (every other modal's .modal box),
     but here .modal-edit-contact is a non-scrolling flex column with
     overflow:hidden — so the "sticky" positioning has no scroll container
     to stick within, and any part of the footer the negative-bottom-bleed
     pulled outside the flex box's own bounds was getting clipped by that
     overflow:hidden instead of rendering flush, cutting off Close/Save on
     shorter viewports (2026-07-14 fix). Reset back to plain flow. */
  position: static;
  bottom: auto;
  flex-shrink: 0;
  margin: 0;
  padding: var(--space-3) var(--space-6);
  border-top: 1px solid var(--border);
  background: var(--surface);
}

/* Legacy tab classes — kept for JS compat, visually inactive */
.modal-tabs    { display: none; }
.modal-tab     { display: none; }
.modal-tab-panel { display: none; }
.modal-tab-panel.active { display: block; }
.modal-edit-contact #editContactMainView { display: block; }

/* ═══ EDIT CONTACT SECTIONS (single-scroll modal) ═══════════════ */
/* No outer card at all now (2026-07-12) — the fields sit directly on the
   modal's own background. .ec-section just groups each section's fields;
   the divider between sections is now carried by the header label itself
   (see .ec-section-header below) rather than a separate border-top rule. */
.ec-section {
  margin-bottom: var(--space-6);
}
.ec-section:last-of-type { margin-bottom: 0; }
.ec-section + .ec-section {
  padding-top: var(--space-6);
}
/* Label sits directly on the divider line instead of floating above it —
   the line is the header's own ::after pseudo-element, extending from the
   end of the text to the row's full width, so there's no separate rule
   needed for "the divider between sections" anymore. One size up from
   before (--text-xs -> --text-sm) per explicit request. */
.ec-section-header {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: var(--text-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 1.2px;
  color: var(--text3);
  margin-bottom: 18px;
  cursor: pointer;
  user-select: none;
}
.ec-section-header::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}
/* Collapsible sections (2026-07-13) — click the header to toggle. Portfolio
   ships open by default; every other section (and the nested Rewards
   Summary sub-block) ships collapsed via the .collapsed class in the HTML.
   .ec-subsection is the same pattern one level deeper, for Rewards Summary
   living inside the Rewards & Acquisition section rather than as its own
   top-level .ec-section. */
.ec-section-toggle {
  flex-shrink: 0;
  font-size: 9px;
  color: var(--text3);
  transition: transform var(--transition);
}
.ec-section.collapsed .ec-section-toggle,
.ec-subsection.collapsed .ec-section-toggle {
  transform: rotate(-90deg);
}
.ec-section.collapsed > *:not(.ec-section-header),
.ec-subsection.collapsed > *:not(.ec-section-header) {
  /* !important — many direct children (toolbar rows like "+ Add Vehicle")
     carry their own inline display:flex, which otherwise wins over this
     rule outright regardless of selector specificity and leaves them
     visible even while the section shows as collapsed. */
  display: none !important;
}

/* ═══ VEHICLE CARDS ════════════════════════════════════════════ */
.vehicle-card {
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4);
  margin-bottom: var(--space-3);
  position: relative;
}
.vehicle-card.primary { border-left: 3px solid var(--accent); }
.vehicle-card-header  { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }
.vehicle-card-name    { font-weight: 700; font-size: var(--text-md); flex: 1; }
.vehicle-card-meta    { font-size: 11px; color: var(--text3); }
.vehicle-service-row  {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 12px;
  padding: 4px 0;
  border-bottom: 1px solid var(--border);
  color: var(--text2);
}
.vehicle-service-row:last-child { border-bottom: none; }

/* ═══ SETTINGS ROWS ════════════════════════════════════════════ */
/* Horizontal label + control row used in all settings panels */
.settings-section { margin-bottom: var(--space-6); }
.settings-section-title {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 1.2px;
  color: var(--text3);
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}
.settings-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
  gap: var(--space-4);
}
.settings-row:last-child { border-bottom: none; }
.settings-label { font-size: var(--text-base); font-weight: 500; color: var(--text); flex: 1; }
.settings-sub   { font-size: var(--text-xs); color: var(--text3); margin-top: 2px; }
/* Hidden on desktop — this row's setting (Text Size) only has an effect on
   mobile, where css/layout.css's mobile media query shows it (display:flex)
   and reads --ui-text-scale into the --text-* tokens. */
.settings-row-mobile-only { display: none; }

/* ═══ SETTINGS SAVE BAR ═══ one per settings tab, at the bottom — every
   field just marks the tab dirty (markSettingsDirty()) instead of saving
   itself, so nothing is written until this button is clicked. */
.settings-save-bar {
  max-width: 600px;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 10px;
}
.settings-save-bar .unsaved-hint { font-size: var(--text-sm); color: var(--warn); opacity: 0; transition: opacity var(--transition); }
.settings-save-bar.dirty .unsaved-hint { opacity: 1; }

/* ═══ SIGNATURE RICH-TEXT EDITOR (Conversations Settings) ═══ */
.sig-toolbar {
  border: 1px solid var(--border2);
  border-bottom: none;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  background: var(--surface2);
  padding: 6px 8px;
}
.sig-toolbar-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.sig-toolbar-row + .sig-toolbar-row { margin-top: 6px; }
.sig-toolbar-row select {
  font-size: var(--control-font-size); height: var(--control-height); padding: 0 6px;
  background: var(--surface); border: 1px solid var(--border2); border-radius: var(--radius-sm); color: var(--text);
}
.sig-toolbar-icons button {
  width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid transparent; border-radius: var(--radius-sm);
  color: var(--text2); cursor: pointer; padding: 0;
}
.sig-toolbar-icons button:hover { background: var(--surface3); color: var(--text); }
.sig-toolbar-sep { width: 1px; height: 20px; background: var(--border2); margin: 0 2px; }
.sig-editor-body {
  min-height: 120px; max-height: 320px; overflow-y: auto;
  border: 1px solid var(--border2); border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  padding: 10px 12px; font-size: 13px; color: var(--text); background: var(--surface);
}
.sig-editor-body:focus { outline: none; border-color: var(--accent); }
.sig-editor-body img { max-width: 100%; }
.sig-code-view {
  width: 100%; min-height: 120px; max-height: 320px;
  border: 1px solid var(--border2); border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  padding: 10px 12px; font-size: 12px; font-family: monospace; color: var(--text); background: var(--surface);
}

/* ═══ CENTRAL SETTINGS PAGE — nav list + content host ═══ every other
   page's Settings tab redirects here; the nav order matches the sidebar's
   own page order (Main Settings pinned first). See relocateSettingsPanels()
   / selectSettingsSection() in js/ui.js. */
.settings-shell   {
  display: flex; align-items: flex-start; gap: 0;
  margin: -20px -20px 0; /* bleed to .content's edges so this reads as a real side panel, like the main sidebar */
}
/* Two layers on purpose: .settings-nav is the full-height column (background
   + border-right stretch all the way down next to whichever section is
   currently open, even a very tall one) — .settings-nav-inner is the actual
   sticky element. Sticky positioning only works within room its OWN box
   provides; stretching the sticky element itself to match the tallest
   sibling (the old single-div version) gave it a box exactly as tall as
   the page, so it never had anywhere to "stick" and just scrolled with
   the content. Nesting a natural-height sticky element inside a stretched,
   non-sticky wrapper keeps the full-height look and fixes the stickiness. */
.settings-nav     {
  width: 200px; flex-shrink: 0;
  background: var(--surface);
  border-right: 1px solid var(--border);
  align-self: stretch;
}
.settings-nav-inner {
  position: sticky; top: 0;
  padding: 20px 0;
}
.settings-nav-item {
  display: flex; align-items: center;
  padding: 8px 14px;
  color: var(--text3);
  cursor: pointer;
  border-left: 2px solid transparent;
  transition: all var(--transition);
  font-size: var(--text-base);
  font-weight: 500;
}
.settings-nav-item:hover  { background: var(--surface2); color: var(--text2); }
.settings-nav-item.active { background: var(--accent-glow); color: var(--accent-bright); border-left-color: var(--accent); }
/* 2026-07-11's fixed height:560px capped every Settings page at that exact
   pixel height regardless of how much real content it had — taller
   sections (Main Settings, Estimates, etc.) got clipped partway down with
   no way to reach the rest. Grow naturally to content height instead; the
   .settings-nav column beside it still matches via its own
   align-self:stretch (above), and the outer .content region (already
   overflow-y:auto everywhere else in the app) handles scrolling the whole
   row together, same as every non-Settings page. */
.settings-content { flex: 1; min-width: 0; padding: 20px; }
.settings-section-host { display: none; }
.settings-section-host.active { display: block; }

/* ═══ SUB-TAB BAR (Conversations, Pipeline, Contacts, all pages) ═ */
/* Sticky so the sub-tab bar (and the Legend/toolbar rows that sit right
   below it on some pages) stays visible below the page title while the
   page's own content scrolls underneath it — same bar, every page, every
   tab, regardless of which sub-tab is currently active. */
.conv-subtabs {
  display: flex;
  gap: 24px;
  padding: 14px 20px 0;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  background: var(--surface);
  position: sticky;
  top: 0;
  z-index: 20;
  /* Once content scrolls up underneath it, a sticky bar with nothing but a
     1px border reads as "clipping" the content rather than floating above
     it — a real shadow makes the bar's own layer obvious. */
  box-shadow: 0 4px 10px rgba(0,0,0,0.35);
}
.conv-subtab {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text3);
  padding-bottom: 12px;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  transition: color var(--transition), border-color var(--transition);
  white-space: nowrap;
}
.conv-subtab:hover  { color: var(--text2); }
.conv-subtab.active { color: var(--accent-bright); border-bottom-color: var(--accent); }

.conv-subpanel        { display: none; flex: 1; overflow: hidden; }
.conv-subpanel.active { display: flex; flex-direction: column; }

/* ═══ TOAST NOTIFICATIONS ══════════════════════════════════════ */
#toast-container {
  position: fixed;
  bottom: 24px;
  right: 24px;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  pointer-events: none;
}
.toast {
  padding: 10px var(--space-4);
  border-radius: 8px;
  font-size: var(--text-base);
  font-weight: 500;
  color: #fff;
  box-shadow: var(--shadow);
  pointer-events: auto;
  animation: toastIn 0.2s ease;
  max-width: 340px;
  line-height: 1.4;
  display: flex;
  align-items: center;
  gap: 8px;
}
.toast.info    { background: var(--accent);          border: 1px solid rgba(255,255,255,0.1); }
.toast.success { background: #16a34a;                border: 1px solid rgba(255,255,255,0.1); }
.toast.warn    { background: #b45309;                border: 1px solid rgba(255,255,255,0.1); }
.toast.error   { background: #dc2626;                border: 1px solid rgba(255,255,255,0.1); }
@keyframes toastIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ═══ INFO TOOLTIP ═════════════════════════════════════════════ */
.info-tip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  color: var(--text3);
  cursor: default;
  position: relative;
  margin-left: 3px;
  vertical-align: middle;
  line-height: 1;
}
/* Mobile-only enlarged tap target (2026-08 feedback: "near-impossible to hit
   the info icons on mobile") — the glyph itself stays the same tiny visual
   size everywhere else in the app; this just adds an invisible hit-area
   centered on it via ::before (absolutely positioned, so it takes no layout
   space and can't push surrounding text/fields around). */
@media (max-width: 768px) {
  .info-tip::before {
    content: '';
    position: absolute;
    top: 50%; left: 50%;
    width: 34px; height: 34px;
    transform: translate(-50%, -50%);
  }
}
/* The actual tooltip popup is rendered by js/ui.js's info-tip portal (one
   shared position:fixed element appended to <body>, positioned from the
   trigger's real bounding rect) instead of a ::after pseudo-element here —
   a plain absolutely-positioned ::after gets silently clipped whenever the
   trigger sits inside any scrolling ancestor (a modal's own overflow-y:auto
   body, the Contact slide-in panel's scroll container, etc.), since z-index
   can't escape an ancestor's overflow clip. */
.info-tip-portal {
  display: none;
  position: fixed;
  width: 220px;
  background: var(--surface3);
  color: var(--text2);
  border: 1px solid var(--border2);
  border-radius: var(--radius-sm);
  padding: 7px 10px;
  font-size: 11px;
  font-weight: 400;
  line-height: 1.5;
  white-space: normal;
  box-shadow: var(--shadow-lg);
  pointer-events: none;
  z-index: 99999;
  text-align: left;
  font-family: var(--font-body);
  letter-spacing: 0;
  text-transform: none;
}

/* ═══ NOTIFICATION CENTER ════════════════════════════════════ */
.notif-bell {
  position: relative;
  width: 34px; height: 34px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border2);
  background: var(--surface2);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  font-size: 16px;
  flex-shrink: 0;
  transition: all var(--transition);
}
.notif-bell:hover { border-color: var(--accent); background: var(--accent-glow); }

.notif-badge {
  position: absolute;
  top: -4px; right: -4px;
  background: var(--danger);
  color: #fff;
  font-size: 9px;
  font-weight: 700;
  min-width: 16px;
  height: 16px;
  border-radius: 8px;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 0 3px;
}

.notif-panel {
  position: fixed;
  top: var(--topbar-height);
  right: 12px;
  width: 340px;
  max-height: 480px;
  background: var(--surface);
  border: 1px solid var(--border2);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  /* Above the base .modal-overlay z-index (1000) — the Estimates slide-in
     panel (#modal-estimate, css/pages.css) is a .modal-overlay at that same
     z-index and sits later in the DOM, so it was painting on top of the
     bell dropdown. The bell needs to stay reachable over any regular modal. */
  z-index: 1100;
  display: none;
  flex-direction: column;
  overflow: hidden;
}
.notif-panel.open { display: flex; }
.notif-panel-header {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; justify-content: space-between;
  flex-shrink: 0;
}
.notif-panel-title { font-size: var(--text-base); font-weight: 700; }
.notif-list  { overflow-y: auto; flex: 1; }
.notif-item  {
  padding: 10px var(--space-4);
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background var(--transition);
  display: flex; gap: var(--space-3); align-items: flex-start;
}
/* The last row in any notif list (page banner card, bell dropdown) doesn't
   need its own divider — its container (the banner's .card border, or the
   bell panel edge) already closes off the bottom, so this border only ever
   doubled up against that edge instead of separating it from anything. */
.notif-item:last-child { border-bottom: none; }
.notif-item:hover  { background: var(--surface2); }
.notif-item.unread { background: var(--accent-glow); }
.notif-item.unread:hover { background: rgba(93,39,141,0.2); }

/* Per-page notification banner (renderPageNotifBanner, js/notifications.js):
   batch feedback — only the topmost item read as a bordered "card", the rest
   just had a thin divider line between them and blurred together. Every item
   in a page banner now gets its own full outline + rounded corners + gap,
   instead of one shared box with internal dividers (which stays the look
   for the bell dropdown's .notif-panel list — unchanged, not part of this). */
.notif-page-banner .notif-item {
  border: 1px solid var(--border2);
  border-radius: var(--radius);
  margin-bottom: 10px;
}
.notif-page-banner .notif-item:last-child { margin-bottom: 0; }
.notif-icon  { font-size: 18px; flex-shrink: 0; margin-top: 1px; }
.notif-body  { flex: 1; min-width: 0; }
.notif-title { font-size: 12px; font-weight: 600; color: var(--text); margin-bottom: 2px; }
.notif-sub   { font-size: 11px; color: var(--text3); line-height: 1.4; }
.notif-time  { font-size: 10px; color: var(--text3); white-space: nowrap; margin-top: 2px; }
.notif-dismiss-btn {
  flex-shrink: 0; align-self: center; width: 24px; height: 24px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 1px solid var(--border2); border-radius: 6px;
  color: var(--text3); cursor: pointer; transition: background var(--transition), color var(--transition);
}
.notif-dismiss-btn:hover { background: var(--accent); border-color: var(--accent); color: #fff; }

/* Notification categories (2026-07-14) — collapses the bell/panel list into
   one section per page/type (Estimates, Invoices, Calendar, etc.) instead of
   one long flat feed, each independently expandable. */
.notif-category-header {
  display: flex; align-items: center; gap: 8px;
  padding: 10px var(--space-4);
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  user-select: none;
  background: var(--surface2);
}
.notif-category-header:hover { background: var(--surface3); }
.notif-category-toggle {
  flex-shrink: 0;
  font-size: 9px;
  color: var(--text3);
  transition: transform var(--transition);
}
.notif-category.collapsed .notif-category-toggle { transform: rotate(-90deg); }
.notif-category-label {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px;
  color: var(--text3);
}
.notif-category.collapsed .notif-category-items { display: none; }

/* ═══ PIPELINE BOARD ════════════════════════════════════════════ */
.pipeline-tabs {
  display: flex;
  gap: 4px;
  margin-bottom: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 4px;
}
.pipeline-tab {
  flex: 1;
  height: var(--control-height);
  padding: 0 10px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--control-radius);
  cursor: pointer;
  font-size: var(--control-font-size);
  font-weight: var(--control-font-weight);
  color: var(--text3);
  transition: all var(--transition);
  white-space: nowrap;
}
.pipeline-tab.active         { background: var(--accent); color: #fff; }
.pipeline-tab:hover:not(.active) { background: var(--surface2); color: var(--text2); }

.pipeline-board {
  display: flex;
  gap: var(--space-3);
  overflow-x: auto;
  padding-bottom: var(--space-2);
  min-height: 480px;
  /* align-items:stretch (flex default, was flex-start) — 2026-07-25
     feedback: with flex-start every column only sized to its own cards'
     height, so a stage with just 1-2 cards had a tiny droppable sliver
     while its neighbors visually ran much taller. Stretching every column
     to match the tallest one makes the whole visual lane a real drop
     target, not just the small area its own content happens to occupy. */
  align-items: stretch;
}
.pipeline-board-top-scroll {
  overflow-x: auto;
  overflow-y: hidden;
  height: 14px;
  margin-bottom: 4px;
}
.pipeline-board-top-scroll > div {
  height: 1px;
}
.pipeline-col {
  /* +30% (2026-07-19 feedback, 200px -> 260px), then +20% more
     (2026-07-25 feedback, 260px -> 312px). */
  min-width: 312px; max-width: 312px;
  display: flex; flex-direction: column;
  gap: var(--space-2); flex-shrink: 0;
  border-radius: 8px;
  transition: background var(--transition), box-shadow var(--transition);
}
.pipeline-col.drag-over {
  background: rgba(93,39,141,0.12);
  box-shadow: 0 0 0 2px var(--accent) inset;
}
.pipeline-col-header {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 10px;
  display: flex; align-items: center; justify-content: space-between;
}
.col-label {
  font-size: var(--text-xs); font-weight: 700;
  color: var(--text2);
  text-transform: uppercase; letter-spacing: 0.5px;
}
.col-count {
  font-size: var(--text-2xs);
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--text3);
  border-radius: 10px;
  padding: 1px 6px;
  font-weight: 600;
}
.lead-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 11px;
  cursor: pointer;
  transition: border-color var(--transition);
  position: relative;
}
.lead-card:hover { border-color: var(--accent); }
.lead-card.priority { border-left: 3px solid var(--accent); }
.lead-name    { font-size: var(--text-base); font-weight: 600; margin-bottom: 3px; }
.lead-vehicle { font-size: var(--text-xs); color: var(--text3); margin-bottom: 5px; }
.lead-meta    { display: flex; gap: 5px; flex-wrap: wrap; align-items: center; }
.lead-time    { font-size: var(--text-2xs); color: var(--text3); margin-left: auto; }
/* Touch-only stand-in for dragging a card between stages — native HTML5
   drag-and-drop (still the desktop interaction) never fires from touch
   input, so mobile needs its own way to move a card (icon-button row,
   Move To opens a stage picker). Hidden on desktop, shown by the mobile
   media query in layout.css; .lead-actions-desktop is the inverse. */
.lead-actions-mobile { display: none; }

/* ═══ AUTOMATION BUILDER ════════════════════════════════════════ */
.automation-list { display: flex; flex-direction: column; gap: var(--space-3); }
.automation-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4);
}
.auto-header  { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-3); }
.auto-name    { font-weight: 600; font-size: var(--text-md); flex: 1; }
.auto-toggle  {
  width: 36px; height: 20px;
  background: var(--border2);
  border-radius: var(--radius-pill);
  position: relative;
  cursor: pointer;
  transition: background var(--transition);
  flex-shrink: 0;
}
.auto-toggle.on { background: var(--accent); }
.auto-toggle::after {
  content: '';
  position: absolute;
  top: 2px; left: 2px;
  width: 16px; height: 16px;
  background: #fff;
  border-radius: 50%;
  transition: transform var(--transition);
  box-shadow: var(--shadow-sm);
}
.auto-toggle.on::after { transform: translateX(16px); }
.auto-flow   { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.auto-step   {
  display: flex; align-items: center; gap: 5px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px 9px;
  font-size: 11px; color: var(--text2); font-weight: 500;
}
.auto-arrow  { color: var(--text3); font-size: 12px; }

/* ═══ PIPELINE ACTIONS MENU ════════════════════════════════════ */
/* Appended to <body> at open time (see togglePipeMenu(), js/db.js) rather
   than living inside the pipeline table's card — that card clips overflow
   for its own rounded corners, which was silently clipping this menu too.
   position/top/right are set inline via JS (anchored to the ⋮ button's
   real position), everything else lives here. */
.pipe-menu-dropdown {
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  min-width: 180px; z-index: 4600; box-shadow: 0 6px 24px rgba(0,0,0,0.4); overflow: hidden;
}
.pipe-menu-item {
  padding: 10px var(--space-4);
  font-size: var(--text-base);
  cursor: pointer;
  display: flex; align-items: center; gap: var(--space-2);
  color: var(--text2);
  transition: background var(--transition);
}
.pipe-menu-item:hover { background: var(--surface2); }

/* Bottom sheet on mobile (2026-07-26 feedback: "whenever a three dot menu is
   tapped on mobile, open a slider from the bottom like the modals") — every
   3-dot row menu app-wide (Contacts, Estimates, Items & Services, Templates,
   Snippets, Pipeline Settings, etc.) shares this one .pipe-menu-dropdown/
   .pipe-menu-item pair even though each page's own togglePipeMenu-style
   function builds/positions it independently — so overriding the shared
   class here retrofits all of them at once, no per-page JS changes needed.
   !important overrides whatever inline top/left/right each page's own
   position...Menu() function sets via getBoundingClientRect (still anchored
   to the ⋮ button on desktop) — same radius/full-width treatment as the
   modal bottom-sheet convention (.modal, css/layout.css's mobile block). */
@media (max-width: 768px) {
  .pipe-menu-dropdown {
    position: fixed !important;
    left: 0 !important; right: 0 !important; top: auto !important;
    /* --keyboard-inset term removed (2026-07-20) — matches
       #mobileBottomBar/#mobileFab/#mobileFabMenu in css/layout.css. Now that
       index.html's viewport meta sets interactive-widget=resizes-content,
       the real layout viewport shrinks above the keyboard natively, so a
       plain `bottom: 0` already lands there without JS help. */
    bottom: 0 !important;
    width: 100% !important; min-width: 0 !important;
    max-height: 70vh; overflow-y: auto;
    border-radius: 16px 16px 0 0 !important;
    border-left: none; border-right: none; border-bottom: none;
    box-shadow: 0 -6px 24px rgba(0,0,0,0.4);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  /* Horizontal padding bumped 16px -> 20px (2026-07-26 feedback) to match
     every modal's own 20px shell padding — these bottom-sheet 3-dot menus
     are the same kind of bottom slide-up, just for row actions instead of
     a whole modal, so they should read as the same family of surface. */
  .pipe-menu-item { padding: 14px var(--space-5); font-size: var(--text-md); }

  /* Dim the rest of the screen behind a bottom-sheet menu, same as modals
     (2026-07-29 feedback: "when a bottom sheet slider appears on screen, dim
     the rest of the content like with the modals bottom sliders"). Two
     :has() selectors cover the two distinct "is it open" mechanisms used
     app-wide, matched to each menu's own real z-index so the scrim sits
     under whichever menu triggered it:
     - .pipe-menu-dropdown (z-index 4600, above) covers every create/remove
       row menu app-wide (Contacts, Estimates, Items & Services, Templates,
       Snippets, Pipeline Settings, etc.) — mere DOM presence means open,
       since these are destroyed on close rather than hidden. EXCEPT
       #convMoreMenu (and several static filter-dropdown divs already in
       index.html — #convSidebarMoreMenu, #estFilterDD, #invFilterDD,
       #svcCatFilterDD, #invCatFilterDD, #disc-combo-items-dd), which also
       carry this class but are persistent nodes toggled via inline
       display:none/block instead of being removed on close.
     - #convMobileMoreMenu (z-index 2000, below) uses a third mechanism, a
       .open class toggle on a persistent .compose-plus-menu node, and sits
       at a much lower z-index than the row menus above, so it needs its
       own rule with a lower scrim z-index to stay on top of its own dim.
     BUG FIX (2026-07-29, caused the whole screen to read as permanently
     dimmed on every mobile page): the exclusion below originally only
     matched `display: none` WITH the space browsers normalize to when JS
     sets el.style.display. But every one of those static divs above is
     hand-authored in index.html as `style="display:none;..."` with NO
     space, and an attribute selector matches the literal string in the DOM
     — so none of them were ever actually excluded, and since
     #convSidebarMoreMenu lives in the sidebar (present on literally every
     page), the scrim was permanently on everywhere. Excluding both
     spacings fixes it for every one of these divs at once, whether they
     started hidden via hand-written HTML or later get hidden again via JS.
     pointer-events:auto (2026-07-31 feedback: "make the greyed out screen
     unclickable... but don't allow for clicking on buttons or bubbles when
     the screen is greyed out") — previously pointer-events:none let a tap
     on the dim area pass straight through to whatever button/bubble sat
     underneath it, which could fire that element's own click handler while
     the sheet was still open. auto makes the scrim itself the real click
     target for anything over the dimmed area, so the content underneath
     can no longer be reached — but since a pseudo-element is never the
     literal e.target of a click (it reports the host element, `body`,
     instead), the existing close-on-outside-click listener's
     `!menu.contains(e.target)` check still evaluates true and still closes
     the menu exactly as before; only clicking outside now closes instead
     of leaking through. */
  body:has(.pipe-menu-dropdown:not([style*="display:none"]):not([style*="display: none"]))::before {
    content: ''; position: fixed; inset: 0; z-index: 4550;
    background: rgba(0,0,0,0.6); pointer-events: auto;
  }
  body:has(#convMobileMoreMenu.open)::before {
    content: ''; position: fixed; inset: 0; z-index: 1999;
    background: rgba(0,0,0,0.6); pointer-events: auto;
  }
}

/* ═══ EVENT MODAL — CONTACT SEARCH DROPDOWN ═════════════════════ */
.ev-customer-dropdown {
  position: absolute;
  top: 100%; left: 0; right: 0;
  margin-top: 2px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  max-height: 220px;
  overflow-y: auto;
  z-index: 100;
  box-shadow: 0 6px 24px rgba(0,0,0,0.4);
}
.ev-customer-item {
  padding: 7px var(--space-3);
  font-size: var(--control-font-size);
  font-weight: var(--control-font-weight);
  cursor: pointer;
  color: var(--text2);
  transition: background var(--transition);
}
.ev-customer-item:hover, .ev-customer-item.active { background: var(--surface2); color: var(--text); }
.ev-customer-item-empty {
  padding: 7px var(--space-3);
  font-size: var(--control-font-size);
  color: var(--text3);
}

/* ═══ CUSTOM DATE PICKER ═══════════════════════════════════════
   Replaces the native browser date-picker popup, which has zero CSS reach
   in any browser (only the input's own calendar-glyph icon is stylable).
   The real <input type="date"> stays in the DOM (hidden, .flow-date-native)
   as the sole source of truth for .value — picking a day here sets it
   directly and dispatches a real 'change' event, so every existing
   onchange="..." handler keeps firing unchanged. 2026-07-09. */
.flow-date-wrap { position: relative; }
.flow-date-native {
  position: absolute !important;
  opacity: 0 !important;
  pointer-events: none !important;
  width: 1px !important; height: 1px !important;
  padding: 0 !important;
  overflow: hidden !important;
}
.flow-date-trigger {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%;
  background: var(--surface2);
  border: 1px solid var(--border2);
  border-radius: var(--control-radius);
  color: var(--text);
  padding: 0 var(--control-padding-x);
  font-family: var(--control-font-family);
  font-size: var(--control-font-size);
  font-weight: var(--control-font-weight);
  height: var(--control-height);
  cursor: pointer;
  transition: border-color var(--transition);
}
.flow-date-trigger:hover, .flow-date-trigger.open { border-color: var(--accent); }
.flow-date-label.placeholder { color: var(--text3); }
.flow-date-trigger svg { flex-shrink: 0; color: var(--text3); }
.flow-date-popup {
  display: none;
  position: absolute;
  top: 100%; left: 0;
  margin-top: 4px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  z-index: 200;
  box-shadow: 0 6px 24px rgba(0,0,0,0.4);
  padding: 10px;
  width: 260px;
}
.flow-date-header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 8px;
}
.flow-date-header-label { font-weight: 600; font-size: var(--text-base); color: var(--text); }
.flow-date-nav-btn {
  background: transparent; border: none; color: var(--text2); cursor: pointer;
  width: 26px; height: 26px; border-radius: 6px; font-size: 16px; line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
.flow-date-nav-btn:hover { background: var(--surface2); color: var(--text); }
.flow-date-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.flow-date-daylabel {
  text-align: center; font-size: 10px; font-weight: 600; color: var(--text3);
  text-transform: uppercase; padding: 4px 0;
}
.flow-date-cell {
  text-align: center; font-size: var(--text-base); color: var(--text2);
  padding: 6px 0; border-radius: 6px; cursor: pointer;
}
.flow-date-cell:hover { background: var(--surface2); color: var(--text); }
.flow-date-cell.other-month { color: var(--text3); opacity: 0.4; cursor: default; }
.flow-date-cell.today { border: 1px solid var(--accent); }
.flow-date-cell.selected { background: var(--accent); color: #fff; font-weight: 600; }
.flow-date-footer {
  display: flex; justify-content: center; margin-top: 8px;
  border-top: 1px solid var(--border); padding-top: 8px;
}
.flow-date-today-btn {
  background: transparent; border: none; color: var(--accent-bright);
  font-size: 12px; cursor: pointer; font-weight: 600;
}

/* ═══ PROGRESS BARS ════════════════════════════════════════════ */
.progress-bar {
  height: 6px;
  background: var(--border);
  border-radius: 3px;
  overflow: hidden;
}
.progress-fill {
  height: 100%;
  border-radius: 3px;
  background: var(--accent);
  transition: width 0.4s ease;
}
.progress-fill.success { background: var(--success); }
.progress-fill.warn    { background: var(--warn);    }
.progress-fill.danger  { background: var(--danger);  }

/* ═══ AVATAR ════════════════════════════════════════════════════ */
.avatar {
  width: 36px; height: 36px;
  border-radius: 50%;
  background: var(--accent);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-display);
  font-size: 13px; font-weight: 700;
  color: #fff;
  flex-shrink: 0;
}
.avatar-sm { width: 28px; height: 28px; font-size: 10px; }
.avatar-lg { width: 44px; height: 44px; font-size: 16px; }
.avatar-xl { width: 56px; height: 56px; font-size: 20px; }

/* ═══ COLUMN PICKER ════════════════════════════════════════════ */
.col-picker-row {
  display: flex; align-items: center; gap: var(--space-2);
  padding: 6px 8px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 12px; color: var(--text2);
  transition: background var(--transition);
}
.col-picker-row:hover { background: var(--surface2); }
.col-picker-row input { accent-color: var(--accent); width: 14px; height: 14px; }
