/* ═══════════════════════════════════════════════════════════════
   FLOW — PAGES
   Page-specific styles and one-off patterns that don't belong
   in components. Organized one section per page/feature.
   ═══════════════════════════════════════════════════════════════ */

/* ═══ CONVERSATIONS ════════════════════════════════════════════ */
.conv-layout {
  display: grid;
  grid-template-columns: 345px 1fr; /* +15% (2026-07-19 feedback, 300px -> 345px). */
  flex: 1;
  background: var(--surface);
  overflow: hidden;
}
.conv-sidebar {
  border-right: 1px solid var(--border);
  display: flex; flex-direction: column; overflow: hidden;
}
.conv-sidebar-header {
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border);
  display: flex; flex-direction: column; gap: var(--space-2);
  flex-shrink: 0;
}
.conv-filter-tabs { display: flex; gap: 3px; }
.conv-filter-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); border: 1px solid transparent;
}
.conv-filter-tab.active            { background: var(--accent); color: #fff; }
.conv-filter-tab:hover:not(.active){ background: var(--surface2); color: var(--text2); }

.conv-list { flex: 1; overflow-y: auto; }
.conv-item {
  padding: 11px var(--space-4);
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  transition: background var(--transition);
  position: relative;
}
.conv-item:hover              { background: var(--surface2); }
.conv-item.active             { background: rgba(93,39,141,0.22); border-left: 3px solid var(--accent); }
.conv-item.unread             { background: rgba(93,39,141,0.10); }
.conv-item.unread .conv-name  { font-weight: 700; color: var(--text); }
.conv-item.unread .conv-preview { color: var(--text); font-weight: 500; }
.conv-item.unread .conv-time  { color: var(--text); }

.conv-item-row { display: flex; gap: 10px; align-items: flex-start; }
.conv-item-content { flex: 1; min-width: 0; }
.conv-item-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 3px; gap: 8px; }
.conv-name    { font-size: var(--text-base); font-weight: 600; }
.conv-time    { font-size: var(--text-sm); color: var(--text3); flex-shrink: 0; }
/* Unread badge sits on the same row as the preview text (2026-07-19
   feedback) — it used to stack under the time in its own column, which
   pushed the preview line down onto what read as a new row whenever a
   notification came in. */
.conv-preview-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.conv-preview { flex: 1; min-width: 0; font-size: var(--text-sm); color: var(--text3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.conv-unread-badge {
  background: var(--accent); color: #fff;
  font-size: var(--text-xs); font-weight: 700;
  min-width: 18px; height: 18px;
  border-radius: 9px;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0 5px; flex-shrink: 0;
}

.conv-main    { display: flex; flex-direction: column; overflow: hidden; }
.conv-topbar  {
  padding: 11px var(--space-4);
  border-bottom: 1px solid var(--border);
  display: flex; align-items: center; gap: var(--space-3);
  flex-shrink: 0;
}
.conv-contact-info  { flex: 1; min-width: 0; }
.conv-contact-name  { font-weight: 700; font-size: var(--text-md); }
.conv-contact-sub   { font-size: var(--text-xs); color: var(--text3); margin-top: 8px; }

/* Back-to-thread-list button — only ever shown on mobile (see media query
   below); the conversation view is a single full-width pane there, so
   there's no list visible alongside it to tap back to otherwise. */
.conv-back-btn { display: none; }

/* Messages */
.conv-messages {
  flex: 1; overflow-y: auto; overflow-x: hidden; padding: 10px var(--space-4);
  display: flex; flex-direction: column; gap: var(--space-2);
  /* overscroll-behavior:contain (2026-07-26 feedback: "I can scroll past
     the compose bar when the keyboard is open on mobile") — without this,
     scrolling this list to its end can rubber-band/chain into the parent
     shell on mobile Safari/Chrome once the on-screen keyboard has resized
     the visual viewport, visually dragging the fixed .conv-compose bar
     along with it instead of it staying pinned. Containing the scroll here
     keeps it fully local to the message list. */
  overscroll-behavior-y: contain;
}
.msg-row              { display: flex; gap: var(--space-2); align-items: flex-end; }
.msg-row.outbound     { flex-direction: row-reverse; }
.msg-avatar           { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: var(--text-2xs); font-weight: 700; flex-shrink: 0; color: #fff; }
.msg-avatar.customer  { background: var(--surface3); color: var(--text2); border: 1px solid var(--border2); }
.msg-avatar.agent     { background: var(--accent); }

.msg-bubble-wrap      { max-width: 68%; display: flex; flex-direction: column; align-items: flex-start; }
.msg-bubble-wrap.out  { align-items: flex-end; margin-left: auto; }
/* Mobile bubbles use far more of the narrower screen width (2026-07-17
   feedback) — same per-side alignment (.out still right-aligns via
   align-items/margin-left above), just a wider cap. */
@media (max-width: 768px) {
  .msg-bubble-wrap { max-width: 90%; }
}
.msg-bubble           { width: fit-content; max-width: 100%; padding: 5px 10px; border-radius: 8px; font-size: var(--text-base); line-height: 1.45; word-break: break-word; }

.msg-bubble.inbound   { background: #1e1e2e; border: 1.5px solid rgba(93,39,141,0.45); color: var(--text); border-bottom-left-radius: 4px; }
.msg-bubble.outbound  { background: var(--accent); border: 1.5px solid rgba(192,192,192,0.25); color: #fff; border-bottom-right-radius: 4px; }

.msg-bubble.email-bubble         { background: #1e1e2e; border: 1.5px solid rgba(93,39,141,0.45); border-left: 3px solid var(--accent); color: var(--text); border-bottom-left-radius: 4px; }
.msg-bubble.email-bubble.outbound{ background: var(--accent); border: 1.5px solid rgba(192,192,192,0.25); border-left: 3px solid rgba(255,255,255,0.3); color: #fff; border-bottom-right-radius: 4px; }
.msg-bubble.call-bubble          { background: #1e1e2e; border: 1.5px solid rgba(93,39,141,0.45); border-left: 3px solid var(--warn); color: var(--text); }
.msg-bubble.note-bubble          { background: var(--accent); border: 1.5px solid rgba(192,192,192,0.2); color: #fff; font-style: italic; opacity: 0.85; }

.msg-bubble.pipeline-change-bubble {
  background: #2a2a2e;
  border: 1.5px solid var(--accent);
  color: var(--text2);
  border-radius: var(--radius);
}
.msg-bubble.pipeline-change-bubble .msg-bubble-header { color: var(--text3); opacity: 0.8; }

.msg-bubble-header {
  font-size: var(--text-2xs); font-weight: 500; opacity: 0.6;
  margin-bottom: 3px; display: block; white-space: nowrap;
}
.msg-bubble.outbound .msg-bubble-header { text-align: right; }

.msg-meta { display: none; }

/* Email thread cards — Gmail-style conversation view instead of a narrow
   chat bubble (emails often carry a full quoted reply chain from the
   customer's own mail client, which reads far better as a real message
   card than squeezed into a one-line-text bubble), but still indented to
   the sender's side like every other message type (2026-07-28 feedback) —
   90% width instead of full-bleed, pushed to the right for outbound and
   left for inbound via auto margins (.conv-messages is a column flexbox,
   so these behave like block-level margin:auto centering on the cross
   axis — the same technique used for .msg-bubble-wrap.out elsewhere). */
.email-thread-card {
  width: 100%; max-width: 90%; box-sizing: border-box; margin: 4px 10px; flex-shrink: 0;
  background: var(--surface2); border: 1px solid var(--border2); border-radius: var(--radius);
  overflow: hidden;
}
.email-thread-card.outbound { border-left: 3px solid var(--accent); margin-left: auto; }
.email-thread-card.inbound  { border-left: 3px solid var(--info); margin-right: auto; }
@media (max-width: 768px) {
  .email-thread-card { max-width: 100%; margin-left: 10px; margin-right: 10px; }
}
.email-thread-header {
  display: flex; align-items: center; gap: 10px; padding: 10px 14px;
}
.email-thread-from   { font-weight: 700; font-size: 13px; color: var(--text); }
.email-thread-subject{ font-size: 11px; color: var(--text3); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* flex-shrink:1 + max-width/ellipsis (2026-07-08 fix) — a failed-send's
   delivery-error text appended here is arbitrary-length (a raw provider
   error string), unlike the plain short timestamp this was designed for;
   left at flex-shrink:0 it forced .email-thread-header (and the card,
   which has no max-width) wider than the conversation pane, making the
   whole thread scroll sideways. Full text still available via title attr
   (see renderEmailThreadCard, js/calendar.js). */
.email-thread-ts      { font-size: 11px; color: var(--text3); white-space: nowrap; flex-shrink: 1; min-width: 0; max-width: 45%; overflow: hidden; text-overflow: ellipsis; }
.email-thread-chevron { color: var(--text3); font-size: 11px; flex-shrink: 0; }
.email-thread-body {
  padding: 0 14px 14px 14px; font-size: var(--text-base); line-height: 1.5;
  color: var(--text); word-break: break-word; border-top: 1px solid var(--border2);
  padding-top: 12px; margin-top: -1px;
}
/* Every older (non-latest) email in the conversation truncates to 3 lines
   instead of hiding entirely behind a collapsed toggle — a "Read more" link
   right below expands just that one card. */
.email-thread-body-clamped {
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}
.email-read-more {
  padding: 0 14px 12px 14px; font-size: 11px; color: var(--accent-light);
  cursor: pointer; text-decoration: underline; margin-top: -6px;
}
.email-thread-reply-row { display: flex; justify-content: center; padding: 12px 14px 14px; }

/* Quoted-reply chain, split out of the email body itself (2026-07-27
   feedback: "separate the new email from the previous replies attached at
   the end of that same email" — customer mail clients paste the entire
   prior message back into a reply, which used to render as one giant blob
   indistinguishable from the actual new reply). splitEmailQuoteBoundary()
   (js/calendar.js) detects the boundary; everything from there on renders
   collapsed here instead of inline in .email-thread-body. */
.email-quoted-toggle {
  padding: 0 14px 12px 14px; font-size: 11px; color: var(--text3);
  cursor: pointer; text-decoration: underline;
}
.email-quoted-body {
  margin: 0 14px 14px 14px; padding: 8px 0 8px 12px; border-left: 2px solid var(--border2);
  color: var(--text3); font-size: var(--text-sm); line-height: 1.5; word-break: break-word;
}

.email-collapsed-toggle {
  width: 100%; box-sizing: border-box; margin: 2px 10px;
  padding: 8px 14px; font-size: 12px; color: var(--text3);
  background: var(--surface2); border: 1px dashed var(--border2); border-radius: var(--radius);
  cursor: pointer; text-align: center;
}
.email-collapsed-toggle:hover { color: var(--text2); border-color: var(--border); }

/* Channel tags */
.ch-sms   { background: var(--success-bg); color: var(--success); }
.ch-email { background: var(--info-bg);    color: var(--info); }
.ch-call  { background: var(--warn-bg);    color: var(--warn); }
.ch-note  { background: rgba(192,192,192,0.1); color: var(--text3); }

.conv-empty {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  color: var(--text3); gap: var(--space-3);
}

/* Compose */
.conv-compose { padding: var(--space-3) var(--space-4); border-top: 1px solid var(--border); flex-shrink: 0; }
.conv-compose-tabs { display: flex; gap: 4px; margin-bottom: var(--space-2); }
.conv-compose-tab {
  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);
  border: 1px solid var(--border); background: transparent;
  transition: all var(--transition);
}
.conv-compose-tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.conv-compose-row { display: flex; gap: var(--space-2); align-items: flex-end; }
.conv-compose-row textarea { min-height: 50px; resize: vertical; flex: 1; }
.email-fields          { display: none; flex-direction: column; gap: 6px; margin-bottom: var(--space-2); }
.email-fields.show     { display: flex; }
.email-fields input    { font-size: 12px; padding: 6px 10px; }
.note-indicator        { font-size: 10px; color: var(--text3); font-style: italic; margin-bottom: 4px; }

/* Compose plus menu */
.compose-plus-btn {
  width: 36px; height: 36px;
  background: var(--surface2); border: 1px solid var(--border2);
  border-radius: 8px; display: flex; align-items: center; justify-content: center;
  cursor: pointer; font-size: 18px; color: var(--text2); flex-shrink: 0;
  transition: all var(--transition); position: relative;
}
.compose-plus-btn:hover { background: var(--accent-glow); border-color: var(--accent); color: var(--accent-bright); }
.compose-plus-menu {
  position: absolute; bottom: 44px; left: 0;
  background: var(--surface); border: 1px solid var(--border2);
  border-radius: var(--radius); padding: 6px;
  z-index: 2000; min-width: 200px; box-shadow: var(--shadow-lg); display: none;
}
.compose-plus-menu.open { display: block; }

/* Mobile message-type dropdown button (2026-07-08 feedback) — desktop keeps
   .conv-compose-tabs; this is the mobile-only replacement, hidden here and
   shown in the mobile media query below. Same visual treatment as
   .compose-plus-btn since they sit side by side on the mobile controls row. */
.conv-compose-type-mobile { display: none; }
/* Mobile-only consolidated "more" arrow (2026-07-21) — hidden on desktop by
   default, shown in the mobile media query below. */
.conv-mobile-more-wrap { display: none; }
.conv-mobile-more-btn:hover { background: var(--accent-light); }
.conv-compose-type-btn {
  width: 36px; height: 36px;
  background: var(--surface2); border: 1px solid var(--border2);
  border-radius: 8px; display: flex; align-items: center; justify-content: center;
  cursor: pointer; color: var(--text2); flex-shrink: 0;
  transition: all var(--transition);
}
.conv-compose-type-btn:hover { background: var(--accent-glow); border-color: var(--accent); color: var(--accent-bright); }
.conv-compose-type-menu {
  position: absolute; bottom: 44px; left: 0;
  background: var(--surface); border: 1px solid var(--border2);
  border-radius: var(--radius); padding: 6px;
  z-index: 2000; min-width: 170px; box-shadow: var(--shadow-lg); display: none;
}
.conv-compose-type-menu.open { display: block; }

/* Send button — text on desktop, arrow icon on mobile (2026-07-08 feedback).
   Both spans always render (see renderConvMain, js/calendar.js); CSS below
   picks which one shows per breakpoint — same dual-render trick already
   used for .btn-label/.conv-topbar-icon-mobile-only elsewhere in
   Conversations. Untouched by the send/undo countdown morph, which
   snapshots+restores the button's innerHTML as a whole (queuePendingSend,
   js/calendar.js). */
.conv-send-btn-icon { display: none; }
.plus-menu-item {
  display: flex; align-items: center; gap: var(--space-3);
  padding: 8px var(--space-3); border-radius: 7px;
  cursor: pointer; font-size: var(--text-base); color: var(--text2);
  transition: background var(--transition);
}
.plus-menu-item:hover { background: var(--surface2); color: var(--text); }
.plus-menu-sep { height: 1px; background: var(--border); margin: 4px 0; }

/* Snippets */
.snippet-card {
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: 8px; padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-2); cursor: pointer;
  transition: border-color var(--transition);
}
.snippet-card:hover { border-color: var(--accent); }
.snippet-title    { font-weight: 600; font-size: var(--text-base); margin-bottom: 4px; }
.snippet-preview  { font-size: 11px; color: var(--text3); line-height: 1.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Snippet editor — categorized variable-insert menu */
.var-insert-wrap { position: relative; display: inline-block; }
.var-insert-menu {
  position: absolute; top: calc(100% + 4px); left: 0;
  background: var(--surface); border: 1px solid var(--border2);
  border-radius: var(--radius); padding: 8px;
  z-index: 2000; width: 260px; max-height: 340px; overflow-y: auto;
  box-shadow: var(--shadow-lg); display: none;
}
.var-insert-menu.open { display: block; }
.var-insert-menu input[type="text"] { width: 100%; font-size: 11px; margin-bottom: 6px; }
.var-cat-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 5px 6px; font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.5px; color: var(--text3); cursor: pointer; user-select: none;
}
.var-cat-header:hover { color: var(--text2); }
.var-cat-items { display: none; }
.var-cat-items.open { display: block; }
.var-insert-item {
  display: flex; flex-direction: column; gap: 1px;
  padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 11px;
}
.var-insert-item:hover { background: var(--surface2); }
.var-insert-item code { color: var(--accent-bright); font-size: 11px; }
.var-insert-item .var-desc { font-size: 10px; color: var(--text3); }
.var-insert-empty { padding: 10px 6px; font-size: 11px; color: var(--text3); text-align: center; }

/* Mark as Read/Unread + Call buttons: text-only on desktop (2026-07-08
   feedback), icon-only on mobile — same buttons, same markup, just which
   half is visible changes per breakpoint. Hidden here (desktop default);
   the mobile media query below re-shows it and hides .btn-label instead. */
.conv-topbar-icon-mobile-only { display: none; }

/* ═══ CONVERSATIONS — MOBILE RESPONSIVE LAYER ═══════════════════
   Same codebase as desktop — no separate app. Desktop's fixed
   300px-sidebar + main two-pane grid doesn't fit a phone screen, so
   below this breakpoint it becomes a single full-width pane at a
   time: the thread list by default, or the open conversation once
   a thread is tapped (toggled via .mobile-thread-open, set/cleared
   by openConv()/closeMobileConv() in calendar.js). Action buttons
   in the conversation topbar go icon-only (text hidden, not
   removed — same .btn-label pattern the sidebar nav already uses
   for its collapsed state) to keep the header from overflowing a
   ~375px viewport. ═══════════════════════════════════════════════ */
@media (max-width: 768px) {
  .conv-layout { grid-template-columns: 1fr; }
  .conv-sidebar { display: flex; }
  .conv-main    { display: none; }
  .conv-layout.mobile-thread-open .conv-sidebar { display: none; }
  .conv-layout.mobile-thread-open .conv-main    { display: flex; }

  .conv-back-btn { display: inline-flex !important; }

  .conv-topbar .btn-label { display: none; }
  .conv-topbar-icon-mobile-only { display: inline-flex !important; }
  .conv-topbar .btn { padding: 0 9px; }
  .conv-topbar { gap: 6px; padding: 9px var(--space-3); }
  .conv-contact-sub { display: none; } /* phone/email subtext — redundant with the Edit button one tap away, and tight on width */

  /* Compose area (2026-07-21 rebuild — replaces the earlier two-row layout,
     2026-07-08) — the separate message-type icon button, "+" insert button,
     and desktop schedule-send caret are all consolidated into ONE small
     arrow next to Send (.conv-mobile-more-wrap, js/calendar.js), so the
     whole row (textarea + Send + arrow) now fits on one line with nothing
     wrapping to a second row underneath. */
  .conv-compose-tabs { display: none; }
  .conv-compose-type-mobile { display: none; }
  .compose-plus-btn { display: none; }
  .conv-send-caret-wrap { display: none; }
  .conv-mobile-more-wrap { display: block; }
  #convSendBtn { width: 44px !important; padding: 0 !important; }
  .conv-send-btn-label { display: none; }
  .conv-send-btn-icon { display: inline-flex; }

  /* Remove the contact/staff-initial circles beside each chat bubble
     (2026-07-08 feedback) — bubble alignment (out/in via .msg-row's
     row-reverse) already communicates who sent what without them, and they
     eat into the limited width a phone screen has for the bubble itself. */
  .msg-avatar { display: none; }

  /* Edit Contact / filter dropdown / Favorite all fold into the existing
     "more" (⋮) menu on mobile instead of competing for header width —
     Call and Mark as Read/Unread stay as direct icon-only buttons.
     .conv-more-mobile-only items (in that same menu) are the mobile-only
     equivalents; hidden on desktop just below since those two actions
     already have their own direct buttons there. */
  .conv-topbar-editbtn, .conv-topbar-filter, .conv-topbar-favbtn { display: none !important; }
  .conv-more-mobile-only { display: flex !important; }

  #convOwnerFilter { width: 74px !important; font-size: var(--control-font-size); }
  #convSidebarNewBtn { display: none; } /* the floating + button covers this on mobile */

  /* Sidebar header sort/filter controls collapse into a three-dot menu
     (toggleConvSidebarMoreMenu()/closeConvSidebarMoreMenu(), js/calendar.js)
     instead of the full row of controls desktop shows. !important needed —
     the New-Conversation/owner-filter wrapper has its own inline
     style="display:flex" (index.html), which otherwise beats this rule
     regardless of media query or source order since inline styles outrank
     any non-!important stylesheet selector. */
  .conv-sidebar-controls-desktop { display: none !important; }
  .conv-sidebar-more-wrap { display: block !important; }

  /* Hiding .conv-sidebar-controls-desktop above removes ITS row, but
     .conv-sidebar-header's own padding-top is untouched — that's the
     "leftover space" above the search bar on mobile (2026-07-14 feedback).
     Search+⋮ is the first thing shown on mobile, so it should sit flush at
     the top like every other mobile list header. Fully-flush (0px) then
     read as too tight once compared side-by-side with the topbar just
     above it (2026-07-19 feedback) — a small amount of breathing room
     restored, well short of the original desktop 12px this was cut down
     from.
     Reset to exactly match Contacts' own header padding (2026-07-26
     feedback) — bottom explicitly pinned to 10px too, instead of inheriting
     the desktop rule's --space-3 (12px), so the gap before the separator
     border-bottom below is identical to Contacts' own 10px gap (see
     #custPanel-main > .search-row further down).
     Bumped to var(--space-5) (2026-07-27 feedback: "sits too high...
     compared to the rest of the search bars, items, estimates") — Items/
     Estimates aren't full-bleed pages, so they get the standard --space-5
     gap above their search row for free via .content's own padding
     (layout.css); this full-bleed page has to add that same 20px itself
     since it sits flush against the topbar with no padding of its own.
     That reasoning undershot the real target (2026-07-19 feedback, "still
     not even close" — confirmed via direct live-DOM measurement in a
     mobile preview, not just re-reading CSS values on paper):
     #page-estimates-panel-main also carries its own inline
     margin-top:16px on top of .content's 20px (real measured search-bar
     top: 36px), and #custPanel-main's own .search-row adds a further 20px
     of padding-top on top of .content's 20px (real measured top: 40px) —
     both comfortably more than the 20px this rule was giving Conversations.
     Bumped by the same 16px Estimates' panel margin adds, landing at the
     same ~36px real top Estimates measures at. Also widened the left/right
     padding to match --space-5 (was --space-4/16px, 4px short of Estimates/
     Contacts' 20px left inset) so the horizontal edge lines up too. */
  .conv-sidebar-header { padding: calc(var(--space-5) + 16px) var(--space-5) 10px; }

  /* Unfreeze the search bar (2026-07-19 feedback: "don't freeze the search
     bar in conversations, let that slide up with the conversations") — on
     desktop .conv-sidebar-header sits outside .conv-list's own overflow:auto
     box, so it never scrolls; on mobile the whole sidebar (header + list)
     becomes one shared scroll container instead, so the search/filter row
     scrolls away with the thread list exactly like a native list header. */
  /* overscroll-behavior-y:contain (2026-07-20 feedback: "I can still scroll
     past the soft stop") — same fix as .content (layout.css) and
     .conv-messages, applied to this page's own scroll container so
     Conversations' list doesn't rubber-band past the keyboard-positioned
     nav bar either. */
  .conv-sidebar { overflow-y: auto !important; overscroll-behavior-y: contain; }
  .conv-list { overflow-y: visible !important; flex: none !important; }
  /* Separator below the search row (item b) is already covered — the base
     .conv-sidebar-header rule above has always had border-bottom:1px solid
     var(--border), on every breakpoint, so nothing further is needed here. */

  /* Internal Chat had NO mobile override at all here — stuck at desktop's
     12px padding-top, unlike Conversations/Contacts above (2026-07-14
     standardization pass). Brings its top gap in line with the other two
     full-bleed pages' 0px standard. (Internal Chat's two-column desktop
     grid layout not collapsing into a single mobile pane is a separate,
     larger gap — not addressed here, flagged for its own pass.) */
  .chat-sidebar-header { padding-top: 0; }

  /* Scoped to #page-conversations specifically — .conv-subtab is a shared
     class used by several other pages that don't have icon prefixes yet,
     so a global rule here would leave their tabs blank. */
  #page-conversations .conv-subtab .btn-label { display: none; }
  #page-conversations .conv-subtab { padding: 0 10px; font-size: 15px; }

  /* Mobile shows only the main Conversations tab — Snippets/Bulk Actions/
     Settings stay desktop-only for now (advanced/infrequent-on-mobile
     flows). The tab bar itself hides entirely rather than shrinking to
     one tab, since a lone tab with nothing to switch between is dead UI. */
  #page-conversations .conv-subtabs { display: none; }

  /* Same reasoning as Conversations above — Settings (Dashboard) and
     Contact Variables + Settings (Contacts) are desktop/advanced-only on
     mobile (2026-07-08 feedback), which leaves exactly one subtab in each
     bar, so the whole bar hides rather than showing a single dead tab. */
  #page-dashboard .conv-subtabs { display: none; }
  #page-customers .conv-subtabs { display: none; }

  /* Every remaining page whose ONLY subtabs are Main + Settings — same
     "whole bar hides, not a single dead tab" reasoning, extended app-wide
     (2026-07-08 feedback: Settings was still reachable via each page's own
     subtab bar on mobile even after moving to the central Settings page
     everywhere else; the cogwheel bottom-nav icon is the one way in now). */
  #page-estimates .conv-subtabs,
  #page-invoices .conv-subtabs,
  #page-payments .conv-subtabs,
  #page-timeclock .conv-subtabs,
  #page-employees .conv-subtabs,
  #page-jobqueue .conv-subtabs,
  #page-photos .conv-subtabs,
  #page-internalchat .conv-subtabs,
  #page-rewards .conv-subtabs,
  #page-reviews .conv-subtabs { display: none; }

  /* Automations (Forms), Items & Services (Items), and Marketing (Blasts)
     each have a real second subtab beyond Main that's still needed on
     mobile — only the Settings tab itself hides here, the bar stays. */
  #page-automations-subtab-settings,
  #page-inventory-subtab-settings,
  #page-social-subtab-settings { display: none; }

  /* Opportunities (2026-07-15: Reports/Settings hidden, Pipeline tab kept as
     the one real mobile view. 2026-07-17 feedback: with Reports/Settings
     already hidden, "Pipeline" was the only tab left in the bar — a single
     dead tab with nothing to switch between, same "whole bar hides instead
     of showing one lone tab" reasoning already applied to Contacts/
     Dashboard/etc. above. The actual Pipeline board content is untouched;
     only the now-pointless tab bar itself goes away.) */
  #page-pipeline .conv-subtabs { display: none; }

  /* "New Opportunity" is now redundant with the mobile FAB, which was
     changed (2026-07-15) to open this exact same stage-picker modal
     instead of a plain new-contact modal — keeping both would just be two
     buttons doing the same thing. Desktop keeps the toolbar button since
     there's no FAB there. */
  #pipeNewOppBtn { display: none; }

  /* Card Fields moves into the ⋮ more-menu on mobile (2026-07-16 feedback)
     — same .conv-more-mobile-only pattern as Conversations' own more-menu
     items, freeing up toolbar width for the smaller mobile screen. Desktop
     keeps the direct button since there's no crowding problem there. */
  #oppCardFieldsBtn { display: none; }

  /* Conversation compose box no longer needs its own reserved bottom
     padding — opening a conversation always puts the page in the
     "depth-hidden" state (setMobileDepthHidden(), js/ui.js), which already
     shrinks the containing .page-fullbleed's own bottom offset to hug the
     real screen edge once the bottom nav bar slides away. The static 78px
     here used to double-reserve that same space even after the bar was
     gone, which is exactly why the composer looked like it thought the
     bar was still there (2026-07-07 feedback). */
}

/* .conv-more-mobile-only items live in the same #convMoreMenu as the
   desktop-only Shared Media/Delete Conversation items — hidden here so
   desktop (where Edit/Favorite already have their own direct buttons)
   doesn't show the same action twice; shown only inside the mobile
   breakpoint above. */
.conv-more-mobile-only { display: none; }

/* Mobile-only "more" (⋮) button that collapses the sidebar's owner-filter
   select + New Conversation button + All/Unread/Favorites tabs — hidden on
   desktop, where .conv-sidebar-controls-desktop shows those controls directly. */
.conv-sidebar-more-wrap { display: none; }

/* ═══ CONTACTS — MOBILE RESPONSIVE LAYER ══════════════════════════
   Columns/Import CSV/Export CSV/Find Duplicates are desktop-only
   conveniences, hidden entirely on mobile (no collapsed-menu replacement —
   2026-07-08 feedback: remove the ⋮ "more" menu outright rather than
   keep it as a home for these). "+ New Contact" was never in that menu
   either, since the floating + FAB already opens the same modal on mobile.
   The 22-option sort dropdown (#custSort) is also desktop-only on mobile —
   replaced by .cont-sort-mobile's plain up/down direction buttons, which
   keep whatever field is already selected (setContactSortDirection(),
   js/db.js) instead of exposing the full field picker on a phone screen. */
@media (max-width: 768px) {
  /* !important needed on both — components.css/pages.css load order means
     an unconditional rule elsewhere can otherwise win over this media
     query regardless of viewport (bit twice by this already this session:
     .conv-sidebar-controls-desktop, .conv-sidebar-more-wrap). */
  .cont-toolbar-desktop { display: none !important; }
  .cont-sort-desktop { display: none !important; }
  .cont-sort-mobile { display: flex !important; padding-right: 0; }

  /* Desktop data table replaced by a card list on mobile — 8-10 columns
     of horizontally-scrolling small text was never going to be usable
     on a phone (2026-07-08 feedback). */
  #page-customers .table-wrap { display: none !important; }
  #contactCardList { display: flex !important; }

  /* Full-width contact list to match Conversations' list (2026-07-08
     feedback) — #custPanel-main's own inline padding:20px and the
     enclosing .card's border+padding were both constraining the card list
     narrower than the page-fullbleed container it actually sits in. The
     toolbar rows keep a little breathing room since they're plain text/
     controls, not a flush-edge list. Top padding dropped from 12px to 0
     (2026-07-14 standardization pass) — this page's own comment already
     said "to match Conversations," but Conversations sits flush at 0px
     (.conv-sidebar-header{padding-top:0}) while this was still leaving a
     12px gap; now genuinely matches instead of just citing it.
     Reset again (2026-07-26 feedback) — Conversations itself later moved to
     a 10px top padding (2026-07-19, felt too tight against the topbar at
     0px), leaving this page's 0px out of step with it; restoring the same
     10px here on .search-row keeps both pages' padding-above-the-search-bar
     identical again.
     Bumped to var(--space-5) + horizontal margin bumped from 16px to
     var(--space-5) (2026-07-27 feedback: "still sits a tiny bit too high
     and... shifted to the left a tiny bit compared to items, estimates") —
     those pages get the standard --space-5 (20px) gap/gutter for free via
     .content's own padding (layout.css) since they aren't full-bleed pages;
     this one has to match it explicitly on both axes. */
  #custPanel-main { padding: 0 !important; }
  /* overscroll-behavior-y:contain (2026-07-20 feedback: "a small bar peeks
     above the nav bar when the keyboard is open... I only see it in
     Contacts") — same fix as .content (layout.css); this page's list scrolls
     via its own inline overflow-y:auto (index.html) rather than .content, so
     it needed the same containment rule applied separately here. */
  #custPanel-main { overscroll-behavior-y: contain; }
  #custPanel-main > .search-row,
  #custPanel-main > #bulkActionBar { margin-left: var(--space-5); margin-right: calc(var(--space-5) - 7px); }
  /* Top padding + right margin each trimmed ~2px (2026-07-26 feedback: "padding
     above/to the right of the search bar is like 2px too much"). Top padding
     trimmed a further 2px (2026-07-21 feedback: "move search bar up by 2px"). */
  #custPanel-main > .search-row { padding-top: calc(var(--space-5) - 4px); }
  /* Separator between the search/sort row and the first contact card
     (2026-07-17 feedback) — matches Conversations' own .conv-sidebar-header
     border-bottom, which Contacts' search-row never had. */
  #custPanel-main > .search-row { padding-bottom: 10px; border-bottom: 1px solid var(--border); }
  #custPanel-main .card { border: none; border-radius: 0; padding: 0 12px; background: transparent; }

  /* Match Conversations' search bar (#convSearch) — fills the row instead
     of the desktop-sized 200px cap (2026-07-14 feedback). Inline style on
     the input wins over an unqualified rule, so !important is needed here
     too, same reasoning as the toolbar swaps above. */
  #custSearch { max-width: none !important; flex: 1 !important; }
}
.cont-sort-mobile { display: none; gap: 6px; }
/* gap:0 (2026-07-14 fix) — each card already carries its own border-bottom
   divider, matching .conv-item's plain-flowing-list style exactly; the
   previous 8px flex gap ON TOP of that border was double-spacing rows
   compared to Conversations, which relies on the border alone. */
#contactCardList { display: none; flex-direction: column; gap: 0; }
/* Flattened to a plain line-separated row (2026-07-12) — matches every
   other list in the app (Conversations, Estimates, etc.) instead of being
   the one holdout still using individually-boxed cards. */
.contact-mobile-card {
  display: flex; align-items: center; gap: 10px;
  border-bottom: 1px solid var(--border);
  padding: 11px var(--space-4); cursor: pointer;
}
.contact-mobile-card-body { min-width: 0; flex: 1; }
/* Sized to match Conversations' list rows (.conv-name/.conv-preview) exactly,
   rather than the smaller ad hoc sizing this list had on its own before.
   font-family set explicitly to Display (2026-07-14 fix) — the contact's own
   name is the "title" of this row, same convention already used for every
   other contact-name display in the app (.modal-title, .customer-name-big,
   the desktop table row's <strong> tag) rather than the ambient Body font
   it was silently falling back to before. */
/* font-family override removed (2026-07-17 feedback) — match Conversations'
   .conv-name, which inherits the plain body font instead of Space Grotesk. */
.contact-mobile-card-name { font-weight: 600; color: var(--text); font-size: var(--text-base); }
.contact-mobile-card-sub { font-size: var(--text-sm); color: var(--text3); margin-top: 2px; }
.contact-mobile-card-badges { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }

/* ═══ CALENDAR ═════════════════════════════════════════════════ */
.cal-header    { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-4); }
.cal-month     { font-family: var(--font-display); font-size: var(--text-lg); font-weight: 700; }
.cal-grid      { display: grid; grid-template-columns: repeat(7,1fr); gap: 4px; }
.cal-day-label { text-align: center; font-size: var(--text-2xs); color: var(--text3); text-transform: uppercase; letter-spacing: 1px; padding: 5px 0; font-weight: 600; }
.cal-day {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 7px; min-height: 132px; padding: 5px;
  cursor: pointer; transition: border-color var(--transition);
}
.cal-day:hover          { border-color: var(--border2); }
.cal-day.today          { border-color: #fff; border-width: 2px; padding: 4px; }
.cal-day.empty          { background: transparent; border-color: transparent; cursor: default; }
.cal-day.has-conflict   { border-color: var(--danger); }
.cal-conflict-badge     { font-size: var(--text-2xs); color: var(--danger); font-weight: 600; margin-top: 2px; }
.cal-day.over-capacity  { border-color: var(--warn); }
.cal-day.closed-day     { background: rgba(255,255,255,0.02); opacity: 0.55; }
.cal-capacity-badge     { font-size: var(--text-2xs); color: var(--warn); font-weight: 600; margin-top: 2px; }
.cal-date { font-size: var(--text-xs); color: var(--text3); font-weight: 600; margin-bottom: 3px; }
.cal-event { background: var(--accent); color: #fff; font-size: var(--text-2xs); border-radius: 3px; padding: 2px 4px; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* "+N more" overflow indicator — was completely unstyled before (inheriting
   whatever the ambient cell font happened to be), which is why it visually
   didn't match the rest of the month cell's text. Sized/weighted the same
   as the appointment chips (.cal-event) and conflict/capacity badges above,
   which all use 9px/600 as this calendar's small-print convention. */
.cal-more { font-size: var(--text-2xs); font-weight: 600; color: var(--text3); margin-top: 2px; }

/* Multi-day spanning bar (month view) — .cal-multiday-slot is an invisible
   placeholder rendered inside every cell in a row that has a multi-day job
   passing through it, purely to reserve consistent vertical space (so
   single-day chips still line up across the row); the actual visible bar is
   a separate .cal-month-bar overlay, absolutely positioned on top of it
   after layout so it can span continuously across multiple day cells. */
.cal-multiday-slot { height: 15px; margin-bottom: 2px; pointer-events: none; }

/* Calendar toolbar — previously entirely unstyled, so its 3 direct children
   (nav+title, view select, filters+button) stacked as full-width blocks
   instead of sitting on one row. */
.cal-toolbar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 14px; }
.cal-title { font-family: var(--font-display); font-size: var(--text-md); font-weight: 700; margin-left: 6px; white-space: nowrap; }
.cal-month-bar {
  position: absolute; color: #fff; font-size: var(--text-2xs); font-weight: 600;
  border-radius: 3px; padding: 2px 6px; white-space: nowrap; overflow: hidden;
  text-overflow: ellipsis; cursor: pointer; z-index: 2; box-sizing: border-box;
}
.cal-month-bar[draggable="true"] { cursor: grab; }

/* Curing tail (2026-07-27 feature; recolored 2026-08 per Ricardo's request) —
   the sibling bar drawn for the extra day(s) a job's curing time occupies
   beyond its real end_date. Used to be a dimmed/striped shade of the job's
   own color; now a fixed, distinct color (CURING_COLOR, set inline in JS)
   so "this is curing" reads at a glance regardless of which service it is,
   instead of looking like a washed-out version of the job's own chip. The
   diagonal-stripe overlay is kept at full opacity as texture, not as a
   dimming effect anymore. */
.cal-month-bar-curing {
  opacity: 1;
  background-image: repeating-linear-gradient(135deg, rgba(255,255,255,0.35) 0 6px, transparent 6px 12px);
}

/* Job-type (service) color coding — standalone classes so they apply to
   ANY element carrying them (calendar chips set color via inline style/
   gradient directly, but the color dot in Calendar Settings relies on
   these classes directly). Deliberately NOT the shared --accent/--info/
   --success/--warn variables — those also drive buttons/badges/toasts
   app-wide, so darkening them here to fix white-text-on-a-bright-chip
   readability would have re-colored unrelated UI. These are their own,
   darker, calendar-only shades of the same hues instead — keep this list
   and CLS_TO_HEX (js/calendar.js) in sync, they must always match. */
.svc-wrap       { background: #4f2178; }
.svc-ppf        { background: #295bac; }
.svc-ceramic    { background: #188a42; }
.svc-tint       { background: #98740a; color: #fff; }
.svc-chrome     { background: #6d727a; color: #fff; }
.svc-correction { background: #0e8174; }
.svc-detailing  { background: #ae500f; }
.svc-headlight  { background: #a5326b; }
.svc-consult    { background: #565b66; }
.svc-brown      { background: #6b4226; }
.svc-red        { background: #8f2323; }
.svc-navy       { background: #1e3a5f; }
.svc-lime       { background: #5a7a1a; }

/* ═══ WEEK VIEW ═══ (previously entirely unstyled — added alongside color coding) */
.cal-week-grid { display: grid; grid-template-columns: 60px repeat(7, 1fr); grid-auto-rows: 44px; }
.cal-week-header { padding: 6px 4px; text-align: center; font-size: var(--text-2xs); color: var(--text3); text-transform: uppercase; background: var(--surface2); }
.cal-week-header.today-col { background: rgba(93,39,141,0.12); }
.cal-week-header.closed-day { opacity: 0.45; }
.cal-time-label { font-size: var(--text-2xs); color: var(--text3); text-align: right; padding: 2px 6px 0 0; border-bottom: 1px solid var(--border); }
.cal-week-cell { position: relative; border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); cursor: pointer; }
.cal-week-cell:hover { background: var(--surface2); }
.cal-week-cell.today-col { background: rgba(93,39,141,0.06); }
.cal-week-cell.off-hours { background: rgba(0,0,0,0.18); }
/* ═══ DAY VIEW ═══ (previously had zero layout CSS — rendered as an
   unstyled vertical text stack) */
.cal-day-view { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
.cal-day-view-header { padding: 12px 16px; background: var(--surface2); font-size: var(--text-base); font-weight: 700; border-bottom: 1px solid var(--border); }
.cal-day-slot { display: grid; grid-template-columns: 70px 1fr; border-bottom: 1px solid var(--border); min-height: 64px; }
.cal-day-slot:last-child { border-bottom: none; }
.cal-day-slot.off-hours  { background: rgba(0,0,0,0.18); }
.cal-day-time { font-size: var(--text-xs); color: var(--text3); padding: 8px 10px 0 0; text-align: right; border-right: 1px solid var(--border); }
.cal-day-events { padding: 8px 10px; display: flex; flex-direction: column; gap: 6px; cursor: pointer; }
.cal-day-event-card { border-radius: 8px; padding: 8px 12px; color: #fff; cursor: pointer; }
.cal-day-event-card.terminal { opacity: 0.55; text-decoration: line-through; }
.cal-day-event-card-title { font-weight: 700; font-size: var(--text-sm); margin-bottom: 3px; }
.cal-day-event-card-row { font-size: var(--text-xs); opacity: 0.92; line-height: 1.5; }
.cal-week-event { position: absolute; left: 2px; right: 2px; color: #fff; font-size: var(--text-2xs); border-radius: 3px; padding: 2px 4px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; z-index: 1; cursor: pointer; }

/* Same-day curing tail (2026-07-27 feature; recolored 2026-08) — child strip
   stacked at the bottom of a short (<=4hr) same-day cure's chip. Now uses
   the fixed CURING_COLOR (inline background, set in JS) instead of a dimmed
   shade of the parent job's color, matching the month-view equivalent
   (.cal-month-bar-curing) — see that rule's comment for why. Positioned
   relative to .cal-week-event, which is already `position:absolute` — a
   valid containing block for this child. */
.cal-week-event-curing {
  position: absolute; left: 0; right: 0;
  opacity: 1;
  background-image: repeating-linear-gradient(135deg, rgba(255,255,255,0.35) 0 6px, transparent 6px 12px);
}

/* Drag-and-drop rescheduling — drop-target hover state, shared by month/week/day views */
.cal-day.drag-over, .cal-week-cell.drag-over, .cal-day-events.drag-over { background: rgba(93,39,141,0.18); outline: 2px dashed var(--accent-light); outline-offset: -2px; }

/* ═══ PRINT VIEWS (Day Sheet, Work Order) ═══
   #printArea sits empty and invisible in normal use; printDaySheet()/
   printWorkOrder() (calendar.js) fill it with plain black-on-white HTML and
   call window.print(). This block hides everything else on the page for
   the duration of the print, showing only #printArea's content. */
#printArea { display: none; }
@media print {
  body * { visibility: hidden; }
  #printArea, #printArea * { visibility: visible; }
  #printArea { display: block; position: absolute; top: 0; left: 0; width: 100%; color: #000; background: #fff; padding: 24px; }
  .print-title    { font-family: var(--font-display); font-size: var(--text-xl); font-weight: 700; margin-bottom: 4px; }
  /* Everything below the title bumped 2 steps up the font-size scale
     (--text-sm -> --text-md, 2026-07-28 feedback) — was uniformly --text-sm. */
  .print-subtitle { font-size: var(--text-md); color: #444; margin-bottom: 16px; }
  .print-section-header { font-size: var(--text-md); font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 1px solid #999; padding-bottom: 4px; margin: 14px 0 6px; }
  .print-row      { font-size: var(--text-md); margin-bottom: 3px; }
  .print-row b     { display: inline-block; min-width: 110px; }
  .print-table     { width: 100%; border-collapse: collapse; font-size: var(--text-md); margin-top: 6px; }
  .print-table th, .print-table td { border: 1px solid #999; padding: 5px 7px; text-align: left; }
  .print-table th  { background: #eee; }

  /* Signature + date line at the bottom of the Work Order (2026-07-28
     feedback) — plain underline + label, no input/canvas needed since this
     is a printed paper form filled in by hand on the shop floor. */
  .print-signature-section { display: flex; gap: 40px; margin-top: 36px; }
  .print-signature-col      { flex: 1; }
  .print-signature-col-narrow { flex: none; width: 160px; }
  .print-signature-line    { border-bottom: 1px solid #333; height: 32px; }
  .print-signature-label   { font-size: var(--text-md); color: #444; margin-top: 4px; }
  /* Pins both signature rows (Technician + Customer) to the bottom of the
     printed page (2026-07-29 feedback: "move the print signature to the
     footer of the page") instead of wherever the content happened to end —
     position:fixed repeats on every printed page, which is exactly how a
     real page footer behaves. */
  .print-signature-footer { position: fixed; bottom: 24px; left: 24px; right: 24px; }

  /* Estimate letterhead document (previewEstimate(), js/calendar.js,
     2026-07-10) — a proper invoice/estimate-style layout instead of the
     plain print-row list above, using Flow's own brand colors. Needs
     explicit color-adjust overrides since #printArea forces black-on-white
     by default and browsers drop background colors on print unless told
     otherwise. */
  #printArea:has(.est-doc) { padding: 0; }
  .est-doc, .est-doc * { -webkit-print-color-adjust: exact; print-color-adjust: exact; color-adjust: exact; }
}

/* Estimate letterhead document (previewEstimate(), js/calendar.js) — kept
   OUTSIDE @media print (2026-07-11 fix) so the in-app preview modal shows
   this exact same designed "paper" look instead of raw unstyled HTML; the
   printed/PDF output (doPrint(), same estPreviewHtml string) was always
   using these rules already since it's real page content at print time —
   only the on-screen preview was missing them, which is why the two used
   to look like completely different documents even though the underlying
   HTML was identical. */
/* Base font for the whole document — Settings → Estimates/Invoices →
   Document Font (--doc-font-body/--doc-font-display, see applyDocFonts() in
   js/ui.js), independent of whatever font the app's own Appearance settings
   use. Every .est-doc-* rule below inherits this via normal CSS cascade
   except the few that explicitly opt into the display face (headline-style
   text), which reference --doc-font-display directly. */
.est-doc { font-family: var(--doc-font-body); }
/* Light header bar (2026-07-25 batch 4) — was a dark bar (#34343b) with
   white text, which silently hid a dark-colored logo against it (the exact
   same problem already fixed on view-estimate.html's header). Logo also
   enlarged to match that same fix. */
.est-doc-header { display: flex; justify-content: space-between; align-items: flex-start; background: #f4f4f5; color: #111; padding: 28px 32px; }
.est-doc-logo { max-height: 100px; max-width: 320px; object-fit: contain; margin-bottom: 6px; }
.est-doc-bizname { font-family: var(--doc-font-display); font-size: 20px; font-weight: 700; color: #111; }
.est-doc-contact { font-size: 10px; color: #000; line-height: 1.5; }
.est-doc-header-right { min-width: 150px; }
.est-doc-doctype { font-size: 22px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--accent); text-align: right; }
.est-doc-num { font-size: 16px; font-weight: 700; color: #111; text-align: right; margin: 2px 0 8px; }
.est-doc-pill { display: inline-block; font-size: 10px; font-weight: 700; text-transform: uppercase; padding: 3px 10px; border-radius: 20px; color: #fff; }
.est-doc-body { padding: 20px 26px; color: #111; background: #fff; }
.est-doc-created-for { font-size: 12px; font-weight: 700; color: #111; margin-bottom: 6px; }
.est-doc-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 20px; font-size: 12px; margin-bottom: 6px; }
.est-doc-info-grid b { color: #555; font-weight: 600; margin-right: 6px; text-transform: uppercase; font-size: 10px; }
.est-doc-section-header { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: var(--accent); border-bottom: 2px solid var(--accent); padding-bottom: 4px; margin: 18px 0 8px; }
.est-doc-table { width: 100%; border-collapse: collapse; font-size: 12px; background: #fff; table-layout: fixed; }
.est-doc-table th { text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: normal; color: #666; background: #fff; padding: 6px 8px; border-bottom: 1px solid #ccc; font-weight: 600; white-space: normal; }
.est-doc-table td { padding: 7px 8px; border-bottom: 1px solid #eee; color: #222; font-size: 12px; vertical-align: top; }
.est-doc-table th:nth-child(n+3), .est-doc-table td:nth-child(n+3) { text-align: right; padding-left: 4px; padding-right: 4px; }
/* Real card treatment (2026-07-26) — background/border/radius/padding
   matching the Estimate slider's own .est-pkg-card-totals.card box exactly
   (that class list is literally `.card`, the app's shared card component),
   instead of a bare unstyled block of rows. Sits to the right in a flex row
   now (see estDocPackageBlockHtml), so margin-left:auto is no longer needed
   to push it right. */
.est-doc-totals { width: 280px; flex-shrink: 0; background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 12px 14px; }
/* Font size increased 2 tokens (+4px, 2026-07-26 feedback) — was reduced 2
   tokens on 2026-07-18; this reverses that back to the original size. */
.est-doc-total-row { display: flex; justify-content: space-between; font-size: 12px; padding: 3px 0; color: #444; }
.est-doc-grand-total { display: flex; justify-content: space-between; align-items: center; background: var(--accent); color: #fff; font-size: 11px; font-weight: 700; padding: 10px 14px; border-radius: 6px; margin-top: 8px; }
.est-doc-notes { font-size: 11px; color: #333; line-height: 1.5; white-space: pre-wrap; }
/* Terms & Conditions / category disclaimer text only (2026-07-26 feedback)
   — smaller and tighter than the general Notes section above, which shares
   .est-doc-notes' base styling but wasn't part of this request. */
.est-doc-notes-compact { font-size: 10px; line-height: 1.05; }
.est-doc-footer { margin-top: 28px; padding-top: 14px; border-top: 1px solid #ccc; font-size: 10px; color: #777; }
/* Signature block (2026-07-26) — moved out of the footer into its own
   section above Terms & Conditions; unsigned placeholder is just a blank
   underline, no longer isolated at the very bottom of the page with a large
   top margin the way the old footer-only version needed. */
.est-doc-sig-block { display: flex; align-items: flex-end; gap: 24px; margin-top: 4px; }
.est-doc-sig-line { width: 220px; height: 1px; border-top: 1px solid #999; }
.est-doc-sig-img { max-height: 70px; max-width: 220px; object-fit: contain; border-bottom: 1px solid #999; padding-bottom: 4px; }
.est-doc-sig-meta { display: flex; gap: 20px; font-size: 11px; color: #333; }
.est-doc-sig-label { font-size: 10px; color: #888; text-transform: uppercase; letter-spacing: 0.4px; }
/* "Items Accepted" summary (2026-07-26) — a concise recap sitting above the
   full Packages section on an accepted estimate. */
.est-doc-accepted-summary { background: #f8f6fb; border: 1px solid var(--accent); border-radius: 6px; padding: 10px 14px; margin-bottom: 4px; }
.est-doc-accepted-row { display: flex; justify-content: space-between; font-size: 12.5px; font-weight: 600; color: #222; padding: 3px 0; }
/* Multi-package printable cards (2026-07-11) — when an estimate has more
   than one package, previewEstimate() wraps each package's line-items
   table through its own deposit-on-acceptance row in one of these cards
   with a title header, instead of only ever showing the primary package's
   numbers flattened into the top-level layout. */
.est-doc-package-card { border: 1px solid #ddd; border-radius: 8px; margin: 18px 0; overflow: hidden; position: relative; }
.est-doc-package-title { background: #f2f2f4; color: #222; font-size: 13px; font-weight: 700; padding: 10px 16px; border-bottom: 1px solid #ddd; }
.est-doc-package-card .est-doc-table { width: 100%; margin-left: 0; margin-right: 0; }
.est-doc-package-card .est-doc-totals { width: 280px; }
/* Accepted-package/add-on treatment (2026-07-11) — once an estimate is
   Accepted and had more than one package/add-on to choose from, the one the
   customer actually picked gets a purple outline and a green ACCEPTED
   badge, and previewEstimate() moves it to the front of its section. */
.est-doc-package-card.accepted { border: 2px solid var(--accent); }
.est-doc-accepted-badge {
  position: absolute; top: -10px; right: 14px; z-index: 1;
  background: var(--success); color: #fff; font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.5px; padding: 3px 10px; border-radius: 20px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.25);
}
/* Package-card variant — sits inline in the title row, centered with the
   package name, instead of floating above/outside the card's top edge. */
.est-doc-accepted-badge-inline {
  background: var(--success); color: #fff; font-size: 10px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.5px; padding: 3px 10px; border-radius: 20px;
  flex-shrink: 0;
}
.est-doc-package-card-body { padding: 14px 16px; }
/* Add-on cards (2026-07-15 fix) have no title bar to hold the ACCEPTED pill
   inline the way a package card does (.est-doc-accepted-badge-inline) — they
   fall back to the floating .est-doc-accepted-badge, but that variant sits
   at top:-10px (partly ABOVE the card's own box), which .est-doc-package-card's
   overflow:hidden then clips clean off since it's outside the box it's
   positioned relative to. Add-on cards get their own badge position (fully
   inside the box, never negative) plus matching body padding so the pill
   has room to sit above the table header without overlapping it. */
.est-doc-addon-card .est-doc-accepted-badge { top: 10px; }
.est-doc-addon-card.accepted .est-doc-package-card-body { padding-top: 34px; }
.cal-event[draggable="true"], .cal-week-event[draggable="true"] { cursor: grab; }

/* Mobile appointment cards (renderUpcomingApptMobileCards(), js/calendar.js)
   — reused by the day-tap agenda list (#calDayApptList below); the
   "Upcoming Appointments" section these were originally built for was
   deleted entirely (2026-07-14 — redundant with auto-selecting today's
   date on load, and never shown on desktop in the first place). */
/* Flat divider-row style (2026-07-14 fix) — matches Conversations'/Contacts'
   plain-flowing-list convention instead of being the one holdout still
   using individually-boxed/bordered tiles. */
.cal-appt-mobile-card {
  border-bottom: 1px solid var(--border);
  padding: 11px var(--space-4); cursor: pointer;
}
.cal-appt-mobile-card.past      { opacity: 0.6; }
.cal-appt-mobile-card.terminal  { opacity: 0.45; text-decoration: line-through; }
.cal-appt-mobile-card-row  { display: flex; align-items: center; gap: 6px; justify-content: space-between; }
.cal-appt-mobile-card-datetime { font-size: var(--text-xs); color: var(--text3); font-weight: 600; }
.cal-appt-mobile-card-name { font-family: var(--font-display); font-weight: 700; color: var(--text); font-size: var(--text-base); margin-top: 4px; }
.cal-appt-mobile-card-sub  { font-size: var(--text-sm); color: var(--text3); margin-top: 2px; }
.cal-appt-mobile-card-badges { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }

/* ═══ CALENDAR — MOBILE RESPONSIVE LAYER ══════════════════════════
   2026-07-08 feedback. Same "hide the whole subtabs bar rather than a lone
   dead tab" reasoning as Dashboard/Contacts — Settings moves to the central
   Settings page anyway (goToPageSettings()), and Pending Bookings becomes
   an always-visible inline section instead of a tab (see
   .cal-mobile-inline-section below), so nothing is left in the bar to
   switch between. */
@media (max-width: 768px) {
  #page-calendar > .conv-subtabs { display: none; }

  /* Forces the Pending Bookings panel to always render on mobile,
     regardless of which desktop tab is internally marked .active (there's
     no tab UI to click on mobile at all once the bar above is hidden) —
     !important needed since .page-subtab-panel's un-active display:none
     lives in layout.css, loaded before this file, same cascade-order
     pattern as everywhere else in this project. */
  .cal-mobile-inline-section { display: block !important; }

  /* Pending Bookings' desktop table header — a cramped 5-column header row
     makes no sense once the rows below it are read one at a time on a phone
     (2026-07-14 feedback); the row content itself still renders below it. */
  #pendingBookingsThead { display: none; }

  /* Waitlist entries — desktop keeps the boxed .card look (unchanged,
     out of scope for this mobile-only pass); on mobile flatten to the same
     divider-row style as every other list (Conversations/Contacts/the
     appointment cards above) instead of being its own boxed-card holdout. */
  .cal-waitlist-entry {
    background: none; border: none; border-radius: 0;
    border-bottom: 1px solid var(--border);
    padding: 11px var(--space-4) !important;
    margin-bottom: 0 !important;
  }

  /* Toolbar (2026-07-08 feedback: both rows were overflowing off the right
     edge of the screen — the filter/action row alone had 2 selects + 3
     buttons fighting for one line with no wrap, so "+ Schedule Appointment"
     and the view-select were rendering entirely off-screen, not just
     visually cramped). flex-wrap here is what actually fixes that — any
     control that doesn't fit drops to its own line instead of being
     clipped. Print Day Sheet and Legend are desktop-only conveniences,
     hidden here rather than shrunk further; "+ Schedule Appointment" is
     hidden too since the mobile FAB (FAB_ACTIONS.calendar, js/ui.js)
     already opens the exact same modal — no need for both. */
  .cal-toolbar { flex-direction: column; align-items: stretch; gap: 8px; }
  .cal-toolbar > div { justify-content: space-between; flex-wrap: wrap; row-gap: 8px; }
  .cal-title { flex: 1; }
  /* Month-only on mobile (2026-07-08 feedback) — Week/Day's per-hour grids
     don't fit a phone width at all; renderCalendar() (js/calendar.js) also
     force-sets calView='month' so hiding this can't be worked around by a
     lingering Settings default. */
  #calViewSelect { display: none !important; }
  #calTechFilter, #calTypeFilter { flex: 1 1 47%; width: auto !important; min-width: 0; }
  .cal-toolbar button[onclick^="printDaySheet"] { display: none; }
  .cal-toolbar button[onclick="openModal('event',null)"] { display: none; }
  .cal-toolbar button[onclick="openModal('internalBlock',null)"] { flex: 1 1 100%; }

  /* Month/week grids — shrink to fit 7 columns in a phone-width screen. */
  .cal-day { min-height: 60px; padding: 3px; }
  /* font-size dropped from these mobile overrides (2026-07-14 standardization
     pass) — their old 8px/9px values are below the token floor (--text-2xs,
     10px) and the "no exceptions" bump target is identical to what the base
     (desktop) rules above already resolved to, so re-stating it here would
     be a no-op; only the genuinely-still-distinct mobile padding/spacing
     tweaks remain. .cal-date keeps its own override since its base is xs
     (11px), one step above the 2xs floor these mobile versions bump to. */
  .cal-date { font-size: var(--text-2xs); margin-bottom: 1px; }
  .cal-event, .cal-week-event, .cal-month-bar { padding: 1px 3px; }
  .cal-day-label { padding: 3px 0; letter-spacing: 0; }
  .cal-week-grid { grid-template-columns: 34px repeat(7, 1fr); grid-auto-rows: 36px; }
  .cal-time-label { padding: 1px 2px 0 0; }
  .cal-week-header { padding: 4px 2px; }

  /* Full-width grid (2026-07-08 feedback) — .page's standard 20px side
     padding otherwise keeps the calendar narrower than the screen; bleeds
     it to the true edges with the same negative-margin escape the
     subtabs bar above already uses (margin:-20px -20px 0), rather than
     restructuring .page itself. */
  #calBody, #calDayApptSection { margin-left: -20px; margin-right: -20px; }

  /* Tapped-day highlight — renderMonthView()'s cell.onclick calls
     showMobileDayAppointments() on mobile instead of opening the New
     Appointment modal; this just marks which cell's appointments are
     currently listed below the grid. */
  .cal-day.selected { border-color: var(--accent); border-width: 2px; padding: 2px; }

  /* Day's-appointments list revealed below the grid on tap — header +
     the reused .cal-appt-mobile-card list (see its shared styling above). */
  .cal-day-appt-mobile-only { padding: 12px 20px 0; }
  .cal-day-appt-header { font-size: var(--text-sm); font-weight: 700; margin-bottom: 8px; }
  #calDayApptList { display: flex; flex-direction: column; gap: 0; }

  /* Pending Bookings, surfaced directly under the calendar on mobile
     (2026-07-27 feedback) — same padding/header treatment as the day-
     appointments section right above it, always visible (not tap-to-
     reveal like that one) since it isn't scoped to a single selected day. */
  .cal-mobile-pending-only { display: block; padding: 20px 20px 0; margin-left: -20px; margin-right: -20px; }
  /* Simplified to name + 3-dot menu only (2026-07-26 feedback) — a single
     row instead of a stacked card now that date/service/day-prior-dropoff
     details no longer render inline (see renderPendingBookingsList(),
     js/calendar.js). */
  .cal-mobile-pending-card {
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    border-bottom: 1px solid var(--border); padding: 10px 0; cursor: pointer;
  }
  .cal-mobile-pending-name { font-weight: 600; color: var(--text); font-size: var(--text-base); }

  /* Self-Booking Requests panel forced inline on mobile (see
     .cal-mobile-inline-section above) — 2026-07-29 feedback additions:
     a section title of its own (desktop already has this as its subtab
     label, so hidden there — see the non-mobile default below), the
     redundant standalone "Generate a Booking Link" button hidden (same
     action is one tap away in the Calendar toolbar's own 3-dot menu on
     mobile now, see toggleCalToolbarMoreMenu()), and the desktop <table>
     swapped for a card list matching #calDayApptList's own row format
     (populated by renderPendingBookingsList(), js/calendar.js). */
  #pendingReqMobileTitle { display: block !important; margin: 0 0 12px; }
  #calMobileGenBookingLinkBtn { display: none !important; }
  #page-calendar-panel-pendingBookings .table-wrap { display: none !important; }
  #pendingBookingsCardList { display: flex !important; flex-direction: column; }
}
/* Hidden by default (desktop) — Pending Bookings stays behind its own
   subtab there, this compact card list is mobile-only. */
.cal-mobile-pending-only { display: none; }
/* Desktop defaults for the 2026-07-29 mobile additions above — the subtab
   bar already shows "Self-Booking Requests" as its own tab label there, and
   the desktop table is the real list, so both stay hidden outside the
   mobile block. */
#pendingReqMobileTitle { display: none; }
#pendingBookingsCardList { display: none; }

/* ═══ INTERNAL CHAT ════════════════════════════════════════════ */
.chat-layout { display: grid; grid-template-columns: 260px 1fr; height: 100%; background: var(--surface); overflow: hidden; }
.chat-sidebar { border-right: 1px solid var(--border); display: flex; flex-direction: column; }
.chat-sidebar-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;
}
.chat-thread-item {
  padding: 10px var(--space-4); cursor: pointer;
  border-bottom: 1px solid var(--border);
  transition: background var(--transition);
  display: flex; align-items: center; gap: var(--space-3);
}
.chat-thread-item:hover  { background: var(--surface2); }
.chat-thread-item.active { background: var(--accent-glow); border-left: 3px solid var(--accent); }
.chat-thread-avatar {
  width: 34px; height: 34px; border-radius: 50%;
  background: var(--accent); display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 700; color: #fff; flex-shrink: 0;
}
.chat-thread-avatar.group { background: var(--surface3); border: 1px solid var(--border2); font-size: 14px; }
.chat-thread-name    { font-size: var(--text-base); font-weight: 600; }
.chat-thread-preview { font-size: 11px; color: var(--text3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-main    { display: flex; flex-direction: column; }
.chat-topbar  { padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: var(--space-3); flex-shrink: 0; }
.chat-messages{ flex: 1; overflow-y: auto; padding: var(--space-4); display: flex; flex-direction: column; gap: var(--space-3); }
.chat-bubble-row       { display: flex; gap: var(--space-2); align-items: flex-end; }
.chat-bubble-row.mine  { flex-direction: row-reverse; }
.chat-bubble {
  max-width: 65%; padding: 8px 12px;
  border-radius: 12px; font-size: var(--text-base); line-height: 1.5; word-break: break-word;
}
.chat-bubble.theirs { background: #1e1e2e; border: 1.5px solid rgba(93,39,141,0.35); color: var(--text); border-bottom-left-radius: 4px; }
.chat-bubble.mine   { background: var(--accent); border: 1.5px solid rgba(192,192,192,0.2); color: #fff; border-bottom-right-radius: 4px; }
.chat-compose { padding: var(--space-3) var(--space-4); border-top: 1px solid var(--border); display: flex; gap: var(--space-2); align-items: flex-end; flex-shrink: 0; }
.chat-compose textarea { flex: 1; min-height: 44px; resize: none; }

/* Back-to-thread-list button — only ever shown on mobile (see media query
   below); same pattern as .conv-back-btn (Conversations). */
.chat-back-btn { display: none; }

/* ═══ INTERNAL CHAT — MOBILE RESPONSIVE LAYER ═══════════════════
   Same codebase as desktop — desktop's fixed 260px-sidebar + main
   two-column grid never collapsed for mobile, so both panes tried to
   render side by side on a phone (2026-07-14 feedback). Mirrors
   Conversations' single-full-width-pane-at-a-time pattern exactly:
   thread list by default, or the open chat once a thread is tapped
   (toggled via .mobile-thread-open, set/cleared by
   openChatThread()/closeMobileChat() in js/snippets.js). ═══════════ */
@media (max-width: 768px) {
  .chat-layout { grid-template-columns: 1fr; }
  .chat-sidebar { display: flex; }
  .chat-main    { display: none; }
  .chat-layout.mobile-thread-open .chat-sidebar { display: none; }
  .chat-layout.mobile-thread-open .chat-main    { display: flex; }

  .chat-back-btn { display: inline-flex !important; }
}

/* ═══ PIPELINE STAGE PILLS ═════════════════════════════════════ */
.stage-pill {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px 3px 12px; border-radius: var(--radius-pill);
  font-size: 11px; font-weight: 600; cursor: pointer; user-select: none;
  transition: all var(--transition);
  border: 1px solid var(--accent-border); background: var(--accent-glow);
  color: var(--accent-bright); white-space: nowrap;
}
.stage-pill:hover { background: rgba(93,39,141,0.22); }
.stage-pill-x    { font-size: var(--text-2xs); color: rgba(192,152,252,0.6); cursor: pointer; border-radius: 50%; padding: 0 1px; transition: all 0.12s; }
.stage-pill-x:hover { color: var(--danger); }
.stage-pills-wrap { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
.add-stage-btn    {
  padding: 3px 10px; border-radius: var(--radius-pill);
  border: 1px dashed var(--border2); background: transparent;
  color: var(--text3); font-size: 11px; font-weight: 600;
  cursor: pointer; transition: all var(--transition); font-family: var(--font-body);
}
.add-stage-btn:hover { border-color: var(--accent); color: var(--accent-bright); }
.stage-picker-dropdown {
  position: fixed; background: var(--surface); border: 1px solid var(--border2);
  border-radius: var(--radius); padding: 6px; z-index: 2000;
  box-shadow: var(--shadow-lg); min-width: 240px; max-height: 320px; overflow-y: auto;
}
.stage-picker-group  { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--text3); padding: 6px 8px 3px; }
.stage-picker-option { padding: 6px 10px; border-radius: 6px; font-size: 12px; cursor: pointer; color: var(--text2); transition: background var(--transition); }
.stage-picker-option:hover { background: var(--surface2); color: var(--text); }

/* ═══ ESTIMATES — multi-package tabs (2026-07-08) ═══════════════ */
.est-pkg-tab {
  display: flex; align-items: center; gap: 6px; border: 1px solid var(--border2); border-radius: 8px;
  background: var(--surface2); padding: 5px 8px; cursor: pointer;
}
.est-pkg-tab.active { border-color: var(--accent-border); background: var(--accent-glow); }
.est-pkg-tab-name {
  border: none; background: transparent; color: var(--text); font-size: 12px; font-weight: 600;
  width: 100px; padding: 0;
}
.est-pkg-tab-total { font-size: 11px; color: var(--text3); white-space: nowrap; }

/* Full lockdown once an estimate is Accepted (js/calendar.js
   setEstimateLockState(), 2026-07-11) — blocks every interactive element
   inside the modal in one shot (line pickers, add/remove buttons, discount
   field, AI Suggest) without needing to enumerate each one individually.
   modal-close and the footer stay clickable so Close/Cancel still work. */
#modal-estimate.est-locked .modal-title-row ~ *:not(.modal-footer) {
  pointer-events: none;
  opacity: 0.65;
  user-select: none;
}
/* Read-only mode must still scroll (2026-07-14 feedback) — the rule above
   targets #estDetailScroll itself (it's the .modal-title-row sibling being
   matched), which blocks touch/drag-scroll on the container along with
   every real control inside it. Give the scroll container itself back its
   pointer-events so wheel/touch/drag-scroll all keep working, then re-apply
   the same none/opacity treatment one level down (to its direct children)
   so every actual field/button is still fully locked — pointer-events is
   inherited, so this reproduces the original "everything inside is
   unusable" effect without the container itself losing hit-testing. */
#modal-estimate.est-locked #estDetailScroll,
#modal-estimate.view-locked #estDetailScroll {
  pointer-events: auto !important;
  opacity: 1 !important;
}
#modal-estimate.est-locked #estDetailScroll > *,
#modal-estimate.view-locked #estDetailScroll > * {
  pointer-events: none;
  opacity: 0.65;
  user-select: none;
}
/* Re-assert the app-wide "these stay clickable even while locked" exceptions
   at higher specificity than the blanket children rule just above. */
#modal-estimate.est-locked #estDetailScroll .info-tip,
#modal-estimate.view-locked #estDetailScroll .info-tip,
#modal-estimate.est-locked #estDetailScroll .ec-section-header,
#modal-estimate.view-locked #estDetailScroll .ec-section-header {
  pointer-events: auto !important;
  opacity: 1 !important;
}

/* Same fix, retargeted to the Invoice modal now that its content is wrapped
   in #invDetailScroll too (2026-07-25 feedback: bring the Estimate modal's
   slide-in treatment over to Invoices). No .inv-locked equivalent to
   Estimate's full-Accepted lockdown exists, so only the generic
   .modal-overlay.view-locked case (components.css) needs covering here. */
#modal-invoice.view-locked #invDetailScroll {
  pointer-events: auto !important;
  opacity: 1 !important;
}
#modal-invoice.view-locked #invDetailScroll > * {
  pointer-events: none;
  opacity: 0.65;
  user-select: none;
}
#modal-invoice.view-locked #invDetailScroll .info-tip,
#modal-invoice.view-locked #invDetailScroll .ec-section-header {
  pointer-events: auto !important;
  opacity: 1 !important;
}

/* Base padding (applies at every width, including mobile) — the desktop
   @media block below overrides this with wider side padding. Deliberately a
   real CSS rule, not an inline style on the element in index.html, so the
   desktop override below can actually win (2026-07-18 fix). */
#estDetailScroll {
  /* Side padding reduced 20px -> 10px (2026-07-29 feedback: "reduce the
     side padding on all modals to 10px") — matches the same flat 10px now
     applied to every other mobile modal's own .modal padding
     (css/layout.css); desktop's own wider override below is unaffected. */
  padding: 0 10px 20px;
}
/* Same base padding, retargeted to the Invoice modal's own scroll wrapper. */
#invDetailScroll {
  padding: 0 10px 20px;
}
/* Mobile double-padding fix (2026-07-21 feedback: "match the padding to the
   rest of the modals at 20px") — the desktop block below zeroes out
   #modal-estimate .modal's own padding so #estDetailScroll's 20px is the
   only padding layer, but that reset was scoped to min-width:769px only
   ("mobile keeps its existing bottom-sheet treatment untouched," per the
   comment above). On mobile, layout.css's bottom-sheet override still puts
   20px padding on .modal itself, stacking with #estDetailScroll's own 20px
   for an effective 40px on each side — every other bottom-sheet modal has
   just the one 20px layer since they don't have a second inner scroll
   wrapper like this one does. Zeroing .modal's padding here too makes this
   modal match everything else at a true 20px. */
@media (max-width: 768px) {
  /* overflow:hidden added (2026-07-29 feedback: "the header is not truly
     frozen — when we scroll up, it scrolls upwards slightly as well").
     .modal's own base rule (components.css) sets overflow-y:auto, making
     .modal itself a SECOND, real scroll container on top of
     #estDetailScroll's own flex:1;overflow-y:auto below — normally .modal
     never has enough overflow to actually scroll since #estDetailScroll
     absorbs all real content height, but it's still a live scroll container
     that can shift by a stray pixel or two on a scroll gesture, and the
     title row's position:sticky sticks relative to THAT scroll, not to
     #estDetailScroll's — so any tiny drift in .modal's own scroll position
     reads as the "frozen" header nudging along with it. Locking .modal to
     overflow:hidden removes the second scroll container entirely; all real
     scrolling continues to happen only inside #estDetailScroll, which was
     always the intended single scroll area on this modal's flex-column
     layout (title row/footer flex-shrink:0, #estDetailScroll flex:1).
     REGRESSION FIX (2026-07-31 feedback: "I cannot scroll through the
     modal, it's frozen in place") — this rule set overflow:hidden but
     never actually added display:flex;flex-direction:column here (mobile
     only had that structure from #estDetailScroll's own inline flex:1,
     which does nothing without a flex CONTAINER declared on its parent).
     Desktop's own separate slide-in rule below already sets this
     correctly; mobile silently never had it, so once .modal's overflow
     became hidden, #estDetailScroll's flex:1/overflow-y:auto had no effect
     at all and any content taller than the modal's fixed height was simply
     clipped with no way to reach it. */
  #modal-estimate .modal { padding: 0 !important; overflow: hidden; display: flex; flex-direction: column; }
  /* .modal-title-row's base rule (components.css) bleeds through its parent's
     padding via a negative margin sized to var(--space-6) (24px), assuming
     the .modal box actually has that much padding to bleed through — but the
     rule directly above zeroes #modal-estimate .modal's padding on mobile
     (so #estDetailScroll's own 20px is the only padding layer, avoiding
     double-padding). With nothing to bleed through, that negative margin
     instead pushes the row past the panel's real edges, and .modal's
     overflow-y:auto (components.css) computes overflow-x to auto too,
     clipping the row — this is why the AI Suggest/Edit buttons on the right
     looked cut off/misaligned on mobile (2026-07-24 feedback). Reset to a
     plain, non-bleeding row matching #estDetailScroll's own 20px gutter.
     position:static (2026-07-29) — now that .modal itself never scrolls
     (see overflow:hidden above), sticky positioning has nothing left to
     stick against; the row is already visually frozen for free by simply
     sitting outside #estDetailScroll's own scroll area as a flex-shrink:0
     sibling, matching .modal-edit-contact's own header treatment. */
  #modal-estimate .modal-title-row {
    position: static;
    z-index: auto;
    margin: 0;
    padding: 16px 20px 12px;
  }
  /* Same two mobile fixes, retargeted to the Invoice modal (2026-07-25
     feedback: bring the Estimate modal's slide-in treatment over to
     Invoices — see the matching desktop block below too). */
  #modal-invoice .modal { padding: 0 !important; overflow: hidden; display: flex; flex-direction: column; }
  #modal-invoice .modal-title-row {
    position: static;
    z-index: auto;
    margin: 0;
    padding: 16px 20px 12px;
  }
  /* Hide the Price ($/unit) column on each estimate line-item row
     (2026-07-29 feedback: "hide the price column and its contents") —
     mobile-only, the row is too cramped to show unit price, qty, and total
     side by side on a small screen; Total alone is enough at a glance, and
     the real per-unit price is still visible (and editable) inside
     openEstLineServiceModal() when the row is tapped. */
  .li-row-price { display: none !important; }
  /* Duration/Qty/Discount columns (2026-07-30 feedback: "return the
     Duration, QTY, and Discount columns to desktop") — brought back for
     desktop only; mobile keeps the same simplified row (name/price/total,
     with Qty/Duration/Discount editing living in the Edit Line Item modal)
     it already had. */
  .li-desktop-col { display: none !important; }
  /* AI Suggest button (2026-07-30 feedback: "remove 'AI suggest' from the
     button on mobile" — icon-only on mobile, matching the icon-only
     convention already used for other mobile buttons). */
  .ai-est-btn-label { display: none !important; }
  /* Estimate modal footer Preview button (2026-07-25 feedback) — Print/Save
     PDF from inside the actual Estimate Preview modal is still reachable via
     the estimates list row's own 3-dot menu, so this doesn't remove the
     feature, just the redundant shortcut crowding the mobile footer. */
  .est-preview-btn { display: none !important; }
}

/* Estimate modal → full-width slide-in panel, not a centered dialog
   (2026-07-14), mirroring Contacts' slide-in panel — desktop only, mobile
   keeps its existing bottom-sheet treatment (layout.css's mobile .modal-
   overlay/.modal !important overrides) untouched. Classes/ids (modal-
   overlay, modal, modal-title-row, est-locked, view-locked) are unchanged
   from the old centered-dialog markup — only positioning/sizing is
   overridden here, so setLockedModalMode()/setEstimateLockState() and every
   pre-existing lockdown selector above keep working with zero JS changes. */
@media (min-width: 769px) {
  /* Always rendered (never display:none) so the slide is a real animated
     transform, not a display:none→flex snap with no visible motion — the
     base .modal-overlay{display:none}/.open{display:flex} toggle (which
     every other modal still uses unmodified) is overridden just for this
     one id. pointer-events keeps the invisible-when-closed box from
     blocking clicks on the Estimates list sitting behind it. */
  #modal-estimate.modal-overlay {
    display: flex !important;
    position: fixed;
    left: var(--sidebar-width);
    top: var(--topbar-height);
    right: 0;
    bottom: 0;
    background: transparent;
    align-items: stretch;
    justify-content: flex-end;
    padding: 0;
    pointer-events: none;
  }
  #modal-estimate.modal-overlay.open {
    pointer-events: auto;
  }
  #modal-estimate .modal {
    width: 100%;
    max-width: none;
    height: 100%;
    max-height: none;
    border-radius: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    transform: translateX(100%);
    transition: transform 0.25s cubic-bezier(0.4,0,0.2,1);
  }
  /* 11x horizontal padding (2026-07-18 feedback, up from 10x) — desktop-only
     scoped here alongside the title-row/footer's matching padding below.
     The real bug this replaces (2026-07-18): #estDetailScroll's base padding
     used to live as an INLINE style in index.html, which always wins over
     this external rule regardless of media query or specificity — so ever
     since the 2026-07-17 health-check fix set that inline value to a plain
     20px, this desktop rule has been silently dead, and the "we lost the
     padding" report was really "the fix that scoped it to desktop-only
     accidentally made it never apply at all." Base padding now lives in
     the plain (non-media-query) rule below instead, so this one can
     actually override it on desktop. */
  /* Fixed-width content, flexible gutter (2026-07-18 fix — replaces the
     previous fixed 330px side padding). That fixed padding ate a constant
     660px regardless of window width, so as the browser was narrowed toward
     the mobile breakpoint the CONTENT column shrank to almost nothing well
     before actually reaching mobile view. Now the content itself is capped
     at a comfortable reading width and centered; the padding is just a
     small flexible gutter that never eats into the content area, so the
     slider stays fully workable at any desktop-range window width. */
  #estDetailScroll {
    padding: 0 24px 20px;
  }
  #estDetailScroll > * {
    max-width: 900px;
    margin-left: auto;
    margin-right: auto;
  }
  /* Contact edit slide-in panel (2026-07-27 feedback: "add the same padding
     and width sizing format as the estimate slider") — same fixed-width-
     content/flexible-gutter treatment as #estDetailScroll directly above.
     No min-width guard needed here (unlike the Estimate rules in this block)
     since #custDetailPanel is already desktop-only — mobile opens the full
     Contact modal instead (see openContactDetail() usage, 2026-07-08). */
  #custDetailScroll > * {
    max-width: 900px;
    margin-left: auto;
    margin-right: auto;
  }
  #modal-estimate.modal-overlay.open .modal {
    transform: translateX(0);
  }
  /* AI Suggest button (2026-07-30 feedback: "remove the sparkles on the ai
     button from desktop") — desktop keeps the plain text label only,
     mobile (css/layout.css's mobile block) keeps the icon and hides the
     label instead, matching the icon-only convention used elsewhere. */
  .ai-est-btn-sparkles { display: none; }
  #modal-estimate .modal-title-row {
    position: static;
    margin: 0;
    /* Matches #estDetailScroll's own flexible gutter + content cap above,
       so the title row and footer stay visually aligned with the content
       between them at any window width. */
    padding: 14px 24px;
    max-width: 948px;
    margin-left: auto;
    margin-right: auto;
    width: 100%;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
  }
  /* .modal-footer's base rule (components.css) is a sticky bleed-through
     trick meant for a modal whose OWN box is the scroll container — here
     #estDetailScroll is the real scroll container instead, and the footer
     sits as a separate flex-column sibling below it (already permanently
     visible, never needs to "stick"), so that trick is reset back to a
     plain flex-column footer instead. */
  #modal-estimate .modal-footer {
    flex-shrink: 0;
    position: static;
    bottom: auto;
    margin: 0;
    padding: 14px 24px;
    max-width: 948px;
    margin-left: auto;
    margin-right: auto;
    width: 100%;
  }

  /* Invoice modal → full-width slide-in panel, mirroring the Estimate
     modal's exact treatment above (2026-07-25 feedback: "adapt everything
     we have in the Estimates page into the Invoices page") — desktop only,
     mobile keeps the bottom-sheet treatment from the block above. */
  #modal-invoice.modal-overlay {
    display: flex !important;
    position: fixed;
    left: var(--sidebar-width);
    top: var(--topbar-height);
    right: 0;
    bottom: 0;
    background: transparent;
    align-items: stretch;
    justify-content: flex-end;
    padding: 0;
    pointer-events: none;
  }
  #modal-invoice.modal-overlay.open {
    pointer-events: auto;
  }
  #modal-invoice .modal {
    width: 100%;
    max-width: none;
    height: 100%;
    max-height: none;
    border-radius: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    transform: translateX(100%);
    transition: transform 0.25s cubic-bezier(0.4,0,0.2,1);
  }
  #modal-invoice.modal-overlay.open .modal {
    transform: translateX(0);
  }
  #invDetailScroll {
    padding: 0 24px 20px;
  }
  #invDetailScroll > * {
    max-width: 900px;
    margin-left: auto;
    margin-right: auto;
  }
  #modal-invoice .modal-title-row {
    position: static;
    margin: 0;
    padding: 14px 24px;
    max-width: 948px;
    margin-left: auto;
    margin-right: auto;
    width: 100%;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
  }
  #modal-invoice .modal-footer {
    flex-shrink: 0;
    position: static;
    bottom: auto;
    margin: 0;
    padding: 14px 24px;
    max-width: 948px;
    margin-left: auto;
    margin-right: auto;
    width: 100%;
  }

  /* Appointment (Schedule Appointment) modal is a regular centered modal
     again (2026-07-25) — the separate read-only Appointment Details slider
     (#eventDetailSliderPanel, below) is what viewing an existing
     appointment opens now, so this modal only ever needs to render as a
     normal edit form. #modal-event intentionally has no overrides here
     anymore; it falls back to the base .modal-overlay/.modal rules
     (components.css), same as every other standard modal in the app. */
}
.est-pkg-tab-remove {
  border: none; background: transparent; color: var(--text3); cursor: pointer; font-size: 12px; padding: 0 2px;
}
.est-pkg-tab-remove:hover { color: var(--danger); }

/* ═══ ESTIMATE DETAILS SLIDER (2026-07-15) ═══════════════════════
   Half-page-width read-only summary panel, layered over the Estimates
   page (not a modal — position:fixed so it escapes .content's own
   layout/overflow entirely, same reasoning as .modal-overlay). */
.est-detail-slider {
  position: fixed; inset: 0; z-index: 500;
  pointer-events: none;
}
.est-detail-slider.open { pointer-events: auto; }
/* Locked while a modal opened FROM this slider (Edit) sits on top of it
   (2026-08 ask: "gray out the details slider and apply the same logic that
   you can't click on anything... while in the edit slider") — mirrors the
   .est-locked pointer-events:none pattern already used for an Accepted
   estimate's own lockdown, just applied to the slider PANEL instead of a
   modal, and toggled generically from openModal()/closeModal() (js/db.js)
   rather than duplicated per-page. Targets .est-detail-slider-panel (not
   the whole fixed overlay) so the backdrop/click-outside-to-close area
   underneath stays exactly as it was, only the visible card dims/locks. */
.est-detail-slider-panel.slider-locked { pointer-events: none; opacity: 0.5; transition: opacity 0.2s ease; }
.est-detail-slider-backdrop {
  position: absolute; inset: 0;
  background: rgba(0,0,0,0.5);
  opacity: 0;
  transition: opacity 0.25s ease;
}
.est-detail-slider.open .est-detail-slider-backdrop { opacity: 1; }
.est-detail-slider-panel {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: 50%; min-width: 460px; max-width: 760px;
  background: var(--surface);
  border-left: 1px solid var(--border2);
  box-shadow: -8px 0 24px rgba(0,0,0,0.25);
  display: flex; flex-direction: column;
  transform: translateX(100%);
  transition: transform 0.28s ease;
}
.est-detail-slider.open .est-detail-slider-panel { transform: translateX(0); }
@media (max-width: 900px) {
  .est-detail-slider-panel { width: 100%; min-width: 0; max-width: none; }
}
.est-detail-slider-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.est-detail-slider-title {
  font-family: var(--font-display); font-weight: 700; font-size: var(--text-md);
}
.est-detail-slider-scroll {
  flex: 1; overflow-y: auto;
  padding: 20px;
  display: flex; flex-direction: column; gap: 16px;
}
/* Bottom content was getting clipped by the mobile bottom nav bar (2026-07-26
   feedback) — this panel is position:fixed/inset:0, escaping .content
   entirely, so it never got the same var(--nav-bar-height) bottom clearance
   already applied to Conversations/Contacts/Internal Chat's own full-bleed
   pages (css/layout.css) for this exact symptom. */
@media (max-width: 768px) {
  .est-detail-slider-scroll { padding-bottom: calc(20px + var(--nav-bar-height, 43px)); }
}
.est-detail-card {
  border: 1px solid var(--border2);
  background: var(--surface2);
  border-radius: 10px;
  padding: 14px;
}
.est-detail-card-title {
  font-size: 10px; color: var(--text3); font-weight: 600; letter-spacing: 0.4px;
  margin-bottom: 10px;
  text-transform: uppercase;
}
.est-detail-info-row {
  display: flex; justify-content: space-between; gap: 12px;
  padding: 5px 0;
}
/* Labels stay at their existing size; only the value/content increases to
   text-base (2026-07-18 feedback). Uppercase reverted (2026-07-19 feedback)
   — row labels read as regular field labels now; .est-detail-card-title's
   own uppercase is untouched, this only affects the per-row labels below it. */
.est-detail-info-row .label { color: var(--text3); font-size: var(--text-sm); }
.est-detail-info-row .value { text-align: right; font-size: var(--text-base); }
.est-detail-contact-card {
  display: flex; align-items: center; gap: 12px;
  cursor: pointer;
}
.est-detail-contact-card:hover { opacity: 0.85; }
.est-detail-contact-card .est-detail-contact-name { font-weight: 600; }
.est-detail-contact-card .est-detail-contact-sub { font-size: var(--text-sm); color: var(--text3); }
.est-detail-pkg-card {
  border: 1px solid var(--border); border-radius: 8px; margin-bottom: 10px; overflow: hidden;
}
.est-detail-pkg-card.accepted { border-color: var(--accent-bright); }
.est-detail-pkg-card-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 10px 12px; cursor: pointer; gap: 10px;
}
.est-detail-pkg-card-head:hover { background: var(--surface2); }
.est-detail-pkg-card-body {
  padding: 0 12px 12px; border-top: 1px solid var(--border);
  display: none;
}
.est-detail-pkg-card-body.open { display: block; padding-top: 10px; }
.est-detail-pkg-line {
  display: flex; justify-content: space-between; gap: 10px;
  padding: 5px 0; font-size: var(--text-sm);
  border-bottom: 1px solid var(--border);
}
.est-detail-pkg-line:last-child { border-bottom: none; }
.est-detail-pkg-line .line-notes {
  color: var(--text3); font-size: var(--text-xs); font-style: italic; margin-top: 2px;
}
.est-detail-history-item {
  display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--border);
  font-size: var(--text-base);
}
.est-detail-history-item:last-child { border-bottom: none; }
.est-detail-history-item .ts { color: var(--text3); font-size: var(--text-xs); white-space: nowrap; }

/* ═══ ESTIMATES / INVOICES — line-item totals ═══════════════════ */
.inv-total-row { display: flex; justify-content: flex-end; gap: var(--space-4); align-items: center; font-size: var(--text-base); color: var(--text2); padding: 4px 0; }
.inv-grand     { font-family: var(--font-display); font-size: 20px; font-weight: 700; color: var(--accent-bright); }

/* Labor/Material bucket pill (2026-07-08 — materials-only sales tax) —
   click to reclassify a line; Material lines are the ones that get taxed. */
.li-bucket-pill {
  flex-shrink: 0; border: 1px solid var(--border2); border-radius: 20px;
  background: var(--surface2); color: var(--text2); font-size: var(--control-font-size); font-weight: 700;
  padding: 4px 10px; cursor: pointer; text-transform: uppercase; letter-spacing: 0.3px;
}
.li-bucket-pill.material { background: var(--accent-glow); color: var(--text2); border-color: var(--accent-border); }

/* Ad-hoc add-on/variable chips — one-off name+price adjustments typed
   directly on an estimate/invoice line, separate from a service's
   pre-configured catalog add-ons above. */
.li-addon-chip {
  display: inline-flex; align-items: center; gap: 5px; font-size: 10px;
  background: var(--surface2); border: 1px solid var(--border); border-radius: 20px;
  padding: 3px 8px; margin: 2px 4px 2px 0;
}
.li-addon-chip span { cursor: pointer; color: var(--text3); }
.li-addon-chip span:hover { color: var(--danger); }

/* Unified searchable Items/Services line-item picker (2026-07-08 rebuild) —
   replaces the old category-then-tier dropdown pair. Reuses the
   .ev-customer-dropdown/.ev-customer-item box styling from the customer
   search picker, plus a type-filter row and per-category group headers. */
.li-picker-dropdown { max-height: 320px; display: flex; flex-direction: column; }
.li-picker-type-row {
  display: flex; gap: 4px; padding: 6px; border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.li-picker-type-btn {
  flex: 1; border: 1px solid var(--border2); border-radius: 20px; background: var(--surface2);
  color: var(--text2); font-size: var(--control-font-size); font-weight: 700; padding: 4px 8px; cursor: pointer;
  text-transform: uppercase; letter-spacing: 0.3px;
}
.li-picker-type-btn.active { background: var(--accent-glow); color: var(--accent-bright); border-color: var(--accent-border); }
.li-picker-list { overflow-y: auto; }
.li-picker-group-header {
  padding: 6px var(--space-4) 3px; font-size: 10px; font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.3px; color: var(--text3); background: var(--surface);
  position: sticky; top: 0;
}
/* Folder-navigation root screen (2026-07-16) — Items/Services/Discounts/
   per-category folders drill into their own flat screen via
   openElsmPickerFolder(); .li-picker-back-row returns to the root list. */
.li-picker-folder-row { display: flex; align-items: center; font-weight: 600; }
.li-picker-back-row {
  padding: 8px var(--space-4); font-size: var(--text-sm); color: var(--accent-bright);
  cursor: pointer; border-bottom: 1px solid var(--border); position: sticky; top: 0;
  background: var(--surface);
}
.li-picker-back-row:hover { background: var(--surface2); }
.li-picker-spacer { height: 10px; }

/* ═══ REWARDS ══════════════════════════════════════════════════ */
.rewards-card {
  background: linear-gradient(135deg, #1a0a2e 0%, var(--surface) 60%);
  border: 1px solid var(--accent-border); border-radius: 12px; padding: 18px; position: relative; overflow: hidden;
}
.rewards-card::before {
  content: ''; position: absolute; top: -40px; right: -40px;
  width: 140px; height: 140px;
  background: radial-gradient(circle, rgba(93,39,141,0.3) 0%, transparent 70%); pointer-events: none;
}
.points-display { font-family: var(--font-display); font-size: 36px; font-weight: 700; color: var(--accent-bright); }
.points-label   { font-size: 11px; color: var(--text3); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; }
.points-bar     { height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; margin: var(--space-2) 0; }
.points-bar-fill{ height: 100%; background: var(--accent); border-radius: 3px; transition: width 0.5s; }
.rewards-stat-row { display: flex; justify-content: space-between; align-items: center; padding: var(--space-2) 0; border-bottom: 1px solid var(--border); font-size: var(--text-base); }
.rewards-stat-row:last-child { border-bottom: none; }
.rewards-stat-label { color: var(--text3); }
.rewards-stat-value { font-weight: 600; }

/* ═══ REVIEWS ══════════════════════════════════════════════════ */
.stars       { color: var(--warn); }
.review-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: var(--space-3) var(--space-4); margin-bottom: var(--space-3); }
.review-meta { display: flex; align-items: center; gap: var(--space-2); margin-bottom: 6px; flex-wrap: wrap; }
.review-reply{ background: var(--surface); border-left: 3px solid var(--accent); padding: 7px 10px; border-radius: 0 6px 6px 0; font-size: 12px; color: var(--text2); margin-top: var(--space-2); }
.platform-btn { padding: 5px 12px; border-radius: var(--radius-pill); border: 1px solid var(--border); background: var(--surface2); color: var(--text3); cursor: pointer; font-size: 11px; font-family: var(--font-body); font-weight: 600; transition: all var(--transition); }
.platform-btn.selected { border-color: var(--accent); color: var(--accent-bright); background: var(--accent-glow); }

/* ═══ STAFF & TIME CLOCK ═══════════════════════════════════════ */
.clock-widget {
  background: linear-gradient(135deg, #1a0a2e 0%, var(--surface) 70%);
  border: 1px solid var(--accent-border); border-radius: var(--radius-lg);
  padding: 28px; text-align: center; position: relative; overflow: hidden;
}
.clock-widget::before {
  content: ''; position: absolute; top: -60px; left: 50%; transform: translateX(-50%);
  width: 300px; height: 200px;
  background: radial-gradient(ellipse, rgba(93,39,141,0.2) 0%, transparent 70%); pointer-events: none;
}
.clock-time   { font-family: var(--font-display); font-size: 48px; font-weight: 700; letter-spacing: -2px; margin-bottom: 4px; }
.clock-date   { font-size: var(--text-base); color: var(--text3); margin-bottom: var(--space-5); }
.clock-status { display: inline-flex; align-items: center; gap: var(--space-2); padding: 6px var(--space-4); border-radius: var(--radius-pill); font-size: 12px; font-weight: 700; margin-bottom: var(--space-5); }
.clock-status.in  { background: var(--success-bg); color: var(--success); border: 1px solid rgba(34,197,94,0.3); }
.clock-status.out { background: rgba(192,192,192,0.1); color: var(--text3); border: 1px solid var(--border); }
.clock-status .dot     { width: 7px; height: 7px; border-radius: 50%; }
.clock-status.in .dot  { background: var(--success); }
.clock-status.out .dot { background: var(--text3); }
.clock-btn   { width: 100%; max-width: 220px; padding: 13px; border: none; border-radius: var(--radius); font-size: var(--text-md); font-weight: 700; cursor: pointer; font-family: var(--font-body); transition: all var(--transition); }
.clock-btn.clock-in  { background: var(--accent); color: #fff; }
.clock-btn.clock-in:hover  { background: var(--accent-light); }
.clock-btn.clock-out { background: var(--danger-bg); color: var(--danger); border: 1px solid rgba(239,68,68,0.3); }
.clock-btn.clock-out:hover { background: rgba(239,68,68,0.22); }
.clock-btn:disabled  { opacity: 0.5; cursor: not-allowed; }

.shift-row { display: grid; grid-template-columns: 120px 80px 80px 80px 1fr 80px; gap: 10px; align-items: center; padding: 9px 12px; border-bottom: 1px solid var(--border); font-size: 12px; }
.shift-row.header { font-size: 10px; color: var(--text3); font-weight: 700; text-transform: uppercase; letter-spacing: 1px; background: var(--surface2); }

.emp-card    { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-4); display: flex; align-items: center; gap: var(--space-4); }
.emp-name    { font-weight: 700; font-size: var(--text-md); margin-bottom: 2px; }
.emp-meta    { font-size: 11px; color: var(--text3); }
.emp-actions { margin-left: auto; display: flex; gap: 6px; align-items: center; }

.pay-card         { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: var(--space-3) var(--space-4); margin-bottom: var(--space-2); }
.pay-card-header  { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-2); }
.pay-card-name    { font-weight: 600; font-size: var(--text-base); }
.pay-line         { display: flex; justify-content: space-between; font-size: 12px; color: var(--text2); padding: 2px 0; }
.pay-line.total   { font-weight: 700; color: var(--text); border-top: 1px solid var(--border); margin-top: 6px; padding-top: 6px; font-size: var(--text-base); }
.pay-line .ot     { color: var(--warn); font-size: 10px; font-weight: 700; }

.tier-row        { display: grid; grid-template-columns: 140px 100px 1fr 80px; gap: 10px; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 12px; }
.tier-row.header { font-size: 10px; color: var(--text3); font-weight: 700; text-transform: uppercase; letter-spacing: 1px; background: var(--surface2); }

/* ═══ JOB QUEUE ════════════════════════════════════════════════ */
.jq-card     { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--space-4); margin-bottom: var(--space-3); }
.jq-header   { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-2); }
.jq-date     { font-size: 11px; color: var(--text3); font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; }
.jq-customer { font-weight: 700; font-size: var(--text-md); }
.jq-vehicle  { font-size: 12px; color: var(--text2); }
.jq-service  { font-size: 11px; }

/* ═══ PHOTO LIBRARY ════════════════════════════════════════════ */
.photo-grid  { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px,1fr)); gap: var(--space-3); }
.photo-card  { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; cursor: pointer; transition: border-color var(--transition); }
.photo-card:hover { border-color: var(--accent); }
.photo-thumb { width: 100%; aspect-ratio: 4/3; object-fit: cover; background: var(--surface3); display: flex; align-items: center; justify-content: center; font-size: 32px; color: var(--text3); }
.photo-meta  { padding: 8px 10px; }
.photo-label { font-size: 11px; font-weight: 600; color: var(--text); }
.photo-sub   { font-size: 10px; color: var(--text3); margin-top: 2px; }

/* ═══ AI PANEL ═════════════════════════════════════════════════ */
.ai-panel {
  background: linear-gradient(135deg, #1a0a2e 0%, var(--surface) 70%);
  border: 1px solid var(--accent-border); border-radius: var(--radius);
  padding: var(--space-4) 18px; margin-bottom: var(--space-4); position: relative; overflow: hidden;
}
.ai-panel::before {
  content: ''; position: absolute; top: -40px; right: -40px;
  width: 180px; height: 180px;
  background: radial-gradient(circle, rgba(93,39,141,0.18) 0%, transparent 70%); pointer-events: none;
}
.ai-panel-title { font-family: var(--font-display); font-size: var(--text-base); font-weight: 700; color: var(--accent-bright); display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-3); }
.ai-result  { background: var(--surface2); border: 1px solid var(--accent-border); border-radius: 8px; padding: var(--space-3) var(--space-4); font-size: var(--text-base); color: var(--text2); line-height: 1.7; white-space: pre-wrap; min-height: 60px; }
.ai-loading { display: flex; align-items: center; gap: var(--space-2); font-size: 12px; color: var(--text3); padding: var(--space-2) 0; }
.ai-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-bright); animation: aiPulse 1.2s ease-in-out infinite; }
.ai-dot:nth-child(2){animation-delay:0.2s;} .ai-dot:nth-child(3){animation-delay:0.4s;}
@keyframes aiPulse{0%,80%,100%{opacity:0.3;transform:scale(0.8);}40%{opacity:1;transform:scale(1);}}
.ai-suggestion-row  { display: flex; gap: var(--space-2); flex-wrap: wrap; margin-top: var(--space-3); }
.ai-suggestion-pill { background: rgba(93,39,141,0.15); border: 1px solid var(--accent-border); border-radius: var(--radius-pill); padding: 4px 12px; font-size: 11px; color: var(--accent-bright); cursor: pointer; transition: background var(--transition); }
.ai-suggestion-pill:hover { background: rgba(93,39,141,0.3); }

/* ═══ AUTOMATION STEP BUILDER ══════════════════════════════════ */
.auto-steps-builder { display: flex; flex-direction: column; gap: var(--space-2); margin: var(--space-3) 0; }
.auto-step-row {
  display: grid; grid-template-columns: 32px 1fr 1fr 120px 32px;
  gap: var(--space-2); align-items: center;
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: 8px; padding: 8px 10px;
}
.auto-step-num { width: 24px; height: 24px; background: var(--accent-glow); border: 1px solid var(--accent-border); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: var(--text-2xs); font-weight: 700; color: var(--accent-bright); flex-shrink: 0; }
.auto-step-row select, .auto-step-row input { font-size: var(--text-sm); padding: 5px 8px; }
.auto-step-row .remove-step { width: 24px; height: 24px; background: var(--danger-bg); border: none; border-radius: 4px; color: var(--danger); cursor: pointer; font-size: var(--text-base); display: flex; align-items: center; justify-content: center; }
.auto-connector { display: flex; align-items: center; gap: 6px; padding: 0 0 0 40px; font-size: var(--text-2xs); color: var(--text3); }
.auto-connector::before { content: ''; width: 1px; height: 20px; background: var(--border); display: block; }
.auto-condition { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px; font-size: 11px; color: var(--text2); display: flex; align-items: center; gap: var(--space-2); }

/* Canvas/List toggle above the step builder */
#autoViewBtn-canvas.active, #autoViewBtn-list.active { background: var(--accent-glow); color: var(--accent-bright); border-color: var(--accent-border); }

/* ═══ AUTOMATION WORKFLOW CANVAS (node-graph view) ══════════════
   Same autoSteps tree as the list view above — a vertical flow of
   connected boxes instead of table rows, with at most one two-lane fork
   (a branch is always terminal, per compileSteps()'s own design — see
   js/db.js). Deliberately centered/narrow rather than free-form 2D
   placement — the topology never needs arbitrary node positions. */
.auto-canvas { display: flex; flex-direction: column; align-items: center; padding: var(--space-3) 0; }
.auto-canvas-node {
  width: 100%; max-width: 460px;
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: 10px; padding: 10px 12px;
}
.auto-canvas-trigger {
  display: flex; align-items: center; gap: 8px; justify-content: center;
  background: var(--accent-glow); border-color: var(--accent-border);
  color: var(--accent-bright); font-size: 12px; font-weight: 700;
  max-width: 220px; padding: 8px 14px;
}
.auto-canvas-node-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.auto-canvas-node-icon { display: flex; color: var(--accent-bright); flex-shrink: 0; }
.auto-canvas-node-head select { flex: 1; }
.auto-canvas-reorder { display: flex; flex-direction: column; gap: 1px; flex-shrink: 0; }
.auto-canvas-reorder-btn {
  width: 18px; height: 13px; background: var(--surface3); border: none;
  border-radius: 3px; color: var(--text3); cursor: pointer; font-size: 8px;
  line-height: 1; display: flex; align-items: center; justify-content: center;
  padding: 0;
}
.auto-canvas-reorder-btn:hover:not(:disabled) { background: var(--accent-glow); color: var(--accent-bright); }
.auto-canvas-reorder-btn:disabled { opacity: 0.3; cursor: default; }
.auto-canvas-node-head .remove-step {
  width: 22px; height: 22px; background: var(--danger-bg); border: none;
  border-radius: 4px; color: var(--danger); cursor: pointer; font-size: 13px;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.auto-canvas-node-body { display: flex; }
.auto-canvas-node-body > * { flex: 1; }
.auto-canvas-delay { display: flex; gap: 4px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--border); }
.auto-canvas-connector { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 6px 0; }
.auto-canvas-line { width: 2px; height: 18px; background: var(--border2); }
.auto-canvas-line-dashed { background: repeating-linear-gradient(to bottom, var(--border2) 0 4px, transparent 4px 8px); }
.auto-canvas-add-row { display: flex; gap: 8px; margin-top: 4px; }
.auto-canvas-branch { max-width: 460px; border-color: var(--accent-border); }
.auto-canvas-fork {
  display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
  width: 100%; max-width: 640px; margin-top: 8px;
}
.auto-canvas-lane {
  display: flex; flex-direction: column; align-items: center;
  border: 1px dashed var(--border2); border-radius: 10px; padding: 10px;
  min-width: 0;
}
.auto-canvas-lane .auto-canvas-node { max-width: 100%; }
.auto-canvas-lane-label { font-size: var(--text-xs); font-weight: 700; margin-bottom: 8px; }
.auto-canvas-lane-yes { color: var(--success); }
.auto-canvas-lane-no { color: var(--danger); }

/* ═══ SETTINGS PAGES ═══════════════════════════════════════════ */
.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);
}

/* ═══ INVENTORY / PROGRESS BARS ════════════════════════════════ */
.inv-bar          { display: flex; align-items: center; gap: var(--space-2); }
.inv-progress     { flex: 1; height: 5px; background: var(--border); border-radius: 3px; overflow: hidden; }
.inv-progress-fill{ height: 100%; border-radius: 3px; background: var(--success); }
.inv-progress-fill.warn   { background: var(--warn); }
.inv-progress-fill.danger { background: var(--danger); }

/* ═══ MOBILE — icon-only button standardization (2026-07-08) ═══════
   Several icon-only buttons around the app grew their own one-off sizing
   before this (the Conversations topbar's icon-only Call/Mark-read
   buttons at .conv-topbar .btn, the compose "+" attach button at
   36px/8px, plain .btn-sm/.btn-xs buttons used only for a "⋮" glyph —
   see .btn-icon-mobile below). Ricardo's feedback: fingers need bigger,
   consistently-square tap targets than the desktop 32px .btn-icon gives.
   This block is the one place all of those now converge on mobile. */
@media (max-width: 768px) {
  .btn-icon, .btn-icon-lg, .btn-icon-mobile,
  .conv-topbar .btn, .conv-compose-type-btn {
    width: 44px; height: 44px; padding: 0;
    border-radius: 10px;
    display: inline-flex; align-items: center; justify-content: center;
    flex-shrink: 0;
  }
  /* .compose-plus-btn removed from the list above (2026-07-24 fix) — it's
     meant to stay hidden on mobile entirely (line ~361, its menu is folded
     into the compose row's single "more" arrow instead), but this rule's own
     display:inline-flex, at equal specificity and later in source order,
     was silently winning over that display:none and showing it again. */
  /* Send button + its "Schedule Send" caret (2026-07-09) — both were
     inline-styled to the desktop 52px compose-row height; on mobile the
     send button drops to the same 44px square as every other icon button
     above, and the caret matches its height (own width stays slim, it's
     just a dropdown indicator, not a full icon button). */
  #convSendBtn { height: 44px !important; }
  /* Clearer separator between Send and its caret (2026-07-29 feedback:
     "currently it just looks like a chopped off button") — the shared
     rgba(255,255,255,0.25) border-left (js/calendar.js inline style) reads
     as barely-there on mobile against the same purple both buttons share,
     so the caret looked like an accidentally-clipped sliver of Send rather
     than its own control. A visibly brighter, slightly thicker line reads
     as an intentional divider between two adjacent buttons instead. */
  .conv-send-caret-btn { height: 44px !important; border-left: 1.5px solid rgba(255,255,255,0.45) !important; }
  /* The mobile "more" arrow next to Send never got this same treatment
     (2026-07-24 feedback: "make the arrow to the right of the send button
     match the height of the send button") — it kept its own inline 52px
     from the JS template, taller than its now-44px .conv-send-wrap parent,
     so it visually poked out below the row (see the pointer-events fix on
     .conv-mobile-more-wrap below for the click-through bug that caused). */
  .conv-mobile-more-btn { height: 44px !important; border-top-left-radius: 0 !important; border-bottom-left-radius: 0 !important; }
  /* 2026-07-31's "square the caret's left corners" fix (below) targeted
     .conv-send-caret-btn, which lives inside .conv-send-caret-wrap —
     display:none on mobile (this same media query, above). The button that
     actually sits next to Send on a phone is this one
     (.conv-mobile-more-btn, the 2026-07-24 consolidated "more" arrow), which
     never got the matching square-corner treatment, so the exact "chopped
     button" look kept recurring on mobile even after that fix shipped. */
  /* The arrow's popup becomes a real bottom sheet (2026-07-24 feedback:
     "match the formatting to the 3 dot menu bottom slider from the
     conversation view") — same treatment as .pipe-menu-dropdown's own
     mobile override (css/components.css), copied here rather than shared
     since #convMobileMoreMenu uses the separate .compose-plus-menu/
     .plus-menu-item class pair, not .pipe-menu-dropdown/.pipe-menu-item.
     Relies on togglePlusMenu() (js/snippets.js) relocating this element to
     document.body on open so position:fixed anchors to the real viewport. */
  #convMobileMoreMenu {
    position: fixed !important;
    left: 0 !important; right: 0 !important; top: auto !important;
    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);
  }
  #convMobileMoreMenu .plus-menu-item { padding: 14px var(--space-5); font-size: var(--text-md); }
  /* The wrap around Send+caret kept its own inline 52px (2026-07-14 fix).
     Its children were already dropped to 44px above, but with an explicit
     height set, a flex child ignores "stretch" and sits flush at the
     cross-start (top) of its box instead — so Send rendered 8px higher than
     the row's bottom edge. The type/+ buttons have no such wrapper, so
     .conv-compose-row's align-items:flex-end pins THEM flush to the row's
     bottom — 8px lower than Send's bottom edge, exactly the reported
     misalignment. Shrinking the wrapper to 44px too puts all three flush
     together. */
  .conv-send-wrap { height: 44px !important; gap: 1px; }
  /* Center the whole compose row on mobile (2026-07-31 feedback: "center
     the send and carot button to the compose box") — .conv-compose-row's
     base rule (css/pages.css) uses align-items:flex-end so Send/caret sit
     flush with the row's bottom edge; on mobile the textarea can grow
     taller than the buttons' fixed 44px, and bottom-alignment then reads as
     the buttons sitting low/disconnected from the box rather than centered
     against it. Desktop is untouched (still flex-end, its own separate
     convention). */
  .conv-compose-row { align-items: center !important; }
  /* Square left corners + a real 1px gap on the Send/caret pair (2026-07-31
     feedback: "the caret and send button still look chopped... make the
     left corners on the caret button square instead of round and add one
     pixel of separation") — the caret's inline style (js/calendar.js)
     already zeroes its left radius and Send already zeroes its right
     radius when the caret is present, for the earlier "seamless pill"
     look; the gap above (.conv-send-wrap) now visually separates them
     instead, so reasserting square corners here (in case anything else
     ever wins the cascade) plus the border-left rgba divider both stay —
     together they read as two distinct, deliberately-adjacent controls
     rather than one clipped button. */
  .conv-send-caret-btn { border-top-left-radius: 0 !important; border-bottom-left-radius: 0 !important; }
}

/* ═══ MOBILE — search bar standardization (2026-07-08) ══════════════
   Every search bar on mobile now matches Conversations' (#convSearch)
   compact sizing — the others (#custSearch, #estSearch) previously used
   the full default 30px-tall/14px-padding input with no override, taller
   and more loosely padded than Conversations'. Layout (width/flex) is left
   alone here, only the visual sizing that was actually inconsistent. */
@media (max-width: 768px) {
  #custSearch, #estSearch, #invSearch, #svcSearch, #inventSearch {
    /* var(--text-sm), not a hardcoded 12px — matches #convSearch's own
       inline font-size token exactly rather than a close-but-different
       px value (2026-07-14 feedback: "contents inside the search bar are
       small compared to conversations"). */
    font-size: var(--text-sm); padding: 6px 10px;
  }
}

/* ═══ MOBILE — Items & Services list: real 2-row card list (2026-07-15) ═══
   Replaces the column-hiding approach with the same table-hidden/card-list-
   shown swap Contacts/Estimates/Invoices already use, so Items and Services
   read consistently with every other mobile list: name+price on one row,
   variation-names(or category)+count/duration on the row below. Desktop is
   untouched — .catalog-mobile-card-list stays display:none there. */
@media (max-width: 768px) {
  #page-inventory .table-wrap { display: none !important; }
  .catalog-mobile-card-list { display: flex !important; }

  /* Full-width like Estimates (2026-07-21) — same negative-margin bleed
     trick as #page-estimates-panel-main below: Items & Services isn't
     .page-fullbleed either (desktop keeps its normal 20px .content
     padding), so this bleeds each subtab panel out through .content's own
     padding instead of restructuring the page's positioning model. Search
     row gets its own horizontal padding back so it doesn't bleed too. */
  #page-inventory-panel-main, #page-inventory-panel-items { margin-left: calc(-1 * var(--space-5)); margin-right: calc(-1 * var(--space-5)); }
  #page-inventory-panel-main > .search-row,
  #page-inventory-panel-items > .search-row { margin-left: var(--space-5); margin-right: var(--space-5); }
  /* Separator below the search row on both subtabs (2026-07-19 feedback)
     — same treatment as Contacts'/Conversations' own search-row bottom
     border. */
  #page-inventory-panel-main > .search-row,
  #page-inventory-panel-items > .search-row { padding-bottom: 10px; border-bottom: 1px solid var(--border); }
}
.catalog-mobile-card-list { display: none; flex-direction: column; }
.catalog-mobile-card { border-bottom: 1px solid var(--border); padding: 11px var(--space-4); cursor: pointer; }
.catalog-mobile-card-row1 { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.catalog-mobile-card-name { font-weight: 600; color: var(--text); font-size: var(--text-base); }
.catalog-mobile-card-price { font-weight: 700; color: var(--text); font-size: var(--text-base); flex-shrink: 0; }
.catalog-mobile-card-row2 { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 2px; font-size: var(--text-sm); color: var(--text3); }
/* Old table-column-hiding classes are no longer applied, but the .table-wrap
   itself stays in the DOM (just hidden) so desktop is completely unaffected. */
.catalog-mobile-sub, .catalog-mobile-price { display: none; }

/* ═══ MOBILE — Calendar toolbar overflow menu (2026-07-14; rebuilt 2026-07-29)
   ═══════════ View select, tech/type filters, Print Day Sheet, and Generate
   a Booking Link fold into one 3-dot menu (toggleCalToolbarMoreMenu(),
   js/calendar.js) to the right of the month/view title — Internal Block is
   dropped entirely (redundant with the mobile + FAB's own menu, see
   toggleMobileFabMenu(), js/ui.js). Desktop is untouched. As of 2026-07-29,
   the real calTechFilter/calTypeFilter/calPrintDaySheetBtn elements are
   simply hidden here and never relocated into the menu anymore — the menu
   itself is now built from plain .pipe-menu-item rows (matching Conversations'
   own mobile 3-dot menu, #convSidebarMoreMenu) that read/set these real
   elements' values directly, so no "make visible again once relocated"
   override rules are needed. */
/* Generate Booking Link card lives permanently in the DOM but is only ever
   shown once relocated into its own modal (openGenBookingLinkModal(),
   js/calendar.js) — desktop now triggers this the same way mobile already
   did (a button above the Self-Booking Requests list), rather than the card
   sitting inline on the page (2026-07-25 feedback). Its own .card border/
   background/padding is flattened once inside the modal so it doesn't
   double-box inside the modal's own box. */
#calGenBookingLinkCard { display: none; }
#genBookingLinkModalBody #calGenBookingLinkCard {
  display: block; width: 100%; max-width: none; margin: 0;
  border: none; background: none; padding: 0;
}

@media (max-width: 768px) {
  .cal-toolbar-more-mobile { display: inline-flex !important; }
  #calInternalBlockBtn { display: none !important; }
  #calViewSelect, #calTechFilter, #calTypeFilter, #calPrintDaySheetBtn {
    display: none;
  }
  .cal-toolbar-row2 { flex-wrap: wrap; }
}

/* ═══ MOBILE — Generate-Booking-Link modal button treatment ═══════════
   Generate a Booking Link never had a ✕ at all (Close only lived in the
   footer) — the ✕ added in index.html is shown here on mobile, and the
   footer Close button it replaces is hidden, so there's still exactly one
   close action, just relocated to match the app's usual modal-header-✕
   convention. (Appointment modal's/Internal Block modal's own footer Cancel
   buttons used to be hidden here too, redundant with their header ✕ —
   reverted, 2026-08 feedback: "return the cancel/close button at the footer
   of all modals they were removed from 2 requests ago".) */
@media (max-width: 768px) {
  #genBookingLinkMobileCloseBtn { display: flex !important; z-index: 5; }
  #genBookingLinkFooter { display: none !important; }
}

/* ═══ MOBILE — Items & Services / Estimates / Invoices toolbar overflow
   menus (2026-07-14) ═══ Category filter + Sort By fold into a 3-dot menu
   next to the search bar (toggleCatalogToolbarMoreMenu(), js/services.js);
   the "Add Service"/"Add Item" buttons are dropped entirely (redundant with
   the mobile + FAB, same reasoning as Calendar's Internal Block above). */
@media (max-width: 768px) {
  .catalog-toolbar-more-mobile { display: inline-flex !important; }
  #svcAddBtn, #invAddBtn { display: none !important; }
  #svcCatFilterWrap, #invCatFilterWrap, #svcSortByBtn, #invSortByBtn { display: none; }
  [id^="catalogToolbarMoreMenu-"] #svcCatFilterWrap,
  [id^="catalogToolbarMoreMenu-"] #invCatFilterWrap { display: block; width: 100%; }
  [id^="catalogToolbarMoreMenu-"] #svcCatFilterBtn,
  [id^="catalogToolbarMoreMenu-"] #invCatFilterBtn,
  [id^="catalogToolbarMoreMenu-"] #svcSortByBtn,
  [id^="catalogToolbarMoreMenu-"] #invSortByBtn { display: flex; width: 100%; }
}

/* ═══ MOBILE — Invoices toolbar overflow menu (2026-07-14) ═══════════
   Status/category filter + sort select fold into a 3-dot menu next to the
   search bar (toggleInvToolbarMoreMenu(), js/calendar.js); Import/Export
   CSV and New Invoice are dropped entirely (redundant with the mobile +
   FAB). Same relocation pattern as the Calendar/Items&Services menus above. */
@media (max-width: 768px) {
  .inv-toolbar-more-mobile { display: inline-flex !important; }
  #invImportBtn, #invExportBtn, #invNewBtn { display: none !important; }
  #invFilterWrap, #invSortSelect { display: none; }
  #invToolbarMoreMenu #invFilterWrap { display: block; width: 100%; }
  #invToolbarMoreMenu #invFilterBtn { display: flex; width: 100%; }
  #invToolbarMoreMenu #invSortSelect { display: block; width: 100%; }
}

/* ═══ MOBILE — Invoices: full-bleed + card list (2026-07-14) ══════════
   Same negative-margin bleed + table-to-card-list swap as Estimates above —
   Invoices previously had NO mobile-specific rendering at all (the full
   8-column desktop table just sat inside a horizontally-scrolling
   .table-wrap), the biggest gap in the app-wide "match Conversations' flat
   list" standardization pass. */
@media (max-width: 768px) {
  #page-invoices-panel-main { margin-left: calc(-1 * var(--space-5)); margin-right: calc(-1 * var(--space-5)); }
  #page-invoices-panel-main > .search-row,
  #page-invoices-panel-main > #invNotifBanner { margin-left: var(--space-5); margin-right: var(--space-5); }
  #page-invoices .table-wrap { display: none !important; }
  #invoiceCardList { display: flex !important; }
  /* Separator below the search row — same treatment as Estimates/Contacts/
     Conversations/Items & Services (css/pages.css:2164 for Estimates). */
  #page-invoices-panel-main > .search-row { padding-bottom: 10px; border-bottom: 1px solid var(--border); }
}
#invoiceCardList { display: none; flex-direction: column; }
.inv-mobile-card {
  border-bottom: 1px solid var(--border);
  padding: 11px var(--space-4);
  cursor: pointer;
}
.inv-mobile-card-row1 { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.inv-mobile-card-name { font-weight: 600; color: var(--text); font-size: var(--text-base); }
.inv-mobile-card-total { font-weight: 700; color: var(--text); font-size: var(--text-base); flex-shrink: 0; }
.inv-mobile-card-row2 { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 2px; font-size: var(--text-sm); color: var(--text3); }

/* ═══ MOBILE — Estimates: full-bleed + card list + toolbar overflow menu
   (2026-07-14) ═══ #page-estimates isn't .page-fullbleed (desktop keeps its
   normal 20px .content padding), so instead of restructuring the page's
   positioning model (risking desktop layout), this bleeds the page out
   through .content's own padding via a negative margin equal to it — same
   net "no padding on the edges" effect as Contacts' page-fullbleed
   treatment, scoped to mobile only. Search row/toolbar get their own
   horizontal padding back, matching Contacts' identical pattern
   (#custPanel-main's margin-left/right:16px on its search row). */
@media (max-width: 768px) {
  /* Only panel-main bleeds out — .conv-subtabs (the Estimates/Templates/
     Settings tab bar) already has its own working negative-margin bleed
     trick via its inline style, sitting as panel-main's SIBLING under
     #page-estimates, so it's unaffected by anything done here. */
  #page-estimates-panel-main { margin-left: calc(-1 * var(--space-5)); margin-right: calc(-1 * var(--space-5)); }
  #page-estimates-panel-main > .search-row,
  #page-estimates-panel-main > #estNotifBanner { margin-left: var(--space-5); margin-right: var(--space-5); }
  /* Separator below the search row (2026-07-19 feedback) — same treatment
     as Contacts/Conversations/Items & Services. */
  #page-estimates-panel-main > .search-row { padding-bottom: 10px; border-bottom: 1px solid var(--border); }

  /* Desktop table replaced by a 2-row card list, same toggle mechanism as
     Contacts' #contactCardList (table-wrap hidden, card list shown). */
  #page-estimates .table-wrap { display: none !important; }
  #estimateCardList { display: flex !important; }

  .est-toolbar-more-mobile { display: inline-flex !important; }
  #estExportBtn, #estNewBtn { display: none !important; }
  #estFilterWrap, #estSortSelect { display: none; }
  #estToolbarMoreMenu #estFilterWrap { display: block; width: 100%; }
  #estToolbarMoreMenu #estFilterBtn { display: flex; width: 100%; }
  #estToolbarMoreMenu #estSortSelect { display: block; width: 100%; }
}
#estimateCardList { display: none; flex-direction: column; }
.est-mobile-card {
  border-bottom: 1px solid var(--border);
  padding: 11px var(--space-4);
  cursor: pointer;
}
.est-mobile-card-row1 { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.est-mobile-card-name { font-weight: 600; color: var(--text); font-size: var(--text-base); }
.est-mobile-card-total { font-weight: 700; color: var(--text); font-size: var(--text-base); flex-shrink: 0; }
.est-mobile-card-row2 { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 2px; font-size: var(--text-sm); color: var(--text3); }
