/* Site Set Co — app design system + shell.
   Tokens + component look match APP-DESIGN-KIT/concepts/concept-c-dispatch.html
   (Concept C — Dispatch) byte-for-byte in the :root block below, EXCEPT
   --text-faint (R2-U1: darkened off the concept's original #8B95A1 --
   battery's TOKENS check knows about this one named exception, see
   tests/battery.py check_tokens()). NOTE ON THE EXACT VALUE: the mock.css
   contract's own :root sets --text-faint to #737E8C with a comment
   claiming "4.6:1 on white" -- verified by computed WCAG math (this
   file's own tests/r2_u1_checks.py check_text_faint_contrast, and by hand)
   that hex is actually 4.12:1, UNDER the 4.5:1 AA floor the whole UGLY #1
   fix exists to clear. #6C7684 below is the same hue, darkened until it
   actually lands at 4.6:1 (verified) -- matching the CONTRACT'S STATED
   INTENT rather than its literal (miscalculated) byte value. mockups/ is
   read-only, so this couldn't be fixed at the source; flagged for whoever
   owns that file next. No external requests (no CDN, no web fonts) --
   system font stack only. */

:root{
  --page-bg: #E7E9ED;
  --app-bg: #F3F5F7;
  --slate: #2E3A46;
  --slate-deep: #232C36;
  --cobalt: #2456D6;
  --cobalt-tint: #E8EEFC;
  --border: #DDE3E9;
  --text-primary: #202832;
  --text-secondary: #5B6672;
  --text-faint: #6C7684;      /* R2-U1: was #8B95A1 (2.50:1 on --page-bg, failed AA); now a verified 4.60:1 on white -- see header comment on mock.css's own #737E8C actually computing to 4.12:1 */
  --mist-bg: #E1F3EA;
  --mist-text: #1F7A4D;
  --amber-bg: #FCEFD9;
  --amber-text: #92620A;
  --slate-chip-bg: #E7EBEF;
  --slate-chip-text: #445162;
  --white: #FFFFFF;
  --money-numeral: #8FB3FF;   /* money-tile numerals only (spec token table) */
  --money-overdue: #E88A80;   /* RV1: money-tile overdue clause only, same
     narrow spec-token pattern as --money-numeral just above -- needs to
     read against --slate-deep, not --danger's on-light contrast pairing */
  /* pkg 10: avatar-dot-only exception to the one-accent rule (O2 legend +
     .visit-card assignee dots). Six muted hues at the Dispatch palette's
     own temperature (same family as --cobalt/--slate, not a new saturated
     set) -- cobalt itself is #1, per the architect ruling. */
  --person-1: #2456D6;
  --person-2: #1E8F8F;
  --person-3: #7C5CBF;
  --person-4: #C0722E;
  --person-5: #4F7A3D;
  --person-6: #B0507A;
  /* R2-U1: system additions (mockups/mock.css contract) -----------------
     danger (btn--danger + destructive forms), two radii (7 controls / 10
     surfaces, mechanically normalized off the old 5/6/8/20/999px zoo --
     999 stays for true pills), and a 4px-base spacing scale (named tokens
     for the values that already sit on that grid; the existing 1/2/3/5/6/
     10/14/18/20px gaps that don't are untouched here -- see build-notes/
     r2/u1-asbuilt.md for the full outlier-by-outlier accounting). */
  --danger: #B3382E;
  --danger-bg: #FBE9E7;
  --radius-control: 7px;
  --radius-surface: 10px;
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
}

*, *::before, *::after{ box-sizing: border-box; }

html, body{
  margin: 0;
  padding: 0;
  width: 100%;
  min-height: 100%;
  background: var(--page-bg);
  color: var(--text-primary);
  font-family: "Avenir Next", "Futura", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
  font-size: 15px;
  line-height: 1.45;
}

h1, h2, h3, p, ul{ margin: 0; }
ul{ padding: 0; list-style: none; }
img, svg{ max-width: 100%; display: block; }
a{ color: inherit; text-decoration: none; }
button{ font-family: inherit; }

/* R2-U1: one focus-visible ring, app-wide (INCOMPLETE #3's fix -- was 3
   form-field :focus rules and the UA default ring everywhere else).
   :focus-visible (not :focus) so a mouse/touch click never draws a ring,
   only real keyboard nav -- same cobalt as the 3 existing .field input/
   select/textarea :focus rules (box-shadow there, since those set
   outline:none on a bordered field; a plain outline here since this rule
   has to work on arbitrary elements -- links, chips-as-links, board
   blocks, buttons -- with no assumption about their own border/radius).
   Left untouched: the 3 existing rules keep their own recipe as-is. */
:focus-visible{
  outline: 2px solid var(--cobalt);
  outline-offset: 2px;
}

/* ==========================================================================
   Shell — top bar (desktop >=768px) + bottom tabs (mobile <768px)
   ========================================================================== */

.topbar{
  background: var(--slate);
  position: sticky;
  top: 0;
  z-index: 20;
}
.topbar-inner{
  max-width: 1200px;
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 16px;
  height: 56px;
  padding: 0 16px;
  min-width: 0;
}

.brand{
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--white);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: -0.01em;
  min-width: 0;
  flex-shrink: 1;
  min-height: 44px;
}
.brand-mark{
  width: 26px;
  height: 26px;
  border-radius: var(--radius-control);
  background: var(--cobalt);
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12.5px;
  font-weight: 700;
  flex-shrink: 0;
}
.brand-name{
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* nav + right-side actions: desktop only, hidden on mobile top bar.
   R6: .topbar-search is the desktop inline search pill (same >=768px
   breakpoint) -- .topbar-mobile-actions is its exact inverse (the
   icon-only search+bell pair, visible ONLY below 768px) so exactly one
   copy of each control is ever on screen. */
.topnav{ display: none; }
.topbar-actions{ display: none; }
.topbar-search{ display: none; }
.topbar-mobile-actions{ display: flex; align-items: center; gap: 4px; margin-left: auto; flex-shrink: 0; }

@media (min-width: 768px){
  .topbar-inner{ height: 64px; padding: 0 28px; gap: 32px; }
  .topnav{
    display: flex;
    align-items: center;
    gap: 4px;
    flex: 1;
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .topnav::-webkit-scrollbar{ display: none; }
  .topbar-search{ display: flex; }
  .topbar-actions{
    display: flex;
    align-items: center;
    gap: 12px;
    flex-shrink: 0;
  }
  .topbar-mobile-actions{ display: none; }
}

.topnav-link{
  display: inline-flex;
  align-items: center;
  color: rgba(255,255,255,0.62);
  font-size: 14px;
  font-weight: 600;
  min-height: 44px;
  padding: 0 14px;
  border-radius: var(--radius-control);
  white-space: nowrap;
}
.topnav-link:hover{ color: var(--white); }
.topnav-link.is-active{
  color: var(--white);
  background: rgba(255,255,255,0.12);
}

.btn-new{ white-space: nowrap; }

/* R4-W1: the topbar "+ New" dropdown -- newmenu.js toggles [hidden] on
   .new-menu-panel, same shape as .avatar-menu-panel just below (own
   class rather than a shared one: this one opens on a plain <button>
   toggle + JS, not a bare <details>, and sits LEFT of the avatar not
   right, so it needs its own left:0 instead of that one's right:0). */
.new-menu{ position: relative; flex-shrink: 0; }
.new-menu-panel{
  position: absolute;
  left: 0;
  top: calc(100% + 8px);
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  box-shadow: 0 14px 32px -10px rgba(35,44,54,0.32), 0 4px 12px rgba(35,44,54,0.1);
  min-width: 180px;
  max-width: calc(100vw - 24px);
  padding: 8px;
  z-index: 40;
}
.new-menu-item{
  display: block;
  width: 100%;
  text-align: left;
  padding: 9px 10px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
  border-radius: var(--radius-control);
}
.new-menu-item:hover{ background: var(--app-bg); }

/* avatar menu — plain <details>/<summary>, no JS needed */
.avatar-menu{ position: relative; flex-shrink: 0; }
.avatar-menu > summary{ list-style: none; cursor: pointer; }
.avatar-menu > summary::-webkit-details-marker{ display: none; }
.avatar-menu > summary::marker{ content: ""; }
.avatar{
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--cobalt);
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 12.5px;
  flex-shrink: 0;
}
.avatar-menu-panel{
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  box-shadow: 0 14px 32px -10px rgba(35,44,54,0.32), 0 4px 12px rgba(35,44,54,0.1);
  min-width: 180px;
  max-width: calc(100vw - 24px);
  padding: 8px;
  z-index: 40;
}
.avatar-menu-name{
  padding: 8px 10px 6px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-faint);
  border-bottom: 1px solid var(--border);
  margin-bottom: 4px;
}
/* R6: the avatar menu grew a second line (audit MISSING "Avatar menu ...
   should list: name + email") -- name stays the 12.5px/faint header
   word, email rides directly under it, one size down, same faint color
   (a label, not new data hierarchy). */
.avatar-menu-email{ display: block; font-size: 11px; font-weight: 500; color: var(--text-faint); }
.avatar-menu-item{
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 9px 10px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
  border-radius: var(--radius-control);
  cursor: pointer;
}
.avatar-menu-item:hover{ background: var(--app-bg); }

/* ==========================================================================
   R6 SLICE -- topbar bell + global search
   ========================================================================== */

/* -- bell: unseen-count badge, shared shape between the desktop
   .topbar-actions copy and the mobile .topbar-mobile-actions copy -- */
.topbar-bell{
  position: relative;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(255,255,255,0.08);
  color: rgba(255,255,255,0.78);
  flex-shrink: 0;
}
.topbar-bell:hover{ color: var(--white); }
.topbar-bell svg{ width: 18px; height: 18px; }
.topbar-bell-dot{
  position: absolute;
  top: 0;
  right: -1px;
  background: var(--danger);
  color: var(--white);
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  border-radius: 999px;
  padding: 2px 5px;
  min-width: 16px;
  text-align: center;
}
.topbar-icon-btn{
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(255,255,255,0.08);
  color: rgba(255,255,255,0.78);
  border: none;
  flex-shrink: 0;
  cursor: pointer;
}
.topbar-icon-btn:hover{ color: var(--white); }
.topbar-icon-btn svg{ width: 18px; height: 18px; }
/* B3: the mobile search-toggle's magnifier now carries its own
   .topbar-search-icon class (base.html) instead of surviving unclassed
   off the descendant rule above alone -- explicit size for it here
   rather than implicit inheritance (same idea as the panel-bar override
   just below .mobile-search-panel-bar). */
.topbar-mobile-actions .topbar-search-icon{ width: 18px; height: 18px; }

/* -- search: desktop inline pill (locked mock's own recipe,
   mockups/mock.css .t-search) -- */
.topbar-search{
  position: relative;
  align-items: center;
  gap: 8px;
  background: rgba(255,255,255,0.10);
  border: 1px solid rgba(255,255,255,0.14);
  color: rgba(255,255,255,0.68);
  font-size: 13px;
  min-height: 44px;
  padding: 0 12px;
  border-radius: 999px;
  min-width: 220px;
  /* B3: grown from 320px to the target mock's 460px (mockups/today-v2.css
     .tv2-search) -- new home is between Office and the bell/+New cluster,
     with room to actually read the longer placeholder now. */
  max-width: 460px;
  flex: 1;
}
.topbar-search-icon{ width: 15px; height: 15px; flex-shrink: 0; }
.topbar-search-input{
  background: none;
  border: none;
  outline: none;
  color: var(--white);
  font-size: 13px;
  font-family: inherit;
  flex: 1;
  min-width: 0;
  min-height: 44px;
}
.topbar-search-input::placeholder{ color: rgba(255,255,255,0.5); }
.topbar-search-input::-webkit-search-cancel-button{ filter: invert(1) opacity(0.6); }
.topbar-search-kbd{
  font-size: 11px;
  border: 1px solid rgba(255,255,255,0.25);
  border-radius: 4px;
  padding: 0 5px;
  color: rgba(255,255,255,0.7);
  flex-shrink: 0;
}

/* -- search results dropdown: shared markup/JS, two host contexts (the
   desktop pill below it, or the mobile full-width panel) -- */
.search-dropdown{
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 8px);
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  box-shadow: 0 14px 32px -10px rgba(35,44,54,0.32), 0 4px 12px rgba(35,44,54,0.1);
  max-height: 70vh;
  overflow-y: auto;
  padding: 6px;
  z-index: 40;
}
.search-dropdown--mobile{ position: static; box-shadow: none; border: none; border-top: 1px solid var(--border); border-radius: 0; max-height: none; flex: 1; overflow-y: auto; }
.search-group + .search-group{ margin-top: 4px; }
.search-group-label{
  padding: 8px 10px 4px;
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-faint);
}
.search-result{
  display: flex;
  flex-direction: column;
  gap: 1px;
  width: 100%;
  text-align: left;
  padding: 8px 10px;
  border-radius: var(--radius-control);
  color: var(--text-primary);
}
.search-result-label{ font-size: 14px; font-weight: 600; }
.search-result-sub{ font-size: 12.5px; color: var(--text-secondary); }
.search-result:hover, .search-result.is-active{ background: var(--app-bg); }
.search-empty{ padding: 14px 10px; font-size: 13px; color: var(--text-faint); }

/* -- mobile search panel: full-width overlay under the topbar, toggled
   via search.js ([hidden] attribute) -- */
.mobile-search-panel{
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  background: var(--white);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 14px 32px -10px rgba(35,44,54,0.28);
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - 56px);
  z-index: 39;
}
/* B3: an unconditional `display: flex` above beats the UA stylesheet's
   own [hidden]{display:none} rule -- same defect class already fixed
   once in this file for .job-new-customer (~1279). The server always
   renders hidden="" on this panel (base.html) and search.js toggles the
   same hidden property, but below the 768px guard just under this rule
   neither had any effect -- it rendered as a blank white bar under the
   header on load. Restated explicitly so hidden actually hides it. */
.mobile-search-panel[hidden]{ display: none; }
@media (min-width: 768px){ .mobile-search-panel{ display: none; } }
.mobile-search-panel-bar{
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
}
.mobile-search-panel-bar .topbar-search-icon{ color: var(--text-faint); width: 18px; height: 18px; }
.mobile-search-panel-bar .topbar-search-input{ color: var(--text-primary); font-size: 15px; }
.mobile-search-panel-bar .topbar-search-input::placeholder{ color: var(--text-faint); }
.mobile-search-close{
  background: none;
  border: none;
  font-size: 22px;
  line-height: 1;
  color: var(--text-faint);
  width: 44px;
  height: 44px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
}

/* bottom tabs — mobile only */
.bottomtabs{
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  background: var(--white);
  border-top: 1px solid var(--border);
  padding: 6px 6px calc(6px + env(safe-area-inset-bottom, 0px));
  z-index: 30;
}
@media (min-width: 768px){
  .bottomtabs{ display: none; }
}

.bottomtab{
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-1);
  font-size: 11px;
  font-weight: 600;
  color: var(--text-faint);
  padding: 6px 2px;
  border: none;
  background: none;
  cursor: pointer;
}
.bottomtab svg{ width: 22px; height: 22px; flex-shrink: 0; }
.bottomtab span{
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.bottomtab.is-active{ color: var(--cobalt); }

.bottomtab-more{ position: relative; flex: 1; min-width: 0; display: flex; }

.more-menu{
  display: none;
  position: absolute;
  right: 4px;
  bottom: calc(100% + 8px);
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  box-shadow: 0 14px 32px -10px rgba(35,44,54,0.32), 0 4px 12px rgba(35,44,54,0.1);
  min-width: 180px;
  max-width: calc(100vw - 16px);
  padding: 6px;
  z-index: 40;
}
.more-menu.is-open{ display: block; }
.more-menu-item{
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 10px 12px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
  border-radius: var(--radius-control);
  cursor: pointer;
}
.more-menu-item:hover{ background: var(--app-bg); }

/* main content well — fluid below 1200px, centered max-width above */
.shell-content{
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px 16px 88px;
}
@media (min-width: 768px){
  .shell-content{ padding: 32px 32px 56px; }
}

/* board-class screens earn width -- everything else keeps the calm 1200 cap */
.shell-content--wide{ max-width: 1720px; }

.shell-content--anon{
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px 20px;
  max-width: none;
}

/* ==========================================================================
   Components
   ========================================================================== */

.card{
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 24px;
}

/* R2-U4: font-size/padding brought to byte-parity with mockups/mock.css's
   own .chip rule (12.5px / 3px 10px, was 11px / 4px 10px -- the locked
   contract's chip text sits one step up the type scale from where this
   had landed, a mismatch the visual gate caught by direct computed-style
   diff against the mock). Padding shrank by the same 1px each side the
   font grew, so total pill height barely moves (verified live, no row
   overflow at either 1440 or 375). */
.chip{
  display: inline-flex;
  align-items: center;
  font-size: 12.5px;
  font-weight: 600;
  padding: 3px 10px;
  border-radius: 999px;
  white-space: nowrap;
}
.chip--done{ background: var(--mist-bg); color: var(--mist-text); }
.chip--next{ background: var(--amber-bg); color: var(--amber-text); }
.chip--neutral{ background: var(--slate-chip-bg); color: var(--slate-chip-text); }
/* R2-U4: the locked mock's own .chip vocabulary (mockups/mock.css) carries
   two color families this app.css didn't define at all --chip--scheduled
   (future-scheduled work, cobalt) and chip--overdue (danger, red) -- app.py/
   db.py's STATUS_CHIP currently folds "scheduled" into chip--next (amber,
   same bucket as "in progress") and has no overdue concept anywhere yet
   (BROKEN #1's own unreconciled-money finding is exactly why: "overdue"
   isn't a computed fact in this codebase today). Added here as pure,
   unwired primitives -- byte-identical to mock.css's own rule bodies,
   built off tokens app.css already carries (--cobalt-tint/--cobalt from
   the base palette, --danger/--danger-bg from R2-U1) -- so the token-level
   gap is closed even though the wiring decision (which states are
   "scheduled" vs "in progress", what "overdue" threshold means) is real
   product work this slice doesn't own. Filed for R3+ in this slice's
   as-built; zero current template references either class, so this is
   strictly additive. */
.chip--scheduled{ background: var(--cobalt-tint); color: var(--cobalt); }
.chip--overdue{ background: var(--danger-bg); color: var(--danger); }

.btn{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
  padding: 11px 20px;
  border-radius: var(--radius-control);
  border: 1px solid transparent;
  cursor: pointer;
  white-space: nowrap;
  font-family: inherit;
}
.btn--primary{ background: var(--cobalt); color: var(--white); }
.btn--primary:hover{ background: #1e49bd; }
.btn--quiet{ background: var(--white); color: var(--text-primary); border-color: var(--border); }
.btn--quiet:hover{ background: var(--app-bg); }
/* R2-U1: destructive variant (INCONSISTENT #3's fix -- Delete/Revoke used
   to share plain btn--quiet with harmless actions, no red anywhere). Wired
   to the existing Revoke/Deactivate/Confirm-remove buttons now; keeps
   every R1 undo/two-step-confirm behavior untouched, this only changes
   which class paints the button. */
.btn--danger{ background: var(--white); color: var(--danger); border-color: var(--border); }
.btn--danger:hover{ background: var(--danger-bg); border-color: var(--danger); }
.btn--block{ width: 100%; }
/* R2-U3 Button Law (INCONSISTENT #3): the one primary per card -- the
   card's commit action -- sits normal-width and right-aligned instead of
   stretching full-width (the .form-stack/.inline-add-form default of
   align-items:stretch). Applied ONLY to the specific commit buttons named
   in the as-built, never as a blanket .form-stack>.btn--primary selector
   -- that would also grab office_import.html's stacked Confirm/Start-over
   pair, which deliberately stay full-width siblings. */
.btn--commit{ align-self: flex-end; }

/* R4-W1: a create-form's own primary + quiet-Cancel row (customer_new.
   html / lead_new.html / jobs_owner.html's inline New Job card) -- a
   dedicated class rather than overloading .btn--commit's align-self,
   which only ever positions a LONE button. */
.form-actions{ display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }

/* form fields — label above, 1px border, cobalt focus ring */
.form-stack{ display: flex; flex-direction: column; gap: 16px; }
.field{ display: flex; flex-direction: column; gap: 6px; text-align: left; }
.field label{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.field input{
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 11px 13px;
  font-size: 15px;
  font-family: inherit;
  color: var(--text-primary);
  background: var(--white);
}
.field input:focus{
  outline: none;
  border-color: var(--cobalt);
  box-shadow: 0 0 0 3px var(--cobalt-tint);
}
.form-error{
  background: var(--amber-bg);
  color: var(--amber-text);
  font-size: 14px;
  font-weight: 600;
  padding: 10px 14px;
  border-radius: var(--radius-control);
  margin: 0 0 4px;
}
.form-message{
  background: var(--cobalt-tint);
  color: var(--cobalt);
  font-size: 14px;
  font-weight: 600;
  padding: 10px 14px;
  border-radius: var(--radius-control);
  margin: 0 0 4px;
}
.form-note{ font-size: 12.5px; color: var(--text-secondary); text-align: center; margin: 0; }
.form-note a{ color: var(--cobalt); font-weight: 600; }

/* flash notes — R1-T1: base.html renders these at the top of the main
   column from Flask's flash(); two kinds, same shape as .form-error/
   .form-message but page-level, dismissible, and (ok only) auto-hiding. */
.flash-stack{
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 0 0 20px;
}
.flash-note{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  border-radius: var(--radius-surface);
  padding: 12px 14px;
  font-size: 14px;
  font-weight: 600;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.flash-note--hiding{ opacity: 0; transform: translateY(-4px); }
.flash-note--ok{ background: var(--mist-bg); color: var(--mist-text); }
.flash-note--warn{ background: var(--amber-bg); color: var(--amber-text); }
.flash-note-text{ overflow-wrap: anywhere; }
.flash-note-dismiss{
  flex: none;
  border: none;
  background: transparent;
  color: inherit;
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 4px;
  opacity: 0.7;
}
.flash-note-dismiss:hover{ opacity: 1; }

/* branded error pages (400/403/404/405/409/500) — one plain sentence + one
   way back, same .card the rest of the app uses. */
.error-shell{
  min-height: 60vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px 20px;
}
.error-card{
  max-width: 420px;
  width: 100%;
  text-align: center;
  padding: 32px;
}
.error-card h1{ margin: 0 0 8px; font-size: 20px; }
.error-card p{ margin: 0 0 20px; color: var(--text-secondary); }

/* R2-U2: the two canonical empty-state treatments (audit UGLY #2 --
   10 one-off classes collapsed to these 2; see templates/_macros.html's
   empty_block()/empty_inline()). block = a plain sentence, used bare
   inside an existing card's own subsection and as the sole content of a
   dedicated `.card` for a whole empty list (the `.card` alone supplies
   the padding a "card empty" wrapper used to add on top). inline = a
   single faint word/dash standing in for a value inside a tight row/
   tile/cell -- context-specific layout is a scoped override below, keyed
   to the ancestor that was already there, not a 3rd class. */
.empty-block{
  margin: 0;
  max-width: 40ch;
  color: var(--text-secondary);
  font-size: 14px;
  font-weight: 500;
}
.empty-inline{
  margin: 0;
  color: var(--text-faint);
  font-size: 12.5px;
  font-weight: 500;
}
/* .money-tile has a dark fill -- text-secondary/faint read invisible on
   it, so the empty word there keeps the tile's own light-on-dark
   treatment (was .money-tile-empty, same two values). */
.money-tile .empty-inline{ font-size: 15px; color: rgba(255,255,255,0.85); }
/* .week-col-visits / .pattern-day-items are narrow, centered day columns
   (was .week-col-empty / .pattern-day-empty's own text-align+padding). */
.week-col .empty-inline{ display: block; text-align: center; padding: 10px 0; }
.pattern-day .empty-inline{ display: block; text-align: center; padding: 6px 0; }
/* the find-a-slot "no openings" word sits in its own breathing room
   below the visit form's other fields (was .slot-empty's own margin). */
.visit-form-card .empty-inline{ display: block; margin: 2px 0 20px; }

/* ==========================================================================
   Auth pages — centered card on --page-bg, wordmark above
   ========================================================================== */

.auth-wrap{
  width: 100%;
  max-width: 400px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;
}
.auth-wordmark{
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--slate-deep);
  font-weight: 600;
  font-size: 17px;
  letter-spacing: -0.01em;
}
.auth-wordmark .brand-mark{ width: 32px; height: 32px; border-radius: var(--radius-surface); font-size: 15px; }
.auth-card{ width: 100%; }
.auth-card h1{
  font-size: 20px;
  font-weight: 600;
  color: var(--slate-deep);
  margin: 0 0 4px;
}
.auth-card .page-sub{ margin: 0 0 20px; }

/* ==========================================================================
   Page layout helpers used by C1 / O1 / area stub
   ========================================================================== */

.page-head{ margin-bottom: 24px; }
.page-head h1{ font-size: 20px; font-weight: 600; color: var(--slate-deep); }
.page-sub{ color: var(--text-secondary); font-size: 14px; font-weight: 500; margin-top: 4px; }
/* R4-W1: additive modifier only -- .page-head alone stays the plain block
   above on every page that doesn't add this class (/office/settings,
   /home, ...); pages with a header-row "+ New X" action (jobs/customers/
   leads) opt in. Same recipe as schedule_owner.html's own .o2-head-row. */
.page-head--with-action{ display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }

.greet{ display: flex; align-items: center; gap: 12px; margin-bottom: 24px; }
.greet .avatar{ width: 44px; height: 44px; font-size: 17px; flex-shrink: 0; }
.greet-text{ display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.greet-text h1{ font-size: 20px; font-weight: 600; color: var(--slate-deep); }

.section-head{ font-size: 12.5px; font-weight: 600; color: var(--text-faint); margin: 0 0 10px; }

.grid-2{ display: grid; grid-template-columns: 1fr; gap: 20px; }
@media (min-width: 700px){
  .grid-2{ grid-template-columns: 1fr 1fr; }
}

/* ==========================================================================
   PKG 2 — day surface (stop cards, stage buttons, log entries, sync word)
   Everything below reuses the :root token set above — no new colors.
   Rhythm: 16/24px spacing. Checked for no horizontal scroll at 320px wide,
   including with #log-details open (long addresses/titles wrap, not scroll).
   ========================================================================== */

.stop-list{ display: flex; flex-direction: column; gap: 16px; margin-bottom: 24px; }

.stop-card{
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 16px;
  min-width: 0;
}
.stop-top{
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 6px;
}
.stop-time{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.stop-title{
  font-size: 15px;
  font-weight: 600;
  color: var(--slate-deep);
  margin: 0 0 3px;
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.stop-number{ font-weight: 500; color: var(--text-secondary); }
.stop-customer{ font-size: 14px; color: var(--text-secondary); margin: 0 0 2px; overflow-wrap: anywhere; }
/* R3-V6 (BUILD "stop card lists the whole crew"): "Crew 1 — you, Jake,
   Rosa, Silvio" -- a quiet line between the customer and the address,
   same weight/color as .stop-customer (identity info, not an action). */
.stop-crew{ font-size: 12.5px; color: var(--text-faint); margin: 0 0 6px; overflow-wrap: anywhere; }
.stop-address{
  display: inline-block;
  font-size: 14px;
  color: var(--cobalt);
  font-weight: 500;
  margin: 0 0 8px;
  overflow-wrap: anywhere;
}
.stop-instructions{
  font-size: 12.5px;
  color: var(--text-secondary);
  background: var(--app-bg);
  border-radius: var(--radius-control);
  padding: 8px 10px;
  margin: 0 0 12px;
  overflow-wrap: anywhere;
}
/* R3-V5: the per-VISIT note (distinct from the job-wide .stop-instructions
   just above it) -- a cobalt left edge is the one visual cue that this is
   specific to THIS stop, not the whole job. */
.stop-instructions--visit{ border-left: 3px solid var(--cobalt); background: var(--cobalt-tint); }
.sched-instructions{
  font-size: 12.5px;
  color: var(--text-secondary);
  background: var(--cobalt-tint);
  border-left: 3px solid var(--cobalt);
  border-radius: var(--radius-control);
  padding: 6px 9px;
  margin: 6px 0 0;
  overflow-wrap: anywhere;
}

.stage-row{ display: flex; gap: 6px; }
.stage-btn{
  flex: 1;
  min-width: 0;
  text-align: center;
  font-size: 12.5px;
  font-weight: 600;
  font-family: inherit;
  padding: 9px 4px;
  border-radius: var(--radius-control);
  border: 1px solid var(--border);
  color: var(--text-faint);
  background: var(--app-bg);
  cursor: pointer;
}
.stage-btn.is-done{ background: var(--mist-bg); border-color: var(--mist-bg); color: var(--mist-text); }
.stage-btn.is-active{ background: var(--cobalt); border-color: var(--cobalt); color: var(--white); }
.stage-btn:disabled{ cursor: default; }

.log-section-head{ margin-top: 24px; }

.entries-list{ display: flex; flex-direction: column; }

.entry-row{
  padding: 12px 0;
  border-top: 1px solid var(--border);
  min-width: 0;
}
.entry-row:first-child{ border-top: none; padding-top: 0; }
.entry-row:last-child{ padding-bottom: 0; }
.entry-row-main{
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
}
.entry-job{ color: var(--text-secondary); font-weight: 600; flex-shrink: 0; }
.entry-notes{ font-size: 14px; color: var(--text-secondary); margin: 4px 0 0; overflow-wrap: anywhere; }
.entry-row-foot{ display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; }
.entry-photos{ font-size: 12.5px; color: var(--text-faint); font-weight: 500; }
.entry-edit-btn{
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--cobalt);
  cursor: pointer;
  margin-left: auto;
}
.entry-status{ display: block; font-size: 12.5px; font-weight: 600; color: var(--amber-text); margin-top: 4px; }

.entry-row.entry--waiting{
  background: var(--amber-bg);
  border-top: none;
  border-radius: var(--radius-control);
  padding: 10px 12px;
  margin: 4px 0;
}
.entry-row.entry--waiting + .entry-row{ border-top: none; }

/* log work — <details>/<summary>, no JS needed to open it */
.log-details{ margin-top: 16px; }
.log-details > summary{
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: 600;
  padding: 11px 20px;
  border-radius: var(--radius-control);
  background: var(--cobalt);
  color: var(--white);
}
.log-details > summary::-webkit-details-marker{ display: none; }
.log-details > summary::marker{ content: ""; }
/* R0-S1: edit mode's one visible signal on the drawer itself */
.log-details.log-details--editing > summary{ background: var(--amber-bg); color: var(--amber-text); }
.log-form{ margin-top: 12px; max-width: 100%; }
.log-form-actions{ display: flex; flex-direction: column; gap: 8px; }

.field-row{ display: flex; flex-wrap: wrap; gap: 12px; }
.field-row .field{ flex: 1; min-width: 120px; }

.field select,
.field textarea{
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 11px 13px;
  font-size: 15px;
  font-family: inherit;
  color: var(--text-primary);
  background: var(--white);
  max-width: 100%;
}
.field textarea{ resize: vertical; min-height: 64px; }
.field select:focus,
.field textarea:focus{
  outline: none;
  border-color: var(--cobalt);
  box-shadow: 0 0 0 3px var(--cobalt-tint);
}

/* sync word — quiet header control, crew pages only */
.sync-wrap{ position: relative; display: flex; align-items: center; margin-left: auto; flex-shrink: 0; }
@media (min-width: 768px){
  .sync-wrap{ margin-left: 0; }
}
#sync-word{
  appearance: none;
  -webkit-appearance: none;
  border: none;
  background: none;
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-secondary);
  padding: 6px 4px;
  cursor: pointer;
  white-space: nowrap;
}
#sync-detail{
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  box-shadow: 0 14px 32px -10px rgba(35,44,54,0.32), 0 4px 12px rgba(35,44,54,0.1);
  min-width: 220px;
  max-width: calc(100vw - 24px);
  padding: 8px;
  z-index: 40;
}
.sync-detail-item,
.sync-detail-note{
  font-size: 12.5px;
  padding: 6px 8px;
  color: var(--text-secondary);
}
/* R2-U3: "All caught up." (was .sync-detail-empty, one of the audit's own
   named 10 ad-hoc empty-state classes -- U2's sweep missed it). Same
   .empty-inline base every other tight-row/tile empty word now uses, kept
   at text-secondary/this row's own padding via a scoped override -- this
   word is the crew's own confirmation the offline queue is really clear
   (same "reads as data, not decoration" reasoning R2-U1 gave #sync-word
   its own contrast upgrade for). */
#sync-detail .empty-inline{ color: var(--text-secondary); padding: 6px 8px; }
/* R0-S1: a stuck (tries>=2) item's server error + its Retry/Remove pair */
.sync-detail-item-error{ color: var(--amber-text); font-size: 12.5px; margin: 2px 0 0; }
.sync-detail-item-actions{ display: flex; gap: 10px; margin-top: 4px; }
.sync-detail-retry-btn,
.sync-detail-remove-btn{
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--cobalt);
  cursor: pointer;
}
.sync-detail-remove-btn{ color: var(--text-secondary); }

/* R0-S1: a conflict/signed-out note that survives the reconciliation
   reload, shown as a dismissible banner on /today */
.sync-note-banner{
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  background: var(--amber-bg);
  color: var(--amber-text);
  font-size: 14px;
  font-weight: 600;
  border-radius: var(--radius-surface);
  padding: 10px 12px;
  margin: 0 0 16px;
}
.sync-note-dismiss{
  background: none;
  border: none;
  font-size: 17px;
  line-height: 1;
  color: inherit;
  cursor: pointer;
  padding: 0 2px;
  flex-shrink: 0;
}
.sync-detail-note{ color: var(--amber-text); font-weight: 600; }

/* ==========================================================================
   ---------- pkg 3 ----------
   C1 week strip + next/done-slim stop cards (B-DAY), C2 job type-ahead +
   duration word (B-DAY), C3 schedule cards (B-PAGES), C5 /more (B-PAGES).
   Tokens only, 16/24 rhythm, no shadows beyond existing idiom (avatar-menu/
   more-menu box-shadow reused verbatim), checked at 320px.
   ========================================================================== */

/* -- R3-V6 (audit MISSING "Week navigation beyond the current week...
   on any Sunday the crew cannot see Monday's schedule at all"): C3's own
   ‹ › chevrons flanking the week strip -- any week is reachable now, not
   just the one containing today. C1 (today_crew.html) doesn't wrap its
   own .week-strip in this (Today is deliberately today-only, no date
   param there), so .week-strip-nav is C3-only; .week-strip itself is
   unchanged and still shared verbatim by both templates. -- */
.week-strip-nav{ display: flex; align-items: center; gap: 8px; margin-bottom: 24px; }
/* min-width:0 is load-bearing here -- a nested flex container's own
   default min-width is `auto` (its content's min-content size), NOT 0,
   even though every .week-day child inside it already sets its own
   min-width:0 -- without this the row refuses to shrink past its 7
   chips' combined min-content width and forces page-level h-scroll at
   320px once the two 44px chevrons are added alongside it. */
.week-strip-nav .week-strip{ flex: 1; min-width: 0; margin-bottom: 0; }
.week-nav-chev{
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  background: var(--white);
  color: var(--text-secondary);
  font-size: 18px;
  font-weight: 600;
}
.week-nav-chev:hover{ border-color: var(--cobalt); color: var(--cobalt); }

/* -- week strip (C1 + C3, same markup/classes on both pages) -- */
.week-strip{ display: flex; gap: 6px; margin-bottom: 24px; }
.week-day{
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 10px 2px;
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  background: var(--white);
}
.week-day-dow{ font-size: 11px; font-weight: 600; color: var(--text-faint); text-transform: uppercase; }
.week-day-num{ font-size: 15px; font-weight: 600; color: var(--text-primary); }
.week-day--today{ border-color: var(--cobalt); background: var(--cobalt-tint); }
.week-day--today .week-day-dow,
.week-day--today .week-day-num{ color: var(--cobalt); }
/* R3-V4: SELECTED is a filled cobalt state, distinct from TODAY's own
   outline+tint -- audit BROKEN "the strip highlights TODAY, never the
   selected day" (schedule_owner.html's Day view is the one template that
   emits this class this rung; C1/C3 keep their pre-existing --today-only
   behavior, out of this rung's own file scope). A day that's both today
   AND selected gets --selected's filled look (it wins -- last rule in
   the cascade, and the more informative of the two states when a click
   already told the owner where they are). */
.week-day--selected{ border-color: var(--cobalt); background: var(--cobalt); }
.week-day--selected .week-day-dow,
.week-day--selected .week-day-num{ color: var(--white); }
.week-day--selected .dot--planned{ border-color: var(--white); }
.week-day--selected .dot--logged{ background: var(--white); }
.week-dots{ display: flex; align-items: center; gap: var(--space-1); min-height: 6px; }
.dot--planned,
.dot--logged{ width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
.dot--planned{ border: 1.5px solid var(--cobalt); background: transparent; }
.dot--logged{ background: var(--cobalt); }

/* -- C1 next stop + done-slim (B-DAY's stop-card markup) -- */
.stop-top-left{ display: flex; align-items: center; gap: 8px; min-width: 0; }
.stop-card--next{ border-color: var(--cobalt); background: var(--cobalt-tint); }
.stop-next-chip{ flex-shrink: 0; }

.stop-card--done-slim{
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
}
.stop-card--done-slim .stop-time{ flex-shrink: 0; }
.stop-card--done-slim .stop-title{
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-secondary);
  /* RV4 W3: crew-v4 crosses the TITLE off, not just fades it -- the green
     "Logged" treatment Karissa approved in round 1 ("the model for what
     DONE looks like on both sides"). */
  text-decoration: line-through;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.stop-card--done-slim .chip{ flex-shrink: 0; }

/* -- C2 job type-ahead dropdown -- */
.job-typeahead{ position: relative; }
#log-job-list{
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 4px);
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  box-shadow: 0 14px 32px -10px rgba(35,44,54,0.32), 0 4px 12px rgba(35,44,54,0.1);
  max-height: 260px;
  overflow-y: auto;
  padding: 6px;
  z-index: 35;
}
#log-job-list [data-option]{
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 10px 12px;
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  color: var(--text-primary);
  border-radius: var(--radius-control);
  cursor: pointer;
  overflow-wrap: anywhere;
}
#log-job-list [data-option]:hover,
#log-job-list [data-option].is-active{ background: var(--app-bg); }
#log-job-list [data-option][data-new-job]{ color: var(--cobalt); font-weight: 600; }

/* R4-W1: /jobs?new=1's customer type-ahead dropdown -- same visual
   recipe as #log-job-list above, a class (not an id) since .job-cust-
   list is this page's own element, not a second copy of that one. */
.job-cust-list{
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 4px);
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  box-shadow: 0 14px 32px -10px rgba(35,44,54,0.32), 0 4px 12px rgba(35,44,54,0.1);
  max-height: 260px;
  overflow-y: auto;
  padding: 6px;
  z-index: 35;
}
.job-cust-list [data-option]{
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  padding: 10px 12px;
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  color: var(--text-primary);
  border-radius: var(--radius-control);
  cursor: pointer;
  overflow-wrap: anywhere;
}
.job-cust-list [data-option]:hover,
.job-cust-list [data-option].is-active{ background: var(--app-bg); }
.job-cust-list [data-option][data-new-customer]{ color: var(--cobalt); font-weight: 600; }
/* the "+ New customer instead" toggle reuses .billing-link-action's
   quiet-cobalt-text look but needs its own <button> reset (no inherited
   border/background/font on a bare <button> the way an <a> already has
   none of). */
.job-cust-new-toggle{ display: block; margin: 2px 0 12px; background: none; border: none; padding: 0; font: inherit; cursor: pointer; }
.job-new-customer{ display: flex; flex-direction: column; gap: 16px; margin-bottom: 16px; padding: 14px; background: var(--app-bg); border-radius: var(--radius-control); }
/* an unconditional `display: flex` above beats the UA stylesheet's own
   [hidden]{display:none} rule (author styles always win over UA ones,
   specificity aside) -- verified live: the server correctly rendered
   hidden="" on a fresh /jobs?new=1 load, yet the New-customer fields
   still showed. Restated explicitly so the server's own hidden attribute
   (jobs_owner.html) actually hides the block again. */
.job-new-customer[hidden]{ display: none; }

/* -- C2 duration word -- */
#log-duration{ font-size: 12.5px; font-weight: 500; color: var(--text-secondary); min-height: 16px; }

/* -- C3 schedule cards (read-only -- no .stage-row on this page) -- */
.sched-day-head{ font-size: 17px; font-weight: 600; color: var(--slate-deep); margin: 0 0 12px; }
.sched-list{ display: flex; flex-direction: column; gap: 12px; }
.sched-card{
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 14px 16px;
  min-width: 0;
}
.sched-card--logged{ background: var(--cobalt-tint); border-color: var(--cobalt); }
/* R3-V6 (audit DEAD "Crew schedule day cards are inert DIVs... a
   read-only dead end"): .sched-card-open wraps the identity block
   (time/chip/title/customer) in a real link into the job thread -- the
   card itself stays a plain div (not a link) so .sched-address below can
   be a SIBLING anchor, never nested inside another one. display:block +
   the same text colors as the plain text it replaces keeps this
   invisible as a "link" until touched/hovered -- it's the whole card
   that should read as tappable, not a blue title. */
.sched-card-open{ display: block; color: inherit; text-decoration: none; }
.sched-top{ display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
.sched-time{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.sched-title{ font-size: 15px; font-weight: 600; color: var(--slate-deep); margin: 0 0 3px; overflow-wrap: anywhere; }
.sched-number{ font-weight: 500; color: var(--text-secondary); }
.sched-customer{ font-size: 14px; color: var(--text-secondary); margin: 0; overflow-wrap: anywhere; }
/* R3-V6 (audit MISSING "Address/instructions on future-day schedule
   cards"): same visual recipe as C1's own .stop-address. */
.sched-address{
  display: inline-block;
  font-size: 13.5px;
  color: var(--cobalt);
  font-weight: 500;
  margin: 8px 0 0;
  overflow-wrap: anywhere;
}

/* -- C5 /more -- */
.more-page{ display: flex; flex-direction: column; gap: 20px; }
@media (min-width: 700px){
  .more-page{ max-width: 480px; }
}
.profile-row{
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 12px;
  padding: 8px 0;
  border-top: 1px solid var(--border);
  font-size: 14px;
}
.profile-row:first-child{ border-top: none; padding-top: 0; }
.profile-label{ color: var(--text-faint); font-weight: 600; }
.profile-value{ color: var(--text-primary); font-weight: 500; text-align: right; overflow-wrap: anywhere; }

#sync-detail-page .sync-detail-item{
  font-size: 14px;
  font-weight: 500;
  color: var(--text-primary);
  padding: 8px 0;
  border-top: 1px solid var(--border);
}
#sync-detail-page .sync-detail-item:first-child{ border-top: none; padding-top: 0; }
/* "All caught up." on this full-page rendering (was .sync-detail-empty)
   is exactly .empty-block's own spec (14px/text-secondary/500) -- no
   override needed, see static/more.html's own render(). */

/* ==========================================================================
   ---------- pkg 4 ----------
   Jobs list (B-LIST): KPI tiles, filter chips, search, job rows. Job thread
   (B-THREAD): header + status rail, visits, log timeline, photo grid,
   punch/checklist rows, docs/equipment rows. Tokens only, 16/24 rhythm,
   checked for no horizontal scroll at 320px. Zero new colors.
   ========================================================================== */

/* -- jobs list (owner): KPI tiles that reconcile with the row count -- */
.kpi-row{ display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 20px; }
.kpi-tile{
  flex: 1;
  min-width: 140px;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.kpi-num{ font-size: 26px; font-weight: 700; color: var(--cobalt); line-height: 1; }
.kpi-label{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.kpi-tile--active{ border-color: var(--cobalt); background: var(--cobalt-tint); }

/* -- jobs list: status filter chips (All + the 7 statuses) -- */
.filter-row{ display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; }
.filter-chip{
  display: inline-flex;
  align-items: center;
  font-size: 12.5px;
  font-weight: 600;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--white);
  border: 1px solid var(--border);
  color: var(--text-secondary);
}
.filter-chip.is-active{ background: var(--cobalt); border-color: var(--cobalt); color: var(--white); }

/* -- R4-W4 SLICE W4 BUILD 4: town chips collapse into this select past 6
   towns (audit UGLY "26 towns render 337px of chips -- nearly half the
   phone viewport"). Plain GET form, submits on change -- no JS file, one
   inline onchange attribute, same footprint as a filter-chip row. -- */
.town-select-form{ margin-bottom: 16px; }
.town-select{
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-secondary);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 6px 12px;
  background: var(--white);
  max-width: 100%;
}

/* -- jobs list: search -- */
.jobs-search{ margin-bottom: 16px; }
.jobs-search input{
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 11px 13px;
  font-size: 15px;
  font-family: inherit;
  color: var(--text-primary);
  background: var(--white);
}
.jobs-search input:focus{
  outline: none;
  border-color: var(--cobalt);
  box-shadow: 0 0 0 3px var(--cobalt-tint);
}

/* -- jobs list: rows (owner + crew share this component) -- */
.jobs-list{ display: flex; flex-direction: column; gap: 10px; }
.job-row{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 14px 16px;
  min-width: 0;
}
.job-row-main{ min-width: 0; flex: 1; }
.job-row-number{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.job-row-title{ font-size: 15px; font-weight: 600; color: var(--slate-deep); margin: 2px 0; overflow-wrap: anywhere; }
.job-row-sub{ font-size: 12.5px; color: var(--text-secondary); overflow-wrap: anywhere; }

/* -- R4-W4 SLICE W4 BUILD 4: the owner list's crew/next-visit/amount
   columns (mockups/jobs-list.html's own locked target -- they'd never
   actually landed). `flex-basis:100%` drops this row onto its own full-
   width line below main+chip at every width (the SAME wrap idiom
   .lead-row .status-row already established), rather than a rigid grid
   that would need separate mobile overrides. Owner-only -- jobs_crew.html
   never emits this markup (money wall), so no crew-facing style needed. -- */
.job-row-meta{
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 14px;
  font-size: 12.5px;
  color: var(--text-secondary);
}
.job-row-amount{ font-weight: 600; color: var(--slate-deep); margin-left: auto; }

/* -- job thread: header card (title+number, customer, address, instructions) -- */
.thread-header{ margin-bottom: 20px; }
.thread-header-top{ display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
.thread-header-titles{ min-width: 0; }
.thread-title{ font-size: 20px; font-weight: 600; color: var(--slate-deep); line-height: 1.3; overflow-wrap: anywhere; }
.thread-number{ font-weight: 500; color: var(--text-secondary); font-size: 14px; }
.thread-customer{ font-size: 14px; color: var(--text-secondary); margin-top: 2px; overflow-wrap: anywhere; }
.thread-customer a{ color: inherit; text-decoration: underline; }
/* -- R4-W4 SLICE W4 BUILD 1: the job thread header's customer tel:/
   mailto: line (audit MISSING "Job thread - header (customer identity)
   ... no phone or email appears anywhere on the thread") -- reuses
   .customer-contact-line's own recipe (the customer profile's read-view
   contact links, R4-W2) verbatim rather than a third color/weight. -- */
.thread-customer-contact{ display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 2px; }
.thread-status-chip{ flex-shrink: 0; }
.thread-address{
  display: inline-block;
  font-size: 14px;
  color: var(--cobalt);
  font-weight: 500;
  margin: 0 0 8px;
  overflow-wrap: anywhere;
}
.thread-instructions{
  font-size: 12.5px;
  color: var(--text-secondary);
  background: var(--app-bg);
  border-radius: var(--radius-control);
  padding: 8px 10px;
  margin: 0 0 4px;
  overflow-wrap: anywhere;
}

/* -- job thread: R4-W2 BUILD 1 "editing job facts" -- the Edit link that
   opens it, and the card itself (a form-stack inside a quiet inset box so
   it doesn't read as just another line of header text). -- */
.thread-edit-link{ margin: 4px 0 8px; }
.thread-edit-card{
  background: var(--app-bg);
  border-radius: var(--radius-control);
  padding: 14px;
  margin: 4px 0 8px;
}

/* -- job thread: owner status rail, 7 one-click buttons, current filled -- */
.status-rail{ display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.status-form{ flex: 1; min-width: 90px; }
.status-btn{
  width: 100%;
  text-align: center;
  font-size: 12.5px;
  font-weight: 600;
  font-family: inherit;
  padding: 9px 4px;
  border-radius: var(--radius-control);
  border: 1px solid var(--border);
  color: var(--text-faint);
  background: var(--app-bg);
  cursor: pointer;
}
.status-btn--current{ background: var(--cobalt); border-color: var(--cobalt); color: var(--white); }

/* -- job thread: shared section rhythm (empty word is .empty-block now,
   templates/_macros.html's empty_block()) -- */
.thread-section{ margin-bottom: 24px; }

/* -- R4-W3 BUILD 4 "thread cards collapse when empty" (locked mock, audit's
   own fix wording: "collapse empty sections to one-line rows with an add
   action") -- Photos/Documents/Punch/Checklist/Equipment render this ONE
   quiet line instead of the full .section-head + .card treatment whenever
   they have nothing in them (and, for the three with an upload/add form,
   that form hasn't been opened). No card box on purpose -- the whole
   point is dropping the visual weight a permanently-empty section used
   to carry on a brand-new job's 13-stacked-card page. Built from the
   SAME two pieces every other empty state already uses (empty_block()'s
   <p> + a plain .btn), just laid out as a row instead of stacked. -- */
.thread-collapsed{ display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 10px; }

/* -- job thread: visits (owner only) -- */
.visit-list{ display: flex; flex-direction: column; }
.visit-row{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
  font-size: 14px;
}
.visit-row:first-child{ border-top: none; padding-top: 0; }
.visit-date{ font-weight: 600; color: var(--text-primary); }
.visit-time{ color: var(--text-secondary); }
.visit-assignees{ color: var(--text-secondary); margin-left: auto; }

/* -- job thread: log timeline, day groups + entry rows -- */
.timeline-total{ font-size: 14px; font-weight: 600; color: var(--slate-deep); margin: 0 0 16px; }
.timeline-days{ display: flex; flex-direction: column; gap: 20px; }
.timeline-day-head{ display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
.timeline-day-date{ font-size: 14px; font-weight: 600; color: var(--slate-deep); }
.timeline-day-hours{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.timeline-entry{ padding: 10px 0; border-top: 1px solid var(--border); }
.timeline-entry:first-child{ border-top: none; padding-top: 0; }
.timeline-entry-main{
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
}
.timeline-entry-edit{ font-size: 12.5px; font-weight: 600; color: var(--cobalt); flex-shrink: 0; }
.timeline-entry-notes{ font-size: 12.5px; color: var(--text-secondary); margin: 4px 0 0; overflow-wrap: anywhere; }
.timeline-entry-photos{ display: block; font-size: 12.5px; color: var(--text-faint); font-weight: 500; margin-top: 4px; }

/* -- R4-W2 BUILD 3 "timesheet truth" -- the owner-only whose-entry word,
   the Edit-reveal's own edit-mode row, and the merged-in correction
   sentence (kind='correction' — plain quiet text, never itself an
   editable row, the locked mock's "You corrected Jake's Thursday entry
   ..."). -- */
.timeline-entry-user{ font-size: 12.5px; font-weight: 500; color: var(--text-secondary); }
/* W4 item 1/4 -- a day_done ("Day log") row on the job thread's own
   timeline, tinted the same way the Today page's own .tdy-daylog row is
   (same --app-bg fill) so it visually reads as a day total, not a plain
   photo/note log, wherever it shows up. */
.timeline-entry--daylog{ background: var(--app-bg); border-radius: 7px; padding: 10px 12px; }
.timeline-entry--daylog:first-child{ padding-top: 10px; }
.timeline-entry--daylog .timeline-entry-notes b{ color: var(--text-primary); }
.timeline-entry--edit{ padding-top: 10px; border-top: 1px solid var(--border); }
.timeline-entry--edit:first-child{ border-top: none; padding-top: 0; }
.timeline-entry--edit .inline-add-form{ margin-top: 0; padding-top: 0; border-top: none; }
.timeline-correction{
  padding: 10px 0;
  border-top: 1px solid var(--border);
  font-size: 12.5px;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}
.timeline-correction:first-child{ border-top: none; padding-top: 0; }

/* -- section-head with a trailing quiet action button (Work log's own
   "+ Add entry" — the locked mock's header button), additive modifier
   like .page-head--with-action; every OTHER .section-head in the app
   stays the plain block-text label it always was. -- */
.section-head--with-action{ display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.section-head-action{ flex-shrink: 0; }

/* -- job thread: photo grid, aspect-square thumbs -- */
.photo-grid{ display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 8px; }
.photo-tile{ display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.photo-tile img{
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--radius-surface);
  border: 1px solid var(--border);
  background: var(--app-bg);
}
.photo-tag{ font-size: 11px; font-weight: 600; color: var(--text-faint); text-align: center; }

/* -- job thread: punch list + checklist rows -- */
.check-list{ display: flex; flex-direction: column; }
.check-row{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.check-row:first-child{ border-top: none; padding-top: 0; }
.check-text{ font-size: 14px; color: var(--text-primary); overflow-wrap: anywhere; }

/* -- R4-W3 BUILD 3 "punch list + checklist come alive" -- the tap-to-
   toggle checkbox (owner AND assigned crew) and its row's trailing
   Done-chip/Delete cluster. .check-toggle-form takes the row's remaining
   width so .check-row-actions (chip + owner-only Delete) stays pinned
   right, same two-child space-between shape .check-row already had. The
   checkbox itself is sized well past the 13px .photo-public-check one --
   this is a primary field action (a crew member ticking it off a ladder),
   not a quiet courtesy toggle. -- */
.check-toggle-form{ flex: 1; min-width: 0; }
.check-toggle{ display: flex; align-items: center; gap: 10px; cursor: pointer; }
.check-toggle input{ width: 18px; height: 18px; flex-shrink: 0; accent-color: var(--cobalt); }
.check-row-actions{ display: flex; align-items: center; gap: 10px; flex-shrink: 0; }

/* -- R7 SLICE: O1's first-run checklist (home_owner.html) -- each row is
   a real link, not a toggle (these check off from real data, never a
   click) -- .check-row/.check-text/.check-list above give it the same
   list rhythm; .onboard-dot is the only new visual, an empty ring that
   fills solid mist-green with a drawn checkmark once its own thing
   exists (same --mist-text green .chip--done already uses). -- */
.onboard-row{ text-decoration: none; }
.onboard-row:hover .check-text{ color: var(--cobalt); }
.onboard-dot{
  width: 18px; height: 18px; flex-shrink: 0; border-radius: 999px;
  border: 2px solid var(--border); position: relative;
}
.onboard-row--done .onboard-dot{ border-color: var(--mist-text); background: var(--mist-text); }
.onboard-row--done .onboard-dot::after{
  content: ""; position: absolute; left: 5px; top: 1px; width: 4px; height: 8px;
  border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.onboard-row--done .check-text{ color: var(--text-secondary); }

/* -- job thread: documents + equipment (equipment stays owner only;
   documents opened to any role with job access in R4-W3 BUILD 2) -- */
.doc-list, .equipment-list{ display: flex; flex-direction: column; }
.doc-row{
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
  font-size: 14px;
}
.doc-row:first-child{ border-top: none; padding-top: 0; }
.doc-label{ font-weight: 600; color: var(--text-primary); }
.doc-filename{ color: var(--cobalt); font-weight: 500; overflow-wrap: anywhere; }
/* R4-W3 BUILD 2: .doc-row itself used to BE the <a> (label+filename, no
   delete control -- documents were read-only). Now that an owner can
   delete one, .doc-row is a plain flex wrapper and .doc-row-link is the
   nested <a> carrying the old two-span layout, so the Delete form has
   somewhere to sit beside it without an <a> illegally wrapping a <form>.
   The Billing card's own report rows still use bare `<a class="doc-row">`
   (reports aren't deletable) -- untouched, .doc-row's own rule above
   still lays those out exactly as before. -- */
.doc-row-link{ display: flex; align-items: baseline; gap: 10px; flex: 1; min-width: 0; }
.equipment-row{ padding: 10px 0; border-top: 1px solid var(--border); }
.equipment-row:first-child{ border-top: none; padding-top: 0; }
.equipment-label{ display: block; font-size: 14px; font-weight: 600; color: var(--text-primary); }
.equipment-detail{ display: block; font-size: 12.5px; color: var(--text-secondary); margin-top: 2px; overflow-wrap: anywhere; }
/* R1-T3: customer_profile.html's equipment rows also carry a Delete
   control (MISSING #2 -- equipment could never be deleted before this;
   job_thread.html's own read-only .equipment-row is untouched, this is
   an additive modifier only) -- same flex shape as .cf-row/.cf-row-info
   just below, so label+detail sit beside Delete instead of stacking
   under a bare block row. */
.equipment-row--edit{ display: flex; align-items: center; gap: 10px; }
.equipment-row-info{ flex: 1; min-width: 160px; display: flex; flex-direction: column; gap: 2px; }
/* R4-W2 BUILD 5 -- the revealed edit-mode row: block (not flex like
   .equipment-row--edit above), same reasoning as .quote-line--edit/
   .bill-line--edit in the money section — a single form child should
   lay out full-width, not shrink-to-fit as one centered flex item. */
.equipment-row--editing{ display: block; }
.equipment-row--editing .inline-add-form{ margin-top: 0; padding-top: 0; border-top: none; }

/* -- RV4 W2: #thread-parts (job setup -- cadence + progress bars). Row
   shape mirrors .equipment-row/.exp-row above; each row's own progress
   reuses the SAME .hbar-block/.hbar-words/.hbar/.hbar-fill trio the Money
   card's hours-budget guard already defines further below -- no second
   bar visual invented for this. -- */
.part-list{ display: flex; flex-direction: column; }
.part-row{ padding: 12px 0; border-top: 1px solid var(--border); }
.part-row:first-child{ border-top: none; padding-top: 0; }
.part-row-top{ display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
.part-label{ font-size: 14px; font-weight: 600; color: var(--text-primary); }
.part-legacy-note{ font-size: 12.5px; color: var(--text-faint); margin: 6px 0 0; }

/* ==========================================================================
   ---------- pkg 5 ----------
   Job thread money surface: quote block, change orders, expenses, job
   costing, billing stub. Owner-only everywhere (server decides; this CSS
   just styles what the template already gates by role). Tokens only,
   16/24 rhythm, money figures right-aligned within their row, wraps at
   320px. Zero new colors -- costing/profit stays calm, no red anywhere.
   ========================================================================== */

/* -- shared: inline add-line forms (quote / change orders / expenses) -- */
.inline-add-form{
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
/* qualified with .field-row to match (not lose to) the existing
   ".field-row .field{ flex:1; min-width:120px; }" rule's specificity --
   a bare ".field--qty{...}" would be silently overridden by it. */
.field-row .field--grow{ flex: 2; }
.field-row .field--qty{ flex: 0 1 96px; min-width: 80px; max-width: 100px; }
.field-row .field--price{ flex: 0 1 130px; min-width: 100px; max-width: 140px; }
.inline-add-btn{ align-self: flex-start; }

/* -- shared: quiet per-line delete (quote lines + expenses ONLY -- change
   orders are append-only and never get one) -- */
.line-delete-form{ flex-shrink: 0; }
.line-delete-btn{
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-faint);
  cursor: pointer;
}
.line-delete-btn:hover{ color: var(--text-secondary); }

/* -- R4-W2 BUILD 2 "money line editing": the quiet Edit link beside
   Delete (quote lines + bill lines), and the edit-mode row it reveals --
   .quote-line--edit/.bill-line--edit swap the read row's flex layout for
   a plain block so the revealed inline-add-form lays out full-width
   instead of shrink-to-fit as one centered flex child. -- */
.line-edit-link{
  flex-shrink: 0;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--cobalt);
}
.quote-line--edit,
.bill-line--edit{ display: block; }
.quote-line--edit .inline-add-form,
.bill-line--edit .inline-add-form{ margin-top: 0; padding-top: 0; border-top: none; }

/* -- #thread-quote -- */
.quote-status{ margin-bottom: 12px; }
.quote-list{ display: flex; flex-direction: column; }
.quote-line{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.quote-line:first-child{ border-top: none; padding-top: 0; }
.quote-line-info{ flex: 1; min-width: 160px; display: flex; flex-direction: column; gap: 2px; }
.quote-line-desc{ font-size: 14px; color: var(--text-primary); overflow-wrap: anywhere; }
.quote-line-qty{ font-size: 12.5px; color: var(--text-secondary); }
.quote-line-total{ font-size: 14px; font-weight: 600; color: var(--text-primary); text-align: right; margin-left: auto; }
.quote-total{
  font-size: 14px;
  font-weight: 600;
  color: var(--slate-deep);
  margin: 12px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

/* -- #thread-changeorders (append-only -- no delete control anywhere) -- */
.co-list{ display: flex; flex-direction: column; }
.co-row{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.co-row:first-child{ border-top: none; padding-top: 0; }
.co-row-info{ flex: 1; min-width: 160px; display: flex; flex-direction: column; gap: 2px; }
.co-note{ font-size: 14px; color: var(--text-primary); overflow-wrap: anywhere; }
.co-added{ font-size: 12.5px; color: var(--text-secondary); }
.co-amount{ font-size: 14px; font-weight: 600; color: var(--text-primary); text-align: right; margin-left: auto; }

/* -- #thread-expenses -- */
.exp-list{ display: flex; flex-direction: column; }
.exp-row{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.exp-row:first-child{ border-top: none; padding-top: 0; }
.exp-row-info{ flex: 1; min-width: 160px; display: flex; flex-direction: column; gap: 2px; }
.exp-label{ font-size: 14px; color: var(--text-primary); overflow-wrap: anywhere; }
.exp-date{ font-size: 12.5px; color: var(--text-secondary); }
.exp-amount{ font-size: 14px; font-weight: 600; color: var(--text-primary); text-align: right; margin-left: auto; }

/* -- #thread-costing (the "Money" card): calm right-aligned money lines,
   no red anywhere -- negative profit shows the true minus sign from
   fmt_money, never a color change. X1's hours-budget guard below is the
   ONE deliberate, spec-pinned exception to "no red" -- the whole point
   of the guard is a visible warning once a fixed-bid job blows its
   hours, so .hbar-fill--over/.hbar-over-words/.costing-profit--over do
   use --danger; every other line on this card stays calm. -- */
.costing-list{ display: flex; flex-direction: column; gap: 12px; }
.costing-group{ display: flex; flex-direction: column; }
.costing-row{ font-size: 14px; font-weight: 600; color: var(--text-primary); text-align: right; margin: 0; }
.costing-row-detail{
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-secondary);
  text-align: right;
  margin: 2px 0 0;
}
.costing-row-detail--workers{ text-align: left; }
.costing-group--profit{ margin-top: 4px; padding-top: 12px; border-top: 1px solid var(--border); }
.costing-profit{ font-size: 17px; font-weight: 700; color: var(--slate-deep); }
.costing-profit--ok{ color: var(--mist-text); }
.costing-profit--over{ color: var(--danger); }

/* -- X1: the hours-budget bar block, ported from mockups/mock.css's own
   .hbar/.hbar-words (same token names, same shape) -- left-aligned
   (unlike every other .costing-row above) since it's a two-ended words
   row over a bar, not a single right-aligned money line. -- */
.hbar-block{ padding: 6px 0 2px; }
.hbar-words{
  display: flex;
  justify-content: space-between;
  gap: 10px;
  font-size: 12.5px;
  font-weight: 500;
  color: var(--text-secondary);
  margin: 0 0 6px;
}
.hbar-words b{ color: var(--text-primary); }
.hbar-over-words{ color: var(--danger); font-weight: 700; }
.hbar{
  height: 10px;
  border-radius: 999px;
  background: var(--app-bg);
  overflow: hidden;
}
.hbar-fill{ display: block; height: 100%; background: var(--cobalt); }
.hbar-fill--over{ background: var(--danger); }
.hbar-caution{ font-size: 12.5px; color: var(--text-faint); text-align: left; margin: 6px 0 0; }

/* -- #thread-billing: stub state word + quiet note pointing at pkg 9 -- */
.billing-state{ font-size: 15px; font-weight: 600; color: var(--text-primary); margin: 0 0 6px; }
.billing-note{ font-size: 12.5px; color: var(--text-secondary); margin: 0; }

/* ==========================================================================
   ---------- pkg 6 ----------
   O1 owner dashboard: flow strip (4 honest counts, zero-$), crew-today
   columns (mirrors C1's stage vocabulary), needs-attention list, money
   tile (the ONE money number on this screen). Tokens only, 16/24 rhythm,
   checked at 320px. Desktop = strip row -> two-column band (crew wide
   left, attention + money tile stacked narrow right) -> nothing else;
   at 375px the band collapses to one column in source order (crew ->
   attention -> money tile last), so the mobile visual order falls out of
   the DOM order below, not extra CSS.
   ========================================================================== */

/* W4 item 5 (today-v9's own .tv6-daybar, binding spec) -- the owner's
   Today page never said what day it was showing; a quiet caps label
   above the flow strip, same idiom as .drawer-section-head elsewhere in
   this app (uppercase, letter-spaced, --text-faint), never shouting. */
.tdy-daybar{
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
  margin: 0 0 14px;
}

/* -- flow strip: 2x2 at mobile widths, 4-across from 768px up -- */
.flow-strip{
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 8px;
  margin-bottom: 24px;
}
@media (min-width: 768px){
  .flow-strip{ grid-template-columns: repeat(4, 1fr); padding: 22px 8px; }
}
.flow-cell{
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-align: center;
  padding: 18px 12px;
  border-radius: var(--radius-surface);
}
.flow-cell:hover{ background: var(--app-bg); }
.flow-num{ font-size: 26px; font-weight: 600; color: var(--slate-deep); line-height: 1; }
.flow-num--accent{ color: var(--cobalt); }
.flow-word{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
/* empty cell word is .empty-inline now, templates/_macros.html's
   empty_inline() -- matches this rule's own values exactly, no override
   needed. */

/* -- two-column band: day panel wide/left, attention + money tile narrow/
   right. Mobile-first is a single column in DOM order (day, attention,
   money) -- the grid-template-areas below only apply >=768px. Area name
   "crew" is unchanged from pkg 6 (grid-template-areas are internal to
   this rule, renaming one is a bigger diff than it's worth) even though
   the panel it now holds is RV1's day/tomorrow band, not the old
   role='crew'-only crew_columns. -- */
.o1-band{
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
  align-items: start;
}
@media (min-width: 768px){
  .o1-band{
    grid-template-columns: 1fr 340px;
    grid-template-areas:
      "crew attn"
      "crew money";
    gap: 24px;
  }
  .day-panel{ grid-area: crew; }
  .attention-panel{ grid-area: attn; }
  .money-tile{ grid-area: money; }
}
.day-panel, .attention-panel{ min-width: 0; }

/* ==========================================================================
   RV1: Today v4 -- day groups (.tdy-group, one per crew/you/unassigned,
   or the single solo "Your day" group) + the shared .tdy-card row both
   `day` and the `tomorrow` fold render (home_owner.html's own tdy_card()
   macro -- one markup, two callers, so a day card and a tomorrow card
   can never drift). Ported from mockups/today-v2.css's .tv2-* recipe,
   translated to this app's own tokens (var(--cobalt)/var(--amber-text)/
   var(--mist-text) etc, var(--radius-control)/var(--radius-surface))
   rather than the mock's raw hexes; --shadow-card is NOT one of those
   tokens (referenced at .month/.dboard but never defined in :root -- a
   pre-existing gap outside this slice, see ctx-home.md) so shadows below
   are literal rgba, matching mockups/mock.css's own --shadow-card VALUE
   (0 1px 2px rgba(32,40,54,.06)) rather than the broken token. Checked
   at 320px: the thumb hides under 480px and the group-head/tomorrow-
   summary rows wrap instead of forcing horizontal scroll.
   ========================================================================== */
.tdy-group{ margin-bottom: 20px; }
.tdy-group-head{
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px 10px;
  margin: 0 0 8px 2px;
}
.tdy-group-head h2{ font-size: 15px; font-weight: 700; color: var(--text-primary); margin: 0; }
.tdy-members{ font-size: 12px; color: var(--text-faint); }
.tdy-load{ margin-left: auto; font-size: 12px; color: var(--text-faint); }

.tdy-editlink{
  font-size: 12.5px;
  font-weight: 600;
  color: var(--cobalt);
  text-decoration: none;
  border: 1px solid var(--border);
  background: var(--white);
  border-radius: 999px;
  padding: 3px 12px;
}
.tdy-editlink:hover{ border-color: var(--cobalt); }

/* RV3 W3 (contract item 4, LAW O): .tdy-card is now a plain block wrapper
   -- its own border/shadow/left-edge/padding stay exactly what they always
   were, it just no longer IS the <a> (a card can carry log lines below the
   link now, and a click target inside a click target is invalid HTML).
   .tdy-card-link is the old flex-row layout, unchanged, just renamed. */
.tdy-card{
  background: var(--white);
  border: 1px solid var(--border);
  border-left: 4px solid var(--border);
  border-radius: var(--radius-surface);
  box-shadow: 0 1px 2px rgba(32,40,54,.06);
  padding: 12px 16px 12px 14px;
  margin-bottom: 10px;
  transition: box-shadow .12s;
}
.tdy-card:hover{ box-shadow: 0 3px 10px rgba(32,40,50,.12); }
.tdy-card--scheduled{ border-left-color: var(--cobalt); }
.tdy-card--done{ border-left-color: var(--mist-text); opacity: .82; }

.tdy-card-link{
  display: flex;
  align-items: center;
  gap: 14px;
  text-decoration: none;
  color: inherit;
}

.tdy-thumb{
  width: 72px;
  height: 56px;
  flex: none;
  border-radius: var(--radius-control);
  background-color: var(--app-bg);
  background-size: cover;
  background-position: center;
  position: relative;
  overflow: hidden;
}
.tdy-thumb--none::after{
  content: "no photos";
  position: absolute;
  left: 5px;
  bottom: 4px;
  font-size: 9.5px;
  color: var(--text-faint);
}

.tdy-main{ min-width: 0; flex: 1; }
.tdy-title{
  display: block;
  font-size: 15px;
  font-weight: 700;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tdy-num{ font-size: 12px; font-weight: 600; color: var(--text-faint); margin-left: 7px; }
/* .tdy-sub/.tdy-people need display:block for the same reason .tdy-title
   just above does: they're plain <span>s, and a bare inline span never
   starts its own line next to a sibling inline span (Jinja's own template
   whitespace between them collapses to nothing meaningful, and the JS
   twin's cardHtml() builder inserts none at all) -- without this a day/
   tomorrow card with both a sub line AND a people line runs them
   together on one row instead of stacking. */
.tdy-sub{ display: block; font-size: 13.5px; color: var(--text-secondary); margin-top: 1px; overflow-wrap: anywhere; }
.tdy-people{ display: block; font-size: 12px; color: var(--text-faint); margin-top: 2px; overflow-wrap: anywhere; }

.tdy-right{ display: flex; flex-direction: column; align-items: flex-end; gap: 6px; flex: none; }
.tdy-time{ font-size: 13px; font-weight: 700; color: var(--text-primary); white-space: nowrap; }
.tdy-card--done .tdy-time{ text-decoration: line-through; color: var(--text-faint); }
/* W4 item 7 (today-v9's own .tv2-card--done rule strikes BOTH title and
   time -- this app previously only struck .tdy-time). */
.tdy-card--done .tdy-title{ text-decoration: line-through; color: var(--text-faint); }
.tdy-chev{ color: var(--text-faint); font-size: 16px; margin-left: 4px; }

/* RV3 W3 -- the job's measure headline bar (Amendment A #2, "N of TARGET
   UNIT finished") on a Today crew card, only rendered when jobs.
   measure_target is set (db.job_measure_progress, called per-job in
   app.py's own _enrich_o1). */
.tdy-measure{ display: block; margin-top: 8px; max-width: 400px; }
.tdy-measure-num{ font-size: 14.5px; font-weight: 800; color: var(--text-primary); letter-spacing: -.01em; }
.tdy-measure-num b{ font-size: 16px; }
.tdy-bar-track{ height: 6px; border-radius: 999px; background: var(--app-bg); border: 1px solid var(--border); overflow: hidden; margin-top: 5px; }
.tdy-bar-fill{ height: 100%; background: var(--cobalt); border-radius: 999px; }

/* RV3 W3 -- timestamped, named log lines under a Today crew card
   (contract item 1) -- a SIBLING of .tdy-card-link (never nested inside
   it, see that macro/JS twin's own header comments), each row its own
   tap target opening the owner log viewer (static/owner_logs.js). Reused
   verbatim (same classes) on office_approvals.html's queue rows and
   office_forgot_done.html's honesty-panel log list -- one look, one place
   each carries its own outer .approval-row/.tdy-logrow wrapper. */
.tdy-logs{ margin-top: 7px; padding-top: 6px; border-top: 1px dashed var(--border); display: flex; flex-direction: column; }
.tdy-logrow{ display: flex; align-items: baseline; gap: 8px; font-size: 12px; color: var(--text-secondary); padding: 5px 4px; margin: 0 -4px; flex-wrap: wrap; border-radius: 6px; cursor: pointer; }
.tdy-logrow:hover, .tdy-logrow:focus-visible{ background: var(--app-bg); outline: none; }
.tdy-logrow time{ color: var(--text-primary); font-weight: 700; flex: none; }
.tdy-log-who{ color: var(--text-primary); font-weight: 700; flex: none; }
.tdy-log-who::before{ content: "— "; font-weight: 400; color: var(--text-faint); }
.tdy-log-txt{ flex: 1 1 auto; min-width: 140px; }
.tdy-log-ph{ color: var(--text-faint); flex: none; white-space: nowrap; font-size: 11.5px; }
.tdy-logrow .chip{ flex: none; margin-left: auto; }
.chip--sm{ font-size: 11px; padding: 2px 9px; }

/* W4 item 1 (today-v9 line 143, binding spec: .tv6-daylog) -- a day_done
   ("Day log") row tints so the owner can tell which row closed the day
   at a glance, never just by reading the words. */
.tdy-daylog{ background: var(--app-bg); border-radius: 7px; padding: 6px 9px; margin-top: 4px; }
.tdy-daylog .tdy-log-txt b{ color: var(--text-primary); }

/* RV3 W3 -- the approval queue (/office/approvals) and the forgot-Done
   honesty panel's own log list: same .tdy-logrow row shape, one level up
   inside a plain .card (no crew-group heading above it there). */
.approval-list{ display: flex; flex-direction: column; }
.approval-row{ display: flex; align-items: baseline; gap: 8px; font-size: 13px; color: var(--text-secondary); padding: 11px 4px; border-top: 1px solid var(--app-bg); flex-wrap: wrap; cursor: pointer; border-radius: 6px; }
.approval-row:first-child{ border-top: 0; }
.approval-row:hover, .approval-row:focus-visible{ background: var(--app-bg); outline: none; }
.approval-row time{ color: var(--text-primary); font-weight: 700; flex: none; }
.approval-row-job{ color: var(--text-faint); font-weight: 400; }
.approval-row .chip{ flex: none; margin-left: auto; }

/* RV3 W3 -- the tappable owner log viewer overlay (contract item 1),
   built lazily by static/owner_logs.js and reused on every surface that
   opens one (Today cards, the approval queue, the forgot-Done panel).
   Same slide-over-from-the-right idiom the RV3 mocks establish
   (mockups/today-v8.html's own .tv6-overlay/.tv6-panel), rebuilt here
   with this app's OWN tokens -- never the mock's separate :root block. */
.log-overlay{
  position: fixed; inset: 0; background: rgba(15,23,42,.45);
  z-index: 90; display: none; justify-content: flex-end;
}
.log-overlay.is-open{ display: flex; }
.log-panel{
  width: 440px; max-width: 92vw; background: var(--white); height: 100%;
  overflow-y: auto; padding: 26px 24px 30px; box-shadow: -10px 0 34px rgba(15,23,42,.28);
  position: relative;
}
.log-panel-close{
  position: absolute; top: 18px; right: 18px; background: var(--app-bg);
  border: 1px solid var(--border); border-radius: 999px; padding: 6px 14px;
  font: inherit; font-size: 13px; font-weight: 600; color: var(--text-primary); cursor: pointer;
}
/* Amendment A #4's own order (header + Close on top, note/who/when/tag
   next, photos BELOW) -- owner_logs.js's renderEntry() builds the markup
   in exactly this order; nothing here re-arranges it. */
.log-panel-tag{
  display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; border-radius: 999px; padding: 4px 10px; margin: 0 46px 12px 0;
}
.log-panel-body h2{ font-size: 19px; margin: 0 0 2px; }
.log-panel-sub{ font-size: 13px; color: var(--text-secondary); margin: 0 0 14px; }
.log-panel-amount{ font-size: 15px; font-weight: 700; color: var(--cobalt); background: var(--cobalt-tint); border-radius: 8px; padding: 10px 13px; margin: 0 0 14px; }
.log-panel-meta{ margin: 0 0 10px; }
.log-note{ font-size: 13.5px; color: var(--text-secondary); background: var(--app-bg); border-radius: 8px; padding: 11px 13px; margin: 0 0 16px; white-space: pre-wrap; overflow-wrap: anywhere; }
.log-photo-grid{ display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 0 0 16px; }
.log-photo{ display: block; position: relative; border-radius: 7px; overflow: hidden; aspect-ratio: 4/3; }
.log-photo img{ width: 100%; height: 100%; object-fit: cover; display: block; }
.log-photo-tag{ position: absolute; left: 5px; bottom: 5px; font-size: 10px; background: rgba(35,44,54,.75); color: var(--white); border-radius: 4px; padding: 1px 6px; }
.log-panel-hint{ font-size: 12.5px; color: var(--text-faint); margin: 0 0 16px; line-height: 1.5; }
/* W4 item 2 (today-v9's own .tv6-preview, binding spec) -- the approve
   panel's before/after preview: "<b>50 yd</b> finished · 600 → <b>650</b>
   of 2,000 yd · 30% → 33%". Cobalt, same family as .log-panel-amount
   above but its own class since the two can appear together (amount =
   what THIS log said; preview = what approving it will DO). */
.log-panel-preview{ font-size: 13.5px; font-weight: 700; color: var(--cobalt); background: var(--cobalt-tint); border-radius: 8px; padding: 11px 13px; margin: 0 0 14px; }
.log-panel-preview b{ font-size: 14.5px; }
/* W4 item 4's own "ALLOW but WARN, never silently clamp" law -- shown
   once, right after a live Approve/Fix amount pushes a part over its own
   target (e.g. "That puts Straight run at 108%."). Amber, the SAME
   .form-error/.flash-note--warn color pairing this app already uses for
   "worth a second look" (never --danger red, which this app reserves for
   a harder stop) -- a distinct class since this is a success-with-a-
   caveat, not a failed action. */
.log-panel-warn{ font-size: 13px; font-weight: 600; color: var(--amber-text); background: var(--amber-bg); border-radius: 8px; padding: 10px 13px; margin: 0 0 14px; }
/* W4 item 3 -- "Locked Aug 8 · 2:14 PM · Ruben" (who approved a locked
   log, and when), directly under the note/photos, right where today-
   v9's own .tv7-lockmeta sits. */
.log-panel-lockmeta{ font-size: 12px; color: var(--text-faint); margin: 0 0 16px; }
.log-panel-acts{ display: flex; gap: 10px; }
.log-panel-acts .btn{ flex: 1; text-align: center; }
/* Same [hidden]-losing-to-an-unconditional-display defect as .log-fix-
   form below -- owner_logs.js sets acts.hidden=true while a fix form is
   open (both fixBtn and fixAmountBtn click handlers), which silently did
   nothing without this. */
.log-panel-acts[hidden]{ display: none; }
.log-fix-form, .log-fix-amount-form{ margin-top: 4px; display: flex; flex-direction: column; gap: 12px; }
/* W4 item 4 -- REAL BUG FOUND LIVE while testing "Fix amount": an
   unconditional `display: flex` above beats the UA stylesheet's own
   [hidden]{display:none} rule (author styles always win over UA ones,
   specificity aside -- SAME defect class this file's own .job-new-
   customer[hidden]/.mobile-search-panel[hidden]/.sheet-overlay[hidden]
   rules already exist to fix, just never applied here). Verified live:
   owner_logs.js correctly set hidden="" on both forms, yet each still
   rendered, permanently, right under its own still-visible Approve/Fix
   button row. This pre-existing gap on .log-fix-form (never previously
   noticed -- Fix, then approve's own form) is fixed here too, alongside
   the new .log-fix-amount-form, since both share the exact same rule
   and the exact same fix. */
.log-fix-form[hidden], .log-fix-amount-form[hidden]{ display: none; }

/* Approve, the one green action in the app that isn't Done/Send -- same
   --mist-text green crew_today.js's own .sheet-send--green already uses
   for the crew's Done button (Amendment A #6's "green, prominent"),
   applied here to a plain .btn so it drops into the existing .btn/.btn--
   primary/.btn--quiet family instead of inventing a parallel one. */
.btn--approve{ background: var(--mist-text); color: var(--white); border-color: transparent; }
.btn--approve:hover{ background: #186b40; }
.btn--approve:disabled{ opacity: .38; cursor: default; }
.btn--approve:disabled:hover{ background: var(--mist-text); }

/* -- tomorrow fold: same .day-collapsed >/▸ ▾ triangle idiom the Day-grid
   view already uses (app.css's own .day-collapsed, schedule_owner.html),
   holding the same .tdy-card rows as the day list above. -- */
details.tdy-tmrw{
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  margin-top: 4px;
}
details.tdy-tmrw summary{
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 10px;
  padding: 12px 16px;
  font-size: 13.5px;
  color: var(--text-secondary);
}
details.tdy-tmrw summary::-webkit-details-marker{ display: none; }
details.tdy-tmrw summary::before{ content: "▸"; color: var(--text-faint); font-size: 12px; }
details.tdy-tmrw[open] summary::before{ content: "▾"; }
details.tdy-tmrw summary b{ color: var(--text-primary); }
.tdy-tmrw-summary-line{ flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
details.tdy-tmrw summary .tdy-editlink{ margin-left: auto; }
.tdy-tmrw-cards{ padding: 6px 12px 4px; }
.tdy-tmrw-cards .tdy-card{ margin-bottom: 8px; }

@media (max-width: 480px){
  .tdy-thumb{ display: none; }
  .tdy-card{ gap: 10px; padding: 10px 12px; }
  .tdy-tmrw-summary-line{ flex-basis: 100%; }
}

/* -- needs attention: renders only when there's something to say (6.0) -- */
.attention-list{ display: flex; flex-direction: column; }
.attention-item{
  display: flex;
  gap: 12px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}
.attention-item:first-child{ border-top: none; padding-top: 0; }
.attention-item:hover p{ color: var(--cobalt); }
.attention-dot{
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--amber-text);
  margin-top: 6px;
  flex-shrink: 0;
}
.attention-item p{ margin: 0; font-size: 14px; line-height: 1.5; color: var(--text-primary); font-weight: 500; overflow-wrap: anywhere; }

/* -- money tile: the ONE money number on O1. Dark slate, numerals pinned
   to the spec's money accent, everything else white/quiet. R0-S2: real
   receivable money now, so the tile is a link (/office/billing) -- block
   display + no underline since it's an <a>, same quiet-link idiom as
   .flow-cell/.attention-item elsewhere on this page. -- */
.money-tile{
  display: block;
  background: var(--slate-deep);
  border-radius: var(--radius-surface);
  padding: 20px 22px;
  color: var(--white);
  text-decoration: none;
}
.money-tile:hover{ background: var(--slate); }
.money-tile-label{ font-size: 12.5px; font-weight: 600; color: rgba(255,255,255,0.6); margin-bottom: 8px; }
.money-tile-amount{ font-size: 20px; font-weight: 600; line-height: 1.35; margin: 0; }
.money-tile-amount b{ color: var(--money-numeral); font-weight: 700; }
/* RV1: the overdue clause rides the SAME amount paragraph (never a
   second row) -- var(--money-overdue), not var(--danger), since this
   sits on --slate-deep and --danger's own on-light pairing (chip--
   overdue etc) doesn't read there. */
.tdy-overdue{ color: var(--money-overdue); font-weight: 700; }
/* empty amount word is .empty-inline now (.money-tile .empty-inline
   override above supplies the light-on-dark size/color). */

/* ==========================================================================
   ---------- pkg 7 ----------
   Customers (B-CUST) + the office drawers (B-OFFICE's office_index/
   office_leads/office_lead/office_applicants, B-ADMIN's office_money/
   office_team/office_settings — neither B owns any CSS this package, so
   every class below covers BOTH builders' landed templates plus B-CUST's
   own customers_list/customer_profile). Tokens only, 16/24 rhythm, no new
   colors, no shadows beyond the existing avatar-menu/more-menu recipe,
   checked at 320px. The money report is the one exception to "the page
   never h-scrolls" — it scrolls in its own container (below).
   ========================================================================== */

/* -- bare stacked .card sections (office_settings/office_team use plain
   sibling .card divs directly in {% block content %}, not .thread-section
   wrappers) need the same between-section rhythm .thread-section gives
   job_thread.html. Scoped to ".shell-content >" (direct children of the
   page content well only) so it never touches: a .thread-section's single
   nested .card (not a direct shell-content child); more.html's three
   .card siblings, which already get their gap from .more-page's own flex
   gap (this rule would double it if unscoped); or home_owner.html's two
   .card's, which sit in separate grid-area wrapper divs, not directly
   adjacent. -- */
.shell-content > .card + .card{ margin-top: 20px; }

/* -- filter chips: the pkg-7 templates (office_leads/office_applicants/
   customers_list) spell the active state "--active" (BEM) rather than
   the pkg-4 "is-active" the class was first styled with — both spellings
   get the same look; this is additive, the original rule is untouched. -- */
.filter-chip--active{ background: var(--cobalt); border-color: var(--cobalt); color: var(--white); }

/* -- office index: seven drawer cards, pinned order -- */
.drawer-grid{ display: flex; flex-direction: column; gap: var(--space-4); }
@media (min-width: 560px){
  .drawer-grid{ display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; }
}
.drawer-card{
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 20px;
  min-width: 0;
}
a.drawer-card:hover{ border-color: var(--cobalt); }
.drawer-card-title{ font-size: 17px; font-weight: 600; color: var(--slate-deep); }
.drawer-card-desc{ font-size: 14px; color: var(--text-secondary); margin: 0; overflow-wrap: anywhere; }
.drawer-card-count{ font-size: 12.5px; font-weight: 600; color: var(--cobalt); }
.drawer-card--soon{ color: var(--text-faint); cursor: default; }
.drawer-card--soon .drawer-card-title{ color: var(--text-faint); }

/* R6: the drawer's own four labeled sections (Money/People/Growth/Setup)
   -- one quiet caps label per group, same weight as .section-head
   elsewhere in the app; a .drawer-card-note is the simplicity pass's
   "quiet setup line" for a not-yet-configured feature (e.g. Payments
   w/o Stripe) -- one small faint sentence, never a second badge, never
   full card weight. */
.drawer-section{ display: flex; flex-direction: column; gap: var(--space-3); }
.drawer-section + .drawer-section{ margin-top: var(--space-5); }
.drawer-section-head{
  font-size: 12.5px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-faint);
}
.drawer-card-note{ font-size: 12.5px; color: var(--text-faint); }

/* -- leads/applicants rows: name + sub line, optional status chip/row.
   Used as both <a> (office_leads.html) and plain <div> (office_applicants
   .html, which nests a .status-row of transition buttons inside) -- flex-
   wrap lets the nested status-row drop to its own line at narrow widths. -- */
.lead-list{ display: flex; flex-direction: column; gap: 10px; }
.lead-row{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 14px 16px;
  min-width: 0;
}
/* R4-W4 SLICE W4 BUILD 1: was `a.lead-row:hover` -- .lead-row is now
   always a <div> (office_leads.html's own row carries its own tel:/
   mailto: links beside the navigating .lead-row-link, office_applicants
   .html's was already a <div>), so the selector drops the tag qualifier
   to keep firing on both. */
.lead-row:hover{ border-color: var(--cobalt); }
.lead-row-name{ flex: 1; min-width: 140px; font-size: 15px; font-weight: 600; color: var(--slate-deep); overflow-wrap: anywhere; }
.lead-row-sub{ font-size: 12.5px; color: var(--text-secondary); overflow-wrap: anywhere; }
/* .lead-row-link -- `display:contents` unwraps the <a> from the box
   model entirely so .lead-row-name/.lead-row-sub inside it keep sitting
   as direct flex items of .lead-row, byte-identical to their old
   position as the row's own direct children (see office_leads.html). */
.lead-row-link{ display: contents; color: inherit; text-decoration: none; }
.lead-row-contact{ flex-basis: 100%; }
.lead-row .status-row{ flex-basis: 100%; margin-top: 2px; }

/* -- shared one-click status/transition row (lead page's 3-button status
   row, applicants' transition buttons) -- reuses pkg-4's .status-btn/
   .status-btn--current look verbatim, just needed its own flex wrapper
   since it isn't always inside a .status-rail-bearing header card. -- */
.status-row{ display: flex; flex-wrap: wrap; gap: 6px; }

/* -- customers list: same row recipe as .lead-row/.job-row, own class
   per the contract (name, town, honest jobs-count word). -- */
.customer-list{ display: flex; flex-direction: column; gap: 10px; }
.customer-row{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 14px 16px;
  min-width: 0;
}
.customer-row:hover{ border-color: var(--cobalt); }
/* -- R4-W4 SLICE W4 BUILD 4: rows at scale -- a second line (street ·
   phone) under the name, right-aligned "last job <date>" in place of the
   old bare "N jobs" (audit MISSING "Rows show only 'Town · N jobs' ...
   answers no real question"). .customer-row-main stacks name+sub
   (min-width:0/flex:1, same recipe as .job-row-main); the old
   .customer-row-sub class is repurposed for the NEW second line (was the
   right-side text) -- .customer-row-meta is the new right-side slot. -- */
.customer-row-main{ min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
.customer-row-name{ font-size: 15px; font-weight: 600; color: var(--slate-deep); overflow-wrap: anywhere; }
.customer-row-sub{ font-size: 12.5px; color: var(--text-secondary); overflow-wrap: anywhere; }
.customer-row-meta{ font-size: 12.5px; color: var(--text-secondary); flex-shrink: 0; white-space: nowrap; }

/* -- R4-W4 SLICE W4 BUILD 4: A-Z jump strip, past 50 rows only (audit
   CONFUSING "no A-Z jump ... at 505 customers"). All 26 letters always
   render (steady width, no layout shift) -- present ones are real anchor
   links, absent ones are inert faint text. -- */
.customer-jump-strip{ display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 12px; }
.customer-jump-letter{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--cobalt);
  border-radius: 999px;
}
.customer-jump-letter:hover{ background: var(--cobalt-tint); }
.customer-jump-letter--empty{ color: var(--text-faint); }
.customer-list-count{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); margin: -6px 0 14px; }

/* -- R4-W2 BUILD 4 "customer edit + archive": archived rows carry a
   Restore control, so they're a <div> (same recipe as .customer-row)
   wrapping a nested link + the restore form, not a bare <a> — a <form>
   can't nest inside one. -- */
.customer-row--archived{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 14px 16px;
  min-width: 0;
}
.customer-row-link{ display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1; }

/* -- lead page contact block + detail quote -- same recipe as job_thread
   .html's .thread-header/.thread-title/.thread-instructions. -- */
.lead-contact{ margin-bottom: 20px; }
.lead-contact-top{ display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
.lead-contact-name{ font-size: 20px; font-weight: 600; color: var(--slate-deep); line-height: 1.3; overflow-wrap: anywhere; }
.lead-contact-line{ font-size: 14px; color: var(--text-secondary); margin: 0 0 4px; overflow-wrap: anywhere; }
.lead-detail{
  font-size: 14px;
  font-style: normal;
  color: var(--text-secondary);
  background: var(--app-bg);
  border-radius: var(--radius-control);
  padding: 12px 14px;
  margin: 0;
  overflow-wrap: anywhere;
}
.convert-note{ font-size: 12.5px; color: var(--text-secondary); margin: 10px 0 0; }

/* -- customers profile: contact card name/inline-edit form. -- */
.customer-contact{ margin-bottom: 20px; }
.customer-contact-name{ font-size: 20px; font-weight: 600; color: var(--slate-deep); line-height: 1.3; margin: 0 0 6px; overflow-wrap: anywhere; }

/* -- R4-W2 BUILD 4 "customer edit + archive": the read-view's tel:/
   mailto: lines, and the quiet "Archived" word above them. -- */
.customer-contact-lines{ display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 4px 0 8px; }
.customer-contact-line{ font-size: 14px; color: var(--cobalt); font-weight: 500; }
.customer-archived-word{ font-size: 12.5px; color: var(--text-faint); font-weight: 600; margin: 0 0 6px; }

/* -- customers profile: custom fields (cf- prefix so it never collides
   with the .field/.field-row form-layout classes every add form uses). -- */
.cf-list{ display: flex; flex-direction: column; }
.cf-row{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.cf-row:first-child{ border-top: none; padding-top: 0; }
.cf-row-info{ flex: 1; min-width: 160px; display: flex; flex-direction: column; gap: 2px; }
.cf-label{ font-size: 12.5px; font-weight: 600; color: var(--text-faint); }
.cf-value{ font-size: 14px; color: var(--text-primary); overflow-wrap: anywhere; }

/* -- customers profile: balance line (Law 6 — equals the billed rows
   shown in #customer-jobs), calm and prominent, no color-only signal. -- */
.balance-line{ font-size: 15px; font-weight: 600; color: var(--slate-deep); margin: 0; }

/* -- note thread (lead page) -- */
.note-list{ display: flex; flex-direction: column; margin-bottom: 16px; }
.note-row{ padding: 10px 0; border-top: 1px solid var(--border); }
.note-row:first-child{ border-top: none; padding-top: 0; }
.note-row-head{ display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 2px; }
.note-author{ font-size: 12.5px; font-weight: 600; color: var(--text-primary); }
.note-date{ font-size: 12.5px; font-weight: 500; color: var(--text-secondary); }
.note-text{ font-size: 14px; color: var(--text-secondary); margin: 0; overflow-wrap: anywhere; }

/* -- money report: the ONE surface that scrolls in its own container —
   the page itself never h-scrolls, even at 320px. -- */
.report-table{
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
}
.report-table table{ width: 100%; min-width: 640px; border-collapse: collapse; }
.report-table thead th{
  text-align: left;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-faint);
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.report-table td{
  padding: 12px 14px;
  font-size: 14px;
  color: var(--text-primary);
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
.report-table tbody tr:last-child td{ border-bottom: none; }
.report-num{ text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.report-job-number{ display: block; font-size: 11px; font-weight: 600; color: var(--text-secondary); }
.report-job-title{ display: block; font-size: 14px; font-weight: 600; color: var(--slate-deep); overflow-wrap: anywhere; }
.report-total td{ font-weight: 700; color: var(--slate-deep); background: var(--app-bg); }

/* -- team drawer: people list -- */
.person-list{ display: flex; flex-direction: column; }
.person-row{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}
.person-row:first-child{ border-top: none; padding-top: 0; }
.person-avatar{ flex-shrink: 0; }
.person-info{ flex: 1; min-width: 140px; display: flex; flex-direction: column; gap: 1px; }
.person-name{ font-size: 15px; font-weight: 600; color: var(--text-primary); overflow-wrap: anywhere; }
.person-email{ font-size: 12.5px; color: var(--text-faint); overflow-wrap: anywhere; }
.person-role{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.person-action-form{ flex-shrink: 0; }

/* -- team drawer: pending invites + reset links list. R7 SLICE replaced
   the old one-time invite reveal (.invite-link-reveal/.invite-link-value,
   rendered exactly once on the creating request) with a PERSISTENT list
   -- every row shows its own live link, always, same fix for resets
   (which never had ANY UI surface before this). -- */
.invite-list{ display: flex; flex-direction: column; }
.invite-row{
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 10px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
  font-size: 14px;
}
.invite-row:first-child{ border-top: none; padding-top: 0; }
.invite-email{ font-weight: 600; color: var(--text-primary); overflow-wrap: anywhere; }
.invite-role{ color: var(--text-secondary); }
.invite-date{ color: var(--text-secondary); margin-left: auto; }
/* -- R7 SLICE: the full row variant (invite AND reset rows) -- header
   line (name/role/date), then the link itself, then its actions, each
   stacked instead of squeezed onto .invite-row's one baseline-aligned
   line. .link-copy-input is the readonly-link twin of .quote-link-value/
   .billing-link-value/.progress-link-value (app.css's own R5-X3 comment
   already names those three "one recipe, not a third copy per section"
   -- this is that recipe, generalized, rather than a fourth). -- */
.invite-row--full{ flex-direction: column; align-items: stretch; }
.invite-row-head{ display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; }
.link-copy-input{
  font-size: 12.5px;
  font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
  color: var(--text-secondary);
  background: var(--app-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 10px 12px;
  overflow-wrap: anywhere;
}
.invite-row-actions{ display: flex; gap: 10px; margin-top: 2px; }

/* -- R3-V1: Crews card (Team page) -- same person-row recipe as
   .person-row above, own class family so a future crew-specific tweak
   never touches the people list. -- */
.crew-list{ display: flex; flex-direction: column; margin-bottom: 16px; }
.crew-row{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}
.crew-row:first-child{ border-top: none; padding-top: 0; }
.crew-rename-form{ display: flex; align-items: center; gap: 6px; }
.crew-name-input{
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 6px 8px;
  width: 120px;
}
.crew-inline-btn{ font-size: 12.5px; padding: 6px 10px; }
.crew-color-select, .crew-assign-select{
  font-size: 12.5px;
  color: var(--text-secondary);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 6px 8px;
  background: var(--white);
}
.crew-members{ flex: 1; min-width: 140px; font-size: 13px; color: var(--text-secondary); overflow-wrap: anywhere; }
.crew-assign-list{ display: flex; flex-direction: column; }
.crew-assign-row{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.crew-assign-row:first-child{ border-top: none; padding-top: 0; }
.crew-assign-name{ flex: 1; min-width: 140px; font-size: 14px; color: var(--text-primary); }
.crew-assign-role{ font-size: 12.5px; font-weight: 400; color: var(--text-faint); }

/* -- X1: Billing rates card (Team page) -- one .inline-add-form-shaped
   row per active worker (own border-top divider, own Save), a plain
   summary line up top so the owner can read the CURRENT effective rate
   without opening the inputs. -- */
.rate-list{ display: flex; flex-direction: column; }
.rate-row:first-child{ margin-top: 0; padding-top: 0; border-top: none; }
.rate-row-head{ display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.rate-row-name{ font-size: 14px; font-weight: 600; color: var(--text-primary); }
.rate-row-current{ font-size: 12.5px; color: var(--text-secondary); }

/* -- team/settings: checkbox + radio rows (approvals gate, mode picker) -- */
.check-field{ display: flex; align-items: center; gap: 10px; font-size: 14px; font-weight: 500; color: var(--text-primary); }
.check-field input{ flex-shrink: 0; width: 16px; height: 16px; accent-color: var(--cobalt); }
.mode-options{ display: flex; flex-direction: column; gap: 10px; margin-bottom: 4px; }

/* -- settings: price book rows -- */
.price-list{ display: flex; flex-direction: column; margin-bottom: 16px; }
.price-row{ display: flex; align-items: center; gap: 10px; padding: 10px 0; border-top: 1px solid var(--border); }
.price-row:first-child{ border-top: none; padding-top: 0; }
.price-label{ flex: 1; min-width: 0; font-size: 14px; color: var(--text-primary); overflow-wrap: anywhere; }
.price-amount{ font-size: 14px; font-weight: 600; color: var(--text-primary); }

/* -- settings: quiet stub notes (PDF branding / services — pkg 9 / the
   website bridge). Standalone (after the price book card) gets the
   between-section gap; the one INSIDE the Mode card sits flush instead. -- */
.settings-note{ font-size: 12.5px; color: var(--text-secondary); margin: 20px 0 0; }
.card .settings-note{ margin: 0 0 12px; }

/* ==========================================================================
   ---------- pkg 8 ----------
   Job thread (B-FLOW): the quote block's full pkg-8 upgrade -- status
   chip + viewed word, version trail, intro note, price-book picker,
   Send/Revise/Revoke actions, the once-per-render share-link reveal.
   Office Quotes drawer (new): filter chips + rows. Office index: the
   Quotes card un-stubbed (reuses .drawer-card/.drawer-card-count, no new
   rule needed there). Tokens only, 16/24 rhythm, no new colors -- the
   lock is a quiet word, never a gray wall. Checked at 320px.
   ========================================================================== */

/* -- quote block: status row gains the viewed word alongside the chip
   (additive to the pkg-5 .quote-status rule two sections up -- that
   rule's margin-bottom stands, this just makes it a wrapping flex row). -- */
.quote-status{ display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.quote-viewed{ font-size: 12.5px; font-weight: 500; color: var(--text-secondary); }

/* -- version trail ("v1 approved Aug 2 · v2 draft"), quiet, only shown
   once a revise has happened (2+ versions -- a fresh v1 doesn't repeat
   what the status chip already says). -- */
.quote-version-trail{ font-size: 12.5px; color: var(--text-secondary); margin: 4px 0 12px; }

/* -- approved lock: a calm word, never a disabled-gray wall. -- */
.quote-locked-word{ font-size: 14px; color: var(--text-secondary); margin: 0 0 4px; }
.quote-signed-by{ font-size: 12.5px; color: var(--text-secondary); margin: 0 0 12px; }

/* -- intro note: editable (draft) or a quiet read-only line (sent/
   approved) -- same quiet-card recipe as .thread-instructions. -- */
.quote-intro-form{ margin-bottom: 16px; }
.quote-intro-readonly{
  font-size: 12.5px;
  color: var(--text-secondary);
  background: var(--app-bg);
  border-radius: var(--radius-control);
  padding: 8px 10px;
  margin: 0 0 16px;
  overflow-wrap: anywhere;
}

/* -- Send / Revise / Revoke -- at most one is ever valid for the current
   status, but the row stays flex/wrap-safe regardless. -- */
.quote-actions{ display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.quote-action-form{ flex-shrink: 0; }

/* -- the share-link reveal: shows every render while the quote is sent
   (not a one-time invite-style reveal -- the owner needs to get back to
   it after the fact). Copy field is a readonly input (tap, select all,
   copy -- no JS, same no-clipboard-API idiom as the rest of the app). -- */
.quote-link-reveal{ margin-top: 16px; }
.quote-link-label{ font-size: 12.5px; font-weight: 500; color: var(--text-secondary); margin: 0 0 8px; }
.quote-link-value{
  display: block;
  width: 100%;
  font-size: 12.5px;
  font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
  color: var(--text-secondary);
  background: var(--app-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 10px 12px;
  overflow-wrap: anywhere;
}
.quote-link-actions{ display: flex; flex-wrap: wrap; gap: 10px; }
.quote-link-action{ font-size: 12.5px; font-weight: 600; color: var(--cobalt); }

/* R5-X3: .link-copy-row / .copy-btn -- shared by all three share-link
   reveals (quote/billing/progress, clipboard.js wires every
   [data-copy-target] the same way) -- one recipe, not a third copy per
   section. The readonly input keeps its own per-section class
   (.quote-link-value/.billing-link-value/.progress-link-value)
   unchanged; this row is just the new flex wrapper + button around it. */
.link-copy-row{ display: flex; align-items: stretch; gap: 8px; margin: 0 0 10px; }
.link-copy-row input{ flex: 1; min-width: 0; margin: 0; }
.copy-btn{ flex-shrink: 0; padding: 8px 14px; }
/* R5-X3: .share-btn -- the same quiet-link look as .quote-link-action/
   .billing-link-action/.progress-link-action (a plain <button>, not an
   <a>, since it drives navigator.share via JS -- clipboard.js unhides it
   only when the browser actually supports Share). */
.share-btn{
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--cobalt);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}
/* R5-X3: the Job P&L (internal) button's own caution word, same amber
   text-only idiom .quote-from-log-caution already established. */
.report-internal-caution{ font-size: 12.5px; font-weight: 600; color: var(--amber-text); margin: 8px 0 0; }

/* -- office quotes drawer: rows (job number+title, customer, total,
   status chip, viewed word, version) -- same card-row recipe as
   .job-row/.lead-row/.customer-row, own class per house pattern. -- */
.quote-row-list{ display: flex; flex-direction: column; gap: 10px; }
.quote-row-link{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 14px 16px;
  min-width: 0;
}
.quote-row-link:hover{ border-color: var(--cobalt); }
.quote-row-main{ min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
.quote-row-number{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.quote-row-title{ font-size: 15px; font-weight: 600; color: var(--slate-deep); overflow-wrap: anywhere; }
.quote-row-sub{ font-size: 12.5px; color: var(--text-secondary); overflow-wrap: anywhere; }
.quote-row-side{ display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; }
.quote-row-total{ font-size: 14px; font-weight: 600; color: var(--text-primary); }
.quote-row-viewed, .quote-row-version{ font-size: 12.5px; color: var(--text-secondary); }

/* ==========================================================================
   ---------- pkg 9 ----------
   Billing (B-BILL): job thread's #thread-billing un-stub (bill status
   chip + lines + total, Create/Send/Revoke/Mark-paid, the once-per-render
   share-link reveal, "billed <date> · paid <date>" words, the generated-
   reports list); the /office/billing drawer (To-bill queue w/ per-job
   Create bill + batch checkboxes + the once-rendered batch links list;
   bills list rows); office index's Billing card (reuses .drawer-card/
   .drawer-card-count, no new rule needed there); Settings' pay_instructions
   textarea (reuses .field/textarea, no new rule needed there); and the
   standalone print_work_summary.html page (its own .print-* family +
   the @media print block -- this on-screen owner page only; A's separate
   Job-report-PDF pipeline builds its own detached, fully self-contained
   HTML for headless Chrome and never touches this stylesheet). Bills
   deliberately mirror the quote machinery's own idiom, so these rules
   mirror the pkg-8 quote-block rules above line for line, own class
   names per house pattern (same reasoning as .quote-row-list's own
   comment: identical look, kept separate so one feature's CSS never
   drifts the other's). Tokens only, 16/24 rhythm, no new colors, no
   shadows beyond the existing card/row recipe. Checked at 320px.
   ========================================================================== */

/* -- #thread-billing: status row (own rule -- no earlier .quote-status
   -style base rule to piggyback on here, unlike the pkg-8 .quote-status
   addition). -- */
.billing-status{ display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 0 0 12px; }

/* -- #thread-billing: bill lines + total -- same recipe as .quote-list/
   .quote-line/.quote-total two sections up, own class family. -- */
.bill-list{ display: flex; flex-direction: column; }
.bill-line{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.bill-line:first-child{ border-top: none; padding-top: 0; }
.bill-line-info{ flex: 1; min-width: 160px; display: flex; flex-direction: column; gap: 2px; }
.bill-line-desc{ font-size: 14px; color: var(--text-primary); overflow-wrap: anywhere; }
.bill-line-qty{ font-size: 12.5px; color: var(--text-secondary); }
.bill-line-total{ font-size: 14px; font-weight: 600; color: var(--text-primary); text-align: right; margin-left: auto; }
.bill-total{
  font-size: 14px;
  font-weight: 600;
  color: var(--slate-deep);
  margin: 12px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

/* -- #thread-billing: "billed <date> · paid <date>" words. -- */
.billing-dates{ display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
.billing-date-word{ font-size: 12.5px; font-weight: 500; color: var(--text-secondary); }

/* -- #thread-billing: Create / Send / Mark-paid -- at most one is ever
   valid for the current state, same "one control at a time" row as
   .quote-actions. Mark-paid carries an inline paid-method select, so
   this row aligns on the button/select baseline instead of the plain
   flex-start the quote row uses. -- */
.billing-actions{ display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; margin-top: 16px; }
.billing-action-form{ flex-shrink: 0; }
.billing-paid-form{ display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px; }
.billing-paid-form .field{ min-width: 130px; }

/* -- #thread-billing: the share-link reveal -- same "shows every render
   while sent, not a one-time invite reveal" idiom as .quote-link-reveal
   two sections up, own class family. -- */
.billing-link-reveal{ margin-top: 16px; }
.billing-link-label{ font-size: 12.5px; font-weight: 500; color: var(--text-secondary); margin: 0 0 8px; }
.billing-link-value{
  display: block;
  width: 100%;
  font-size: 12.5px;
  font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
  color: var(--text-secondary);
  background: var(--app-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 10px 12px;
  overflow-wrap: anywhere;
}
.billing-link-actions{ display: flex; flex-wrap: wrap; gap: 10px; }
.billing-link-action{ font-size: 12.5px; font-weight: 600; color: var(--cobalt); }

/* -- #thread-billing: generated-reports sub-section. Reuses .doc-list/
   .doc-row/.doc-label/.doc-filename as-is (a report IS a doc row --
   same reuse call as .check-list across #thread-checklist/#thread-punch),
   just its own header word + a bottom action row for the print/PDF
   buttons. -- */
.billing-reports{ margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border); }
.billing-reports-head{ font-size: 12.5px; font-weight: 600; color: var(--text-faint); margin: 0 0 10px; }
.billing-report-actions{ display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }

/* -- /office/billing: the To-bill queue rows (own class family -- same
   look as .price-row/.person-row, one row per job). The batch checkbox
   sits outside any <form> (per-row forms can't nest inside the shared
   batch form), reaching it via the input's form="" attribute -- see the
   template's own comment. -- */
.bill-queue-list{ display: flex; flex-direction: column; }
.bill-queue-row{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}
.bill-queue-row:first-child{ border-top: none; padding-top: 0; }
.bill-queue-check{ display: flex; align-items: center; flex-shrink: 0; }
.bill-queue-check input{ width: 18px; height: 18px; accent-color: var(--cobalt); }
.bill-queue-info{ flex: 1; min-width: 140px; display: flex; flex-direction: column; gap: 2px; }
.bill-queue-number{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.bill-queue-title{ font-size: 15px; font-weight: 600; color: var(--slate-deep); overflow-wrap: anywhere; }
.bill-queue-sub{ font-size: 12.5px; color: var(--text-secondary); overflow-wrap: anywhere; }
.bill-queue-total{ font-size: 14px; font-weight: 600; color: var(--text-primary); flex-shrink: 0; }
.bill-queue-action-form, .bill-queue-action{ flex-shrink: 0; }
.bill-batch-bar{ display: flex; justify-content: flex-end; margin-top: 14px; }

/* -- /office/billing: R0-S3 review-first batch, step 1's result -- one
   .bill-review-row per draft bill, reusing #thread-billing's own
   .bill-list/.bill-line/.bill-total classes (job_thread.html) for the
   line items and .bill-queue-number/-title/-sub for the job identity
   line, so a reviewed bill's lines look identical here and on the job
   thread -- same number, Law 6. -- */
.bill-review-card{ margin-bottom: 20px; }
.bill-review-row{ padding: 16px 0; border-top: 1px solid var(--border); }
.bill-review-row:first-child{ border-top: none; padding-top: 0; }
.bill-review-head{ display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; margin-bottom: 4px; }

/* -- /office/billing: the once-rendered batch links list -- team's
   .invite-link-reveal idiom, list-shaped for N links at once. -- */
.bill-batch-links-reveal{ margin-top: 14px; }
.bill-batch-links-list{ display: flex; flex-direction: column; }
.bill-batch-link-row{ padding: 10px 0; border-top: 1px solid var(--border); }
.bill-batch-link-row:first-child{ border-top: none; padding-top: 0; }
.bill-batch-link-label{ display: block; font-size: 12.5px; font-weight: 600; color: var(--text-primary); margin: 0 0 4px; overflow-wrap: anywhere; }
.bill-batch-link-value{
  display: block;
  width: 100%;
  font-size: 12.5px;
  font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
  color: var(--text-secondary);
  background: var(--app-bg);
  border-radius: var(--radius-control);
  padding: 10px 12px;
  margin: 0;
  overflow-wrap: anywhere;
}

/* -- /office/billing: the Bills list -- same card-row recipe as
   .quote-row-list two sections up, own class family. -- */
.bill-row-list{ display: flex; flex-direction: column; gap: 10px; }
.bill-row-link{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 14px 16px;
  min-width: 0;
}
.bill-row-link:hover{ border-color: var(--cobalt); }
.bill-row-main{ min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
.bill-row-number{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.bill-row-title{ font-size: 15px; font-weight: 600; color: var(--slate-deep); overflow-wrap: anywhere; }
.bill-row-sub{ font-size: 12.5px; color: var(--text-secondary); overflow-wrap: anywhere; }
.bill-row-side{ display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; }
.bill-row-total{ font-size: 14px; font-weight: 600; color: var(--text-primary); }
.bill-row-viewed, .bill-row-paid{ font-size: 12.5px; color: var(--text-secondary); }

/* -- print_work_summary.html: standalone page, on-screen preview mirrors
   a printed sheet -- a page-bg wrap around a white "sheet" card, same
   wrap/card idiom quote_public.html uses for its own standalone page.
   .no-print elements (the on-screen-only toolbar) vanish under @media
   print, and the sheet drops its border/radius/shadow there so the
   printed (and PDF-printed) page is calm black-on-white with no gray
   page chrome around it. -- */
.print-page{
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  justify-content: center;
  background: var(--page-bg);
  padding: 32px 16px 56px;
}
.print-sheet{
  width: 100%;
  max-width: 760px;
  height: fit-content;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 28px 24px;
}
.print-toolbar{ display: flex; justify-content: flex-end; gap: 10px; margin-bottom: 20px; }
.print-head{ margin-bottom: 24px; }
.print-company{ font-size: 12.5px; font-weight: 600; color: var(--text-faint); margin: 0 0 6px; }
.print-job-title{ font-size: 20px; font-weight: 600; color: var(--slate-deep); }
.print-job-number{ font-weight: 500; color: var(--text-secondary); font-size: 14px; }
.print-job-sub{ font-size: 14px; color: var(--text-secondary); margin: 4px 0 0; overflow-wrap: anywhere; }
.print-section{ margin-bottom: 24px; }
.print-section-head{ font-size: 12.5px; font-weight: 600; color: var(--text-faint); margin: 0 0 10px; }
.print-total{ font-size: 14px; font-weight: 600; color: var(--slate-deep); margin: 0 0 14px; }
.print-days{ display: flex; flex-direction: column; gap: 16px; }
.print-day-head{
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 8px;
}
.print-day-date{ font-size: 15px; font-weight: 600; color: var(--text-primary); }
.print-day-hours{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.print-entry{ padding: 8px 0; border-top: 1px solid var(--border); }
.print-entry:first-child{ border-top: none; padding-top: 0; }
.print-entry-time{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); margin: 0; }
.print-entry-notes{ font-size: 14px; color: var(--text-primary); margin: 4px 0 0; white-space: pre-line; overflow-wrap: anywhere; }
.print-photo-grid{ display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); gap: 8px; margin-top: 8px; }
.print-photo{ width: 100%; height: 90px; object-fit: cover; border-radius: var(--radius-control); border: 1px solid var(--border); }

@media print{
  .no-print{ display: none !important; }
  body{ background: var(--white); }
  .print-page{ background: var(--white); min-height: 0; padding: 0; }
  .print-sheet{ max-width: none; border: none; border-radius: 0; padding: 0; }
  .print-day{ break-inside: avoid; }
  .print-photo-grid{ break-inside: avoid; }
}

/* ==========================================================================
   ---------- pkg 10 ----------
   Owner Schedule, O2 (Builder B-SURFACE, schedule_owner.html only -- the
   crew C3 page/classes above are untouched). One calendar-object class,
   .visit-card, used by both the week grid and the day list via a single
   Jinja macro. Week view (desktop >=768px default, or ?view=week) is 7
   columns that h-scroll INSIDE their own .week-grid-wrap container --
   the page itself never scrolls sideways. Day view (375px default, or
   ?view=day) reuses the week-strip component verbatim from pkg 3
   (.week-strip/.week-day/.week-dots/.dot--planned/.dot--logged, styled
   in the pkg-3 section above -- nothing new needed for it here) with
   owner-wide dot truth. --person-1..6 live in :root (above) since
   they're spec-pinned tokens, not pkg-10-scoped ones. Tokens only
   otherwise, 16/24 rhythm, no shadows beyond the existing idiom, checked
   at 320/375/1280/1920 -- zero '$' anywhere on this page.
   ========================================================================== */

/* -- page head: h1 + the Week/Day toggle + New visit (hidden while the
   inline form below is already open) -- */
.o2-head-row{ display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 20px; }
/* R2-U4: this h1 had no rule of its own anywhere in the file -- it was
   riding the bare browser default (2em, bold, whatever the ambient
   font-size happened to be), which computed live at 30px, off every one
   of the 6-step scale's 7 sanctioned sizes and visibly heavier than every
   other page's own title (the exact "type zoo" failure mode UGLY #1 was
   about, just invisible to that slice's font-size: grep since there was
   no font-size: declaration to find -- an absence, not a wrong value).
   Same 20px/600/--slate-deep recipe .page-head h1 and .greet-text h1
   already use everywhere else in the app -- restores /schedule to the
   app's own established page-title convention. */
.o2-head-row h1{ font-size: 20px; font-weight: 600; color: var(--slate-deep); }
.o2-actions{ display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* R3-V1: .view-toggle/.view-toggle-link (the old CSS-decides-by-viewport
   pill) is retired -- .viewseg below (the mock's own cluster component)
   replaces it, server-decided, no viewport toggling left to do. */

/* -- inline new/edit card: plain .card/.form-stack/.field/.field-row/
   .check-field, all reused verbatim -- only the head row + the standalone
   delete form (a sibling of the Save form, never nested inside it) are
   new here. -- */
.visit-form-card{ margin-bottom: 24px; }
.visit-form-head{ display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 4px; }
.visit-form-cancel{ font-size: 12.5px; font-weight: 600; color: var(--text-faint); }
.visit-form-delete-form{ margin-top: 4px; }

/* -- R3-V5 THE VISIT CARD upgrades (audit MISSING #2) --
   job customer/address preview, duration presets, the crew clash
   advisory, optional worker add-ons, and the instructions textarea --
   all reuse the house .field/.check-field vocabulary; only the few
   genuinely new pieces below get their own rules. -- */
.field-hint{ font-weight: 500; color: var(--text-faint); }
.visit-job-preview{
  font-size: 12.5px;
  color: var(--text-secondary);
  margin: 4px 0 0;
  overflow-wrap: anywhere;
}
.duration-presets{ display: flex; gap: 8px; margin: -6px 0 2px; }
.duration-preset{
  font-size: 12px;
  font-weight: 600;
  color: var(--text-secondary);
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 5px 12px;
  cursor: pointer;
}
.duration-preset:hover{ border-color: var(--cobalt); color: var(--cobalt); }
.duration-preset:focus-visible{ outline: 2px solid var(--cobalt); outline-offset: 1px; }
/* the clash line reads as an amber advisory (same family as .visit-clash/
   the amber tray-aging tint) -- deliberately NOT the red .form-error
   treatment, since this never blocks Save. */
.visit-clash-warning{
  font-size: 12.5px;
  font-weight: 600;
  color: var(--amber-text);
  background: var(--amber-bg);
  border-radius: var(--radius-control);
  padding: 7px 10px;
  margin: -4px 0 2px;
}
.visit-extra-list{
  display: flex;
  flex-direction: column;
  gap: 4px;
  max-height: 168px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 8px 10px;
}
.check-field--compact{ font-size: 13px; font-weight: 500; gap: 8px; }

/* -- RV4 W2: round-8 planning-time role picker, one per assigned person
   (the primary Assign-to select's own sibling .field, and each extra-
   worker checkbox row's own inline add-on). .visit-extra-row wraps the
   pre-existing .check-field--compact checkbox + the new select so they
   sit on one line without touching .visit-extra-list's own layout.
   .visit-role-select is deliberately its OWN small style (not ".field
   select", which only applies inside a .field ancestor -- the extra-
   worker rows aren't one) so it looks right in both spots. -- */
.visit-extra-row{ display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.visit-extra-row .check-field--compact{ flex: 1; min-width: 0; }
.visit-role-select{
  flex-shrink: 0;
  max-width: 128px;
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 6px 8px;
  font-size: 12.5px;
  font-family: inherit;
  color: var(--text-primary);
  background: var(--white);
}
.visit-role-select:focus{
  outline: none;
  border-color: var(--cobalt);
  box-shadow: 0 0 0 3px var(--cobalt-tint);
}

/* -- avatar dots: legend row + every .visit-card's assignee line share
   the same base + the six pinned hues; the hollow variant is Unassigned
   (border only, transparent fill -- same recipe as .dot--planned). -- */
.person-dot{ width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.person-dot--p0{ background: var(--person-1); }
.person-dot--p1{ background: var(--person-2); }
.person-dot--p2{ background: var(--person-3); }
.person-dot--p3{ background: var(--person-4); }
.person-dot--p4{ background: var(--person-5); }
.person-dot--p5{ background: var(--person-6); }
.person-dot--unassigned{ background: transparent; border: 1.5px solid var(--text-faint); }

.schedule-legend{ display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; margin-bottom: 20px; }
.legend-item{ display: flex; align-items: center; gap: 6px; }
.legend-name{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }

/* -- R3-V1: the persistent schedule header cluster -- ported token-for-
   token from mockups/mock.css's own .sched-cluster/.date-nav/.date-title/
   .viewseg/.crewfilter (the locked target: schedule-month.html /
   dispatch-board.html), with `button` swapped for `a` (real GET
   navigation, no forms) and `.cf`'s span swapped for a real link (the
   mock's own chips are static illustration; ours actually toggle
   ?crews=) -- same look either way. -- */
.sched-cluster{ display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.date-nav{ display: flex; align-items: center; border: 1px solid var(--border);
  border-radius: var(--radius-control); overflow: hidden; background: var(--white); }
.date-nav a{ border: 0; background: var(--white); font-size: 14px; padding: 8px 12px;
  color: var(--text-primary); text-decoration: none; display: inline-block; }
.date-nav .today{ border-left: 1px solid var(--border); border-right: 1px solid var(--border); font-weight: 600; }
.date-title{ font-size: 17px; font-weight: 700; color: var(--text-primary); }
.viewseg{ display: flex; background: var(--white); border: 1px solid var(--border);
  border-radius: var(--radius-control); overflow: hidden; margin-left: auto; }
.viewseg a{ font-size: 13.5px; padding: 8px 14px; color: var(--text-secondary); text-decoration: none; }
.viewseg a.is-on{ background: var(--slate); color: var(--white); font-weight: 600; }
.crewfilter{ display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.crewfilter .cf{ display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px;
  font-weight: 600; color: var(--text-secondary); border: 1px solid var(--border);
  background: var(--white); border-radius: 999px; padding: 4px 10px; text-decoration: none; }
.crewfilter .cf i{ width: 9px; height: 9px; border-radius: 50%; background: var(--pc); flex-shrink: 0; }
.crewfilter .cf.off{ opacity: .38; }

/* ==========================================================================
   R3-V2 -- the Month view. Ported token-for-token from the locked mock
   (mockups/mock.css's own .month/.mcell/.mchip/.mmore/.cap -- the SAME
   class names, same numbers -- schedule-month.html at 1440 is the pixel
   target). One responsive idiom: the grid stays a 7-column CSS grid at
   every width down to 320 (never collapses to a list), chips shrink under
   900px, and under 600px the text chips hide in favor of `.mdots` (a
   color-dot-per-chip + total-count summary, both server-rendered always --
   see schedule_owner.html's own comment on why this single-DOM/media-
   query split isn't the retired three-views anti-pattern). `.mcell` is an
   `<a>` (whole cell -> that day's Day view); `.mchip--unassigned` is the
   one addition beyond the mock -- a visit with no crew_id (every visit
   still made through the unchanged new/edit-visit form) reads as a
   neutral badge rather than inventing a color the contract never asked
   for. Tokens only, --person-1..6 reused verbatim, no shadow beyond the
   existing .month/.mcell recipe (--shadow-card, already a token).
   ========================================================================== */
.month{ border: 1px solid var(--border); border-radius: var(--radius-surface); overflow: hidden;
  background: var(--border); display: grid; grid-template-columns: repeat(7, 1fr); gap: 1px;
  box-shadow: var(--shadow-card); margin-bottom: 8px; }
.month .dow{ background: var(--app-bg); font-size: 11px; font-weight: 700; letter-spacing: .07em;
  color: var(--text-secondary); text-transform: uppercase; padding: 8px 10px; }
.mcell{ background: var(--white); min-height: 118px; padding: 6px 6px 8px; position: relative;
  display: block; text-decoration: none; }
.mcell.dim{ background: var(--app-bg); }
.mcell .d{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); padding: 2px 4px;
  display: inline-block; }
.mcell.today .d{ background: var(--cobalt); color: var(--white); border-radius: 999px;
  min-width: 22px; text-align: center; }
.mcell .cap{ float: right; font-size: 11px; color: var(--text-faint); padding: 3px 4px 0 0; }
.mchip{ display: flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 600;
  color: var(--white); background: var(--pc, var(--cobalt)); border-radius: 5px; padding: 2px 6px;
  margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mchip--unassigned{ background: var(--text-faint); }
.mchip time{ font-weight: 700; opacity: .9; }
.mmore{ display: block; font-size: 11px; font-weight: 700; color: var(--cobalt); padding: 3px 6px 0; }
.mdots{ display: none; gap: 4px; align-items: center; margin-top: 6px; flex-wrap: wrap; }
.mdots .dot{ width: 7px; height: 7px; border-radius: 50%; background: var(--pc, var(--text-faint));
  flex: none; }
.mdots .cnt{ font-size: 11px; font-weight: 700; color: var(--text-secondary); }
.month-legend-note{ font-size: 12.5px; color: var(--text-faint); margin: 0 0 20px; }

@media (max-width: 899px){
  .mcell{ min-height: 84px; padding: 4px 4px 6px; }
  .mchip{ font-size: 11px; padding: 1px 4px; gap: 3px; }
  .mcell .cap{ font-size: 11px; }
}
@media (max-width: 599px){
  /* density degradation (Fantastical's own pattern, audit-findings-
     2026-08-03.md's Industry research section: "when cells are too small
     for chips, degrade to dots/heat instead of clipping") -- text chips
     hide, the dot+count summary (always in the DOM) takes over. */
  .mchip, .mmore{ display: none; }
  .mdots{ display: flex; }
  .mcell{ min-height: 60px; padding: 3px 3px 5px; }
}

/* -- the one calendar-object component: time + stage chip on top (same
   top-row recipe as .stop-card/.sched-card), title, assignee dot+name,
   and the amber clash word only when present. A plain link, not a
   button -- click-to-edit, nothing else. -- */
.visit-card{
  display: block;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 12px 14px;
  min-width: 0;
  /* R3-V5 touch: see .day-slot's own comment -- DAY-MOVE's long-press-
     to-lift needs native touch scroll disabled on the draggable surface
     itself, never on the page/grid around it. */
  touch-action: none;
}
.visit-card:hover{ border-color: var(--cobalt); }
/* R3-V5: the dragged source card, dimmed in place while its ghost
   travels -- same recipe as .dv--dragging/.tray-item--dragging. */
.visit-card--dragging{ opacity: 0.35; }
.visit-card-top{ display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
.visit-time{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.visit-job{ font-size: 14px; font-weight: 600; color: var(--slate-deep); margin: 0 0 6px; line-height: 1.3; overflow-wrap: anywhere; }
.visit-number{ font-weight: 500; color: var(--text-secondary); }
.visit-assignee{ display: flex; align-items: center; gap: 6px; }
.visit-assignee-name{ font-size: 12.5px; font-weight: 500; color: var(--text-secondary); overflow-wrap: anywhere; }
.visit-clash{ font-size: 11px; font-weight: 600; color: var(--amber-text); margin: 6px 0 0; }

/* R3-V1: the old Week view (7-column card grid, .week-grid-wrap/
   .week-grid/.week-col*) is retired -- Week now 302s to Board
   server-side (app.py), so that markup never renders; its CSS is
   removed along with it rather than left dead. */

/* -- day view: heading (the week strip above it reuses pkg 3's
   .week-strip untouched, R3-V4 adds .week-day--selected above). -- */
.day-head-row{ display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
.o2-day-head{ font-size: 17px; font-weight: 600; color: var(--slate-deep); margin: 0; }
.day-empty-note{ font-size: 12.5px; color: var(--text-faint); }
/* R3-V4: `.visit-stack` is now the DAY GRID's own outer wrapper (kept as
   the class name battery/pkg10_checks.py's own check_one_item_type
   already scans for "the day view's own .visit-stack holds exactly
   today's visits" -- a flex column of TWO children (the col-heads row,
   the grid itself) still satisfies that literally, so no driver churn
   was needed there for this rename-in-place). */
.visit-stack{ display: flex; flex-direction: column; gap: 12px; }

/* R3-V1: the pkg-10 CSS-decides-by-viewport toggle (.o2-week-view/
   .o2-day-view display rules + the .o2-force-day/.o2-force-week
   modifiers) is retired -- the SERVER now decides which one view
   renders (app.py's `view`), so each section is either fully in the DOM
   or absent entirely; no visibility CSS is needed to pick among them
   anymore. */

/* ==========================================================================
   R3-V4 -- THE DAY TIME GRID. Columns per CREW (solo/no-crew: one
   synthetic column), a fixed 7:00-18:00 visible band with grey shoulders
   over any hour outside the company's own configured workday, the rest
   collapsed behind a native <details> reveal (zero JS). Geometry is
   entirely server-computed (db.build_day: top/height in px, 1px/minute,
   left/width in %) -- this section only lays out the GRID SCAFFOLD (hour
   gutter, column tracks, the now-line, hour-row lines/shoulder tint) and
   positions blocks via `position: absolute` inside each `.day-col`
   (whose own `position: relative` + `height: var(--section-h)` is what
   every child's top/height number is relative to).
   ========================================================================== */
.day-col-heads,
.day-section-inner{
  display: grid;
  grid-template-columns: 52px repeat(var(--col-count), minmax(120px, 1fr));
}
.day-grid{ overflow-x: visible; }
/* Below ~700px, a squeezed <120px column wraps "7:00 AM - 3:30 PM" onto
   three lines and clips the stage chip -- same "don't squeeze past
   legible, scroll instead" call the crew-row Board already makes at its
   own narrow breakpoint. A raised 150px floor plus #day-grid-wrap's own
   overflow-x:auto (declared above) trades a bit of horizontal scroll for
   a block that's actually readable. */
@media (max-width: 700px){
  .day-col-heads,
  .day-section-inner{ grid-template-columns: 44px repeat(var(--col-count), minmax(150px, 1fr)); }
  .day-col .visit-card{ padding: 5px 7px; }
  .day-col .visit-card .visit-card-top{ flex-wrap: wrap; gap: 4px; margin-bottom: 3px; }
  .day-col .visit-card .visit-time{ font-size: 11px; }
  .day-col .visit-card .visit-job{ font-size: 12.5px; margin-bottom: 3px; }
}
#day-grid-wrap{ overflow-x: auto; -webkit-overflow-scrolling: touch; }
.day-col-heads{
  border-bottom: 1px solid var(--border);
  padding-bottom: 10px;
  margin-bottom: 2px;
}
.day-gutter-head{ min-width: 0; }
.day-col-head{
  min-width: 0;
  padding: 0 8px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.day-col-head-name{ font-size: 13.5px; font-weight: 600; color: var(--text-primary); overflow-wrap: anywhere; }
.day-anytime{ display: flex; flex-direction: column; gap: 6px; margin-top: 2px; }
.day-anytime .visit-card{ position: static; } /* undoes the .day-col-scoped absolute rule below -- these aren't grid-positioned */

.day-collapsed{ margin: 0; }
.day-collapsed > summary{
  list-style: none;
  cursor: pointer;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-faint);
  padding: 6px 8px 6px 60px;
  user-select: none;
}
.day-collapsed > summary::-webkit-details-marker{ display: none; }
.day-collapsed > summary:hover{ color: var(--cobalt); }
.day-collapsed > summary::before{ content: "▸ "; }
.day-collapsed[open] > summary::before{ content: "▾ "; }

.day-section-inner{ position: relative; }
.day-gutter{ position: relative; min-width: 0; height: var(--section-h); }
.day-hour-label{
  position: absolute;
  left: 0; right: 6px;
  transform: translateY(-6px);
  font-size: 10.5px;
  font-weight: 600;
  color: var(--text-faint);
  text-align: right;
  white-space: nowrap;
}
.day-hour-line{
  position: absolute;
  left: 0; right: 0;
  border-top: 1px solid var(--border);
  pointer-events: none;
}
.day-hour-line--shoulder{ border-top-color: var(--border); }
.day-shoulder-band{
  position: absolute;
  left: 0; right: 0;
  height: 60px;
  background: var(--app-bg);
  pointer-events: none;
}
.day-now-line{
  position: absolute;
  left: 0; right: 0;
  height: 2px;
  background: var(--danger);
  z-index: 6;
  pointer-events: none;
}
.day-now-line::before{
  content: "";
  position: absolute;
  left: 46px;
  top: 50%;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--danger);
  transform: translate(-50%, -50%);
}
.day-col{
  position: relative;
  min-width: 0;
  height: var(--section-h);
  border-left: 1px solid var(--border);
}
/* R3-V5: DAY-MOVE's own drop target highlight (board.js), same visual
   language as Board's own .daycell--drop-target. */
.day-col--drop-target{ outline: 2px dashed var(--cobalt); outline-offset: -2px; }
.day-slot{
  position: absolute;
  left: 0; right: 0;
  z-index: 1;
  display: block;
  /* R3-V5 touch: deliberately NOT touch-action:none here, unlike .dv/
     .tray-item/.visit-card just below -- those are small, discrete
     tiles (blocking native scroll under one tile costs little, since a
     finger can always land just outside it); .day-slot instead TILES
     THE ENTIRE day-grid background (every hour, every column), so a
     none here would make it impossible to touch-scroll the day view at
     all anywhere inside the grid -- a far worse regression than the one
     this rung is fixing. The trade honestly accepted: DRAG-SELECT on
     touch is best-effort (a slow, still-then-drag gesture usually lifts
     before the OS claims it for panning, same odds every other touch-
     action:auto element gets) rather than the same guaranteed-reliable
     long-press this file gives the other three surfaces; a plain tap's
     own click-to-create (v4's own feature) is completely unaffected
     either way. See board.js's own header comment. */
}
.day-slot:hover{ background: var(--cobalt-tint); }
/* R3-V5: the drag-select range preview (board.js's own day-select ghost)
   -- a filled cobalt band with its own live "7:00 AM – 9:00 AM" label,
   positioned/sized purely by inline style (top/height), same "CSS lays
   out the scaffold, JS supplies the numbers" split every other Day-grid
   geometry piece already follows. */
.day-select-ghost{
  position: absolute;
  left: 2px; right: 2px;
  z-index: 3;
  background: var(--cobalt-tint);
  border: 2px dashed var(--cobalt);
  border-radius: 5px;
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
}
.day-select-ghost-label{
  font-size: 11.5px;
  font-weight: 700;
  color: var(--cobalt);
  background: var(--white);
  border-radius: 999px;
  padding: 2px 8px;
  white-space: nowrap;
}
.day-col .visit-card{
  position: absolute;
  z-index: 2;
  margin: 0 2px;
  padding: 6px 8px;
  overflow: hidden;
  border-left: 3px solid var(--pc, var(--text-faint));
  box-shadow: 0 1px 3px rgba(32,40,50,.14);
}
.day-col .visit-card .visit-job{ overflow-wrap: anywhere; }
.day-col .visit-card--stacked{ box-shadow: 0 3px 8px rgba(32,40,50,.22); border: 1px solid var(--white); }
.visit-card-more{
  position: absolute;
  top: 4px; right: 4px;
  font-size: 10.5px;
  font-weight: 700;
  color: var(--white);
  background: var(--slate-deep);
  border-radius: 999px;
  padding: 1px 6px;
}

/* ==========================================================================
   R3-V4 -- List (agenda) view. A plain chronological table, day-grouped
   -- the missing print/dispatch-sheet surface the audit's own Industry
   research names ("crews still tape paper schedules to dashboards").
   `.crew-chip` is new (List's own crew-identity chip, --pc-colored like
   every other crew marker in the app); everything else reuses the
   house .card/.chip/.empty-block vocabulary.
   ========================================================================== */
.agenda-head-row{ display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
.agenda-total{ font-size: 13px; font-weight: 600; color: var(--text-secondary); }
.agenda-days{ display: flex; flex-direction: column; gap: 20px; }
.agenda-day{ background: var(--white); border: 1px solid var(--border); border-radius: var(--radius-surface); padding: 14px 16px; }
.agenda-day--today{ border-color: var(--cobalt); }
.agenda-day-head{
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  padding-bottom: 8px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.agenda-day-date{ font-size: 14.5px; font-weight: 600; color: var(--slate-deep); }
.agenda-day-count{ font-size: 12px; color: var(--text-faint); }
.agenda-rows{ display: flex; flex-direction: column; }
.agenda-row{
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 4px;
  border-top: 1px solid var(--border);
  min-width: 0;
}
.agenda-row:first-child{ border-top: none; padding-top: 0; }
.agenda-row:hover{ background: var(--app-bg); }
.agenda-row-time{ flex: 0 0 auto; width: 84px; font-size: 12px; font-weight: 600; color: var(--text-secondary); }
.agenda-row-main{ flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.agenda-row-customer{ font-size: 14px; font-weight: 600; color: var(--slate-deep); overflow-wrap: anywhere; }
.agenda-row-job{ font-size: 12.5px; color: var(--text-secondary); overflow-wrap: anywhere; }
.agenda-row-crew{ flex: 0 0 auto; }
.crew-chip{
  display: inline-flex;
  align-items: center;
  font-size: 11px;
  font-weight: 600;
  padding: 3px 9px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--pc, var(--text-faint)) 16%, var(--white));
  color: var(--slate-deep);
  border: 1px solid var(--pc, var(--border));
  white-space: nowrap;
}
.crew-chip--unassigned{ background: var(--app-bg); border-color: var(--border); color: var(--text-faint); }

/* -- print: a clean day-by-day dispatch sheet (Cmd/Ctrl-P from List) --
   hides the whole app shell (base.html's own header/nav carry no
   .no-print hook of their own, so this targets them by their real
   selectors directly) plus this page's own header cluster and Print
   button; each .agenda-day avoids splitting across a page break. -- */
@media print{
  .topbar, .bottomtabs, .sched-cluster, .schedule-legend,
  .board-word, #board-tray, .no-print{ display: none !important; }
  body{ background: var(--white); }
  .shell-content{ padding: 0 !important; max-width: none !important; }
  .agenda-day{ border: 1px solid #000; break-inside: avoid; }
  .agenda-row:hover{ background: none; }
}

/* ==========================================================================
   ---------- pkg 11 ----------
   Card & bank payments + receipts (B-SURFACES). Three surfaces touched,
   most of it deliberate REUSE, not new rules -- built ahead of A per the
   pkg-8/9 integration standard (stripe_api.py/payments table not landed
   as of this build, verified by grep):
   - templates/bill_public.html (X0b) is the standalone token page (its
     own inline <style>, offline.html's pattern -- see that file's own
     header comment) and carries ZERO dependency on this stylesheet, same
     as every other X0-series public page. Its Pay-by-card/Pay-by-bank
     buttons, the pinned bank-fee note, and the receipt link are styled
     inline there, not here -- nothing for this section to add.
   - templates/office_settings.html's new Payments card is a checkbox +
     two password-type key inputs + three quiet notes -- exactly the same
     shape as the existing Mode radios (.check-field) and Company text
     fields (.field), inside the same .card/.section-head/.form-stack/
     .btn wrapper every other card here already uses. No new rule needed.
   - templates/job_thread.html's #thread-billing gains a receipt link
     next to the existing paid-by word. It reuses .billing-link-action's
     own look verbatim (the same "colored bold inline link" recipe the
     share-link's Text it/Email it actions already use, two sections up)
     -- just one small wrapper below for its own top margin, since it
     sits outside the .billing-link-actions flex row those live in.
   Tokens only; no shadows beyond the existing recipe; checked at 320px.
   ========================================================================== */
.billing-receipt{ margin-top: 10px; }

/* ==========================================================================
   ---------- R3-V3 -- THE CREW-ROW DISPATCH BOARD ----------
   Owner Schedule, Board view (mockups/dispatch-board.html -- the locked
   visual contract, ported class-for-class: .board-wrap/.dboard/.avstack/
   .crewcell/.daycell/.dv/.tray-item). Retires p2-1's ENTIRE 24-hour
   time-grid Board (the --min-px geometry idiom, the hour gutter, the
   per-day sticky head, .board-block/.board-chip, the clash-word system,
   the two-section unassigned/no-visit tray, both drag-state classes) --
   see build-notes/r3/v3-asbuilt.md for why each piece is gone rather
   than adapted: rows are now CREWS (Wyatt's dispatch-model ruling), so
   there is no more "whose day is this" ambiguity a time-grid or a clash
   word needed to resolve, and tiles are whole-day (no minute geometry
   left to calc). The old p2-2 per-day-per-person capacity strip is
   retired the same way -- its job (visible booked load) is now the crew
   row's own "N visits · Xh this week" word, crew-scoped and week-scoped
   like the rest of this board instead of person-scoped and day-scoped.

   Board is day-granular: a CSS grid, label column + one column per
   visible day (--board-cols, set inline off board.dates' own length --
   6 normally, 7 the moment a Sunday visit exists). Responsive: >=1024px
   the grid's day columns are `1fr` (no scroll -- tile text truncates,
   never overflows, same .dv .c/.w ellipsis recipe as the mock); below
   that the columns switch to a readable fixed minimum and the grid
   itself gets its own horizontal scroll (mirrors every other in-
   container-scroll grid this app already uses -- the PAGE never scrolls
   sideways). The tray sits beside the board >=1024px, stacks below it
   narrower than that (mirrors .board-layout's own retired breakpoint).
   ========================================================================== */

.board-note{ font-size: 12.5px; color: var(--text-faint); margin: 10px 0 0; }

.board-wrap{ display: flex; flex-direction: column; gap: 16px; margin-bottom: 8px; }
@media (min-width: 1024px){
  .board-wrap{ display: grid; grid-template-columns: 1fr 268px; gap: 16px; align-items: start; }
}

.dboard{
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  overflow: hidden;
  background: var(--border);
  display: grid;
  /* minmax(0, 1fr), NOT bare 1fr -- a bare `1fr` track's automatic
     minimum is its content's own min-content width, which a nowrap
     "Kitchen gut remodel · Cranston" .dv .w span can make wide enough
     to blow the whole grid out past its container (classic CSS Grid
     "blowout": the ellipsis/overflow:hidden on .dv .c/.w never even
     gets a chance to fire, since sizing happens before that clipping
     would apply) -- confirmed live before shipping (Friday's column
     clipped clean off at 1440px). minmax(0, 1fr) lets a track shrink
     to 0 and THEN grow with 1fr, so the ellipsis rule does its job. */
  grid-template-columns: 220px repeat(var(--board-cols), minmax(0, 1fr));
  gap: 1px;
  box-shadow: var(--shadow-card);
}
@media (max-width: 899px){
  .dboard{
    grid-template-columns: 150px repeat(var(--board-cols), 118px);
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
}
.dboard .h{
  background: var(--app-bg);
  padding: 8px 10px;
  font-size: 12px;
  font-weight: 700;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: .05em;
  white-space: nowrap;
}
.dboard .h.today{ color: var(--cobalt); }

/* -- crew identity cell: stacked avatars (overlapping circles, first-
   name initials) + name + member list + the week-load word. The
   synthetic solo/no-crew row (build_board's own fallback) renders with
   NO avstack at all (row.members is [] there) -- Wyatt's own ruling:
   "solo mode: single row, no avatars". -- */
.avstack{ display: flex; flex: none; }
.avstack .av{
  margin-left: -9px;
  border: 2px solid var(--white);
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--pc);
  color: var(--white);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
  flex: none;
}
.avstack .av:first-child{ margin-left: 0; }
.crewcell{
  background: var(--white);
  padding: 10px 12px;
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.crewcell .nm{ font-size: 13.5px; font-weight: 600; color: var(--text-primary); overflow-wrap: anywhere; }
.crewcell .nm-members{ font-size: 13.5px; font-weight: 600; color: var(--text-primary); overflow-wrap: anywhere; }
.crewcell .fr{ font-size: 11px; color: var(--text-faint); }
@media (max-width: 899px){
  /* the full "— Marcus, Jake, Rosa, Silvio" roster has no room in a
     150px label column (verified live: it wrapped to 7+ lines and blew
     out the row's height) -- the avatar stack alone still carries
     identity at this width; the crew filter chips (header cluster,
     always visible) are where the abbreviated "Marcus +3" word already
     lives for anyone who needs the full roster spelled out. */
  .crewcell .nm-members{ display: none; }
  .crewcell{ padding: 8px; gap: 6px; }
  .avstack .av{ width: 24px; height: 24px; font-size: 10.5px; margin-left: -7px; }
}

/* -- one day cell per crew row: a plain flex stack of tiles (no time-axis
   geometry left -- a day either has N visits or it doesn't). -- */
.daycell{
  background: var(--white);
  padding: 6px;
  min-height: 64px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.daycell--drop-target{ outline: 2px dashed var(--cobalt); outline-offset: -2px; }

/* -- the one visit tile: time + customer (bold) + "job · town", crew-
   color left edge. Done visits (stage=='done', template-resolved) gray
   out and strike the customer line -- the mock's own .dv.done recipe,
   byte-identical. tile.color_idx None (the solo/no-crew synthetic row)
   renders --pc as --text-faint instead of guessing a crew color. -- */
.dv{
  display: block;
  border-left: 3px solid var(--pc);
  background: var(--app-bg);
  border-radius: 5px;
  padding: 4px 7px;
  min-width: 0;
  touch-action: none; /* R3-V5 touch: see .day-slot's own comment */
}
.dv:hover{ background: var(--cobalt-tint); }
.dv time{ font-size: 11px; font-weight: 700; color: var(--text-secondary); display: block; }
.dv .c{
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dv .w{
  display: block;
  font-size: 11px;
  color: var(--text-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dv.done{ opacity: .55; }
.dv.done .c{ text-decoration: line-through; }

/* -- drag states (board.js drives these). Ghost = the floating clone
   following the pointer; dragging = the original tile/tray-item left
   dim in place while its ghost travels. -- */
.dv--ghost{
  opacity: 0.6;
  border-style: dashed;
  box-shadow: 0 4px 14px rgba(35,44,54,0.22);
  pointer-events: none;
  z-index: 6;
  background: var(--white);
  border-radius: 5px;
}
.dv--dragging{ opacity: 0.35; }
.tray-item--dragging{ opacity: 0.35; }

/* -- R3-V5 TOUCH: the long-press-to-lift cue (audit DEAD "touch dragging
   hard-disabled... the guard is one line") -- a small scale + lift shadow
   marks the exact moment a held touch has been confirmed as a drag, not
   a scroll (board.js adds this class in liftForTouch, before any pointer
   has actually moved yet, so the cue itself is the "you can drag now"
   signal). Works identically for a MOUSE drag too (it just never lingers
   there, since a mouse gesture is confirmed by movement, not a timer) --
   shared across every draggable class this file defines. -- */
.dv.drag-lifted,
.tray-item.drag-lifted,
.visit-card.drag-lifted{
  transform: scale(1.03);
  box-shadow: 0 6px 18px rgba(32,40,50,.24);
  z-index: 5;
  transition: transform .1s ease, box-shadow .1s ease;
}
/* -- the live time readout while dragging a Day-view visit (audit
   CONFUSING "no live time readout while dragging... you only learn the
   landing time after release") -- follows the pointer via inline style. -- */
.drag-time-badge{
  position: fixed;
  background: var(--slate-deep);
  color: var(--white);
  font-size: 12px;
  font-weight: 700;
  padding: 4px 10px;
  border-radius: 999px;
  pointer-events: none;
  z-index: 1000;
  box-shadow: 0 4px 12px rgba(32,40,50,.24);
}

/* -- the Unscheduled tray: jobs with no future visit, aged fresh/aging/
   old (mockups/dispatch-board.html's own .tray-item, byte-identical
   class names/recipe). -- */
.tray{ display: flex; flex-direction: column; }
.tray-item{
  display: block;
  border: 1px solid var(--border);
  border-left-width: 3px;
  border-radius: 7px;
  background: var(--white);
  padding: 8px 10px;
  margin-bottom: 8px;
  min-width: 0;
  touch-action: none; /* R3-V5 touch: see .day-slot's own comment */
}
.tray-item:hover{ background: var(--app-bg); }
.tray-item .tt{ font-size: 13px; font-weight: 600; color: var(--text-primary); overflow-wrap: anywhere; }
.tray-item .ts{ font-size: 12px; color: var(--text-secondary); overflow-wrap: anywhere; }
.tray-item .age{ font-size: 11px; font-weight: 700; margin-top: 2px; display: inline-block; }
.tray-item.fresh{ border-left-color: var(--border); }
.tray-item.aging{ border-left-color: var(--amber-text); }
.tray-item.aging .age{ color: var(--amber-text); }
.tray-item.old{ border-left-color: var(--danger); }
.tray-item.old .age{ color: var(--danger); }
.tray-hint{ font-size: 12px; color: var(--text-faint); margin-top: 2px; }

/* -- the drag/drop feedback toast (board.js). CONFUSING audit fix: the
   old inline #board-word sat at the top of the page, "far from the
   block being dragged; mid-grid you never see it" -- now a fixed
   bottom-center toast, near wherever the drop actually happened,
   readable at any scroll position. Success and failure share the same
   element/slot (board.js toggles the modifier); `hidden` is what keeps
   it out of the layout when quiet, same contract as before. -- */
.board-word{
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  background: var(--slate-deep);
  color: var(--white);
  font-size: 13px;
  font-weight: 600;
  padding: 10px 18px;
  border-radius: 999px;
  box-shadow: 0 8px 24px rgba(32,40,50,.28);
  z-index: 80;
  max-width: calc(100vw - 32px);
  text-align: center;
}
.board-word--fail{ background: var(--danger); }

/* -- "Repeating weeks" link, on the same row as the Board/Week/Day pill
   but not part of it (a separate small action, not a fourth toggle
   state). R3-V6: color bumped off --text-faint (audit CONFUSING "styled
   in --text-faint -- it looks like a disabled label, not a link") to a
   real, legible link color. -- */
.patterns-link{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }
.patterns-link:hover{ color: var(--cobalt); }

/* -- find-a-slot: the new-visit card's trigger reuses .billing-link-action
   verbatim (the house "quiet bold colored inline link" recipe); the three
   suggestion chips are small quiet buttons, same pill recipe as
   .filter-chip but its own class since these aren't a filter -- a link,
   not a form control, honest about what happens on click (prefill + swap
   the page, nothing submits). -- */
.slot-chips{ display: flex; flex-wrap: wrap; gap: 8px; margin: 2px 0 20px; }
.slot-chip{
  display: inline-flex;
  align-items: center;
  font-size: 12.5px;
  font-weight: 600;
  padding: 7px 12px;
  border-radius: 999px;
  background: var(--app-bg);
  border: 1px solid var(--border);
  color: var(--text-primary);
}
.slot-chip:hover{ border-color: var(--cobalt); }
/* empty "no openings" word is .empty-inline now (.visit-form-card
   .empty-inline override above supplies the margin). */
.slot-trigger{ display: inline-block; margin: 2px 0 20px; }


/* ==========================================================================
   Patterns page (templates/schedule_patterns.html). List + create at the
   top (same "list card, then a create-form card" idiom as office_team.html
   and office_settings.html's price book); the selected pattern's editor
   below reuses .card/.section-head/.field/.field-row/.check-field/
   .line-delete-form/.line-delete-btn/.settings-note verbatim (empty word
   is .empty-block, templates/_macros.html's empty_block())
   -- only the 7-day column layout and the stamped-week exceptions rows are
   genuinely new shapes.
   ========================================================================== */

.pattern-list{ display: flex; flex-direction: column; gap: 8px; }
.pattern-row{
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: var(--app-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 10px 14px;
}
.pattern-row:hover{ border-color: var(--cobalt); }
.pattern-row--active{ border-color: var(--cobalt); background: var(--cobalt-tint); }
.pattern-row-label{ font-size: 14px; font-weight: 600; color: var(--slate-deep); overflow-wrap: anywhere; }
.pattern-row-count{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); flex-shrink: 0; }

.pattern-editor-head{ display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 4px; }

/* -- R3-V6 de-uglify (audit UGLY "SEVEN permanently-expanded add-item
   forms ... looks like a debug page, not a scheduling tool", "at 1440px
   the weekend is cut off behind an in-card horizontal scroll with no
   affordance"): an even 7-column GRID, no forced per-column min-width --
   with every add-item form collapsed behind its own <details> (below),
   each column's real content is just a short item-card stack, so all 7
   fit inside the card at 1440 with zero horizontal scroll (the mock's
   own calm density). .pattern-days-wrap kept as a belt-and-suspenders
   h-scroll container (never removed -- costs nothing when unneeded,
   guards the page from ever scrolling sideways if a future item card
   grows wide) but the grid itself no longer forces it to fire. Below
   899px (this file's own established stack breakpoint, e.g. the day-grid
   mobile density fix) each day drops to a full-width stacked block --
   seven short lists read better than seven squeezed slivers on a phone. -- */
.pattern-days-wrap{ overflow-x: auto; margin: 4px 0 20px; }
.pattern-days{ display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 10px; }
@media (max-width: 899px){
  .pattern-days{ grid-template-columns: 1fr; }
}
.pattern-day{
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
}
.pattern-day-head{
  font-size: 12.5px;
  font-weight: 700;
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: 0.01em;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}
.pattern-day-items{ display: flex; flex-direction: column; gap: 8px; min-height: 20px; }
/* empty-day dash is .empty-inline now (.pattern-day .empty-inline
   override above supplies the centering/padding). */

.pattern-item{
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 8px 10px;
}
.pattern-item-time{ font-size: 11px; font-weight: 600; color: var(--text-secondary); margin: 0; }
.pattern-item-title{ font-size: 12.5px; font-weight: 600; color: var(--slate-deep); margin: 0; overflow-wrap: anywhere; }
.pattern-item-assignee{ display: flex; align-items: center; gap: var(--space-1); }
.pattern-item-assignee-name{ font-size: 11px; font-weight: 500; color: var(--text-secondary); }
.pattern-item .line-delete-form{ align-self: flex-start; margin-top: 2px; }

/* -- R3-V6: the collapsed "+ Add" trigger -- <details>/<summary>, no JS
   needed to open it (same no-JS-needed disclosure idiom as
   today_crew.html's own #log-details, styled quiet/small instead of that
   one's big primary-blue button -- this is a per-day secondary action,
   not the page's main CTA). -- */
.pattern-add-details{ margin-top: auto; }
.pattern-add-details > summary{
  list-style: none;
  cursor: pointer;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--cobalt);
  padding: 6px 2px;
  user-select: none;
}
.pattern-add-details > summary::-webkit-details-marker{ display: none; }
.pattern-add-details > summary::marker{ content: ""; }
.pattern-add-details[open] > summary{ color: var(--text-secondary); }

/* -- the open add-item form: fields stack vertically (a narrow grid
   column has no room for .field-row's side-by-side layout except the
   Start/End pair, which keeps it) -- its own compact field rhythm, same
   input look as everywhere else via the bare .field/select/input rules
   above, just tighter gaps. -- */
.pattern-add-form{
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 8px;
  border-top: 1px dashed var(--border);
}
.pattern-add-form .field-row{ gap: 8px; }
.pattern-add-form .field label{ font-size: 11px; }
.pattern-add-form .check-field{ font-size: 12.5px; }
.pattern-add-note{ font-size: 11px; color: var(--text-faint); margin: -2px 0 0; }
.pattern-add-btn{ align-self: stretch; }

/* -- R3-V6: the merged Copy-onto-a-week control (was two separate "Stamp
   this week"/"Stamp next week" forms+buttons -- audit's own rename-map
   fix, "one control replacing the two stamp buttons") -- one form, the
   date input's own label doubles as the sentence ("Copy onto week of
   <picker>"). -- */
.pattern-copy-form{ display: flex; flex-wrap: wrap; align-items: flex-end; gap: 16px; }
.pattern-copy-form .field{ display: flex; flex-direction: column; gap: 8px; }

.pattern-week-list{ display: flex; flex-direction: column; gap: var(--space-4); }
.pattern-week{
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 10px 14px;
  background: var(--app-bg);
}
.pattern-week-label{ font-size: 12.5px; font-weight: 600; color: var(--slate-deep); margin: 0 0 2px; }
.pattern-week-words{ font-size: 12.5px; color: var(--text-secondary); margin: 0; }
.pattern-week-diff{ margin: 8px 0 0; padding-left: 18px; }
.pattern-week-diff li{ font-size: 12.5px; color: var(--text-secondary); margin: 0 0 3px; }
.pattern-unstamp-form{ margin-top: 8px; }

/* ==========================================================================
   ---------- p2-3 ----------
   Job thread (Builder B-SURFACE): the quote block's "Draft from the log"
   shortcut -- a quiet button + the math-shows-its-work words, plus a
   small tag on the one auto line it produces (item.source). Sits inside
   the existing #thread-quote editable branch, above the manual add-line
   form (a third way to add a line, alongside typing one and picking from
   the price book). Same "math shows its work" idiom as .costing-row-
   detail, own class family per house pattern (that reasoning is repeated
   at every _-row-detail-alike rule in this file -- one feature's CSS
   never drifts another's). Tokens only, no new colors, checked at 320px.
   ========================================================================== */

/* -- the button + words sit on one quiet row, wrapping at narrow widths
   (button first, words below) rather than a fixed two-column layout --
   the words can run long ("14 h 0 min logged × $1,234.50/hr" territory
   never actually happens, but wrap-safety costs nothing). -- */
.quote-from-log{ display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 0 0 16px; }
.quote-from-log-form{ flex-shrink: 0; }
.quote-from-log-words{ font-size: 12.5px; font-weight: 500; color: var(--text-faint); margin: 0; }
/* R0-S3: shown INSTEAD of the quiet math words above when the quote
   already has a manual "Labor" line -- same amber warning-word look as
   .visit-clash/.board-word elsewhere, never the .form-error banner box
   (this isn't a rejected submit, just a heads-up next to a live button). */
.quote-from-log-caution{ font-size: 12.5px; font-weight: 600; color: var(--amber-text); margin: 0; }

/* -- the auto line's small tag -- same quiet-faint recipe as .quote-
   viewed/.billing-date-word, sits in the line's info column under the
   qty×price row (.quote-line-info is already flex-column, gap:2px, so
   this just drops into the stack -- no layout change needed there). -- */
.quote-line-source{ font-size: 11px; font-weight: 500; color: var(--text-faint); }

/* ==========================================================================
   ---------- p2-4 ----------
   Jobber import (Builder B-SURFACE): office_import.html's upload/preview/
   result states + the office_index.html Import card. Deliberately reuses
   almost everything verbatim rather than inventing new components: the
   two upload slots sit in the existing .grid-2/.card/.field recipe, the
   mapping + status tables reuse .report-table's in-container-scroll
   recipe byte-for-byte (pkg 7), the Law-6 imported/already-here/skipped/
   total counts reuse .flow-strip/.flow-cell (O1) as a 4-up stat row, and
   the confirm-step explainer reuses .form-message. Only three things are
   genuinely new below, all small and calm on purpose. Tokens only,
   checked at 320px.
   ========================================================================== */

/* -- mapping table's "not imported — <reason>" cell text: same quiet-
   faint recipe as .quote-line-source above, not an error color -- an
   unmapped column is an honest fact about the file, not a mistake. -- */
.import-not-mapped{ color: var(--text-faint); font-weight: 500; }

/* -- quiet note lists: duplicate-candidate words, skipped-row words,
   not-previewed/not-processed words, per-file count breakdowns. One
   family for all four so the import page never invents a new "alert"
   look per list -- everything here is calm, plain-sentence disclosure. -- */
.quiet-list{ display: flex; flex-direction: column; gap: 6px; margin: 12px 0 0; padding: 0; list-style: none; }
.quiet-list li{ font-size: 12.5px; color: var(--text-secondary); overflow-wrap: anywhere; }
.report-table + .quiet-list{ margin-top: 14px; }

/* -- the standing "what Jobber doesn't export" box: calm information,
   NOT the .form-error amber warning look (architect ruling) -- a plain
   tinted card using --app-bg (the shell's own ambient background) so it
   reads as "just a fact" rather than "something to fix", the same
   distinction --app-bg already draws against white .card content
   elsewhere on this page. -- */
.import-honesty{
  background: var(--app-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 20px 24px;
}
.import-honesty .quiet-list li{ color: var(--text-primary); }

/* -- file inputs default to a slightly mismatched browser widget inside
   .field's padded/bordered box (built for text inputs) -- tighter
   padding only, everything else (border, radius, focus ring) stays the
   same shared rule. -- */
.field input[type="file"]{ padding: 9px 10px; }

/* ==========================================================================
   ---------- p2-5 ----------
   Job thread (Builder B-SURFACE): the X2 star toggle word in #thread-
   header, the new #thread-progress section (mint/revoke the customer's
   no-login progress link, same once-per-render share-link-reveal idiom
   pkg-8/pkg-9's own .quote-link-reveal/.billing-link-reveal already
   established two sections up -- own class family here per that same
   house pattern, identical look, kept separate so one feature's CSS
   never drifts another's), and the quiet per-photo "show on the
   progress page" checkbox added to the existing #thread-photos grid.
   progress_public.html/progress_dead.html (the standalone /p/<token>
   pages) own NO rules here -- same no-shared-stylesheet rule quote_
   public.html/bill_public.html already follow, their tokens are copied
   by hand into their own inline <style> blocks instead. Tokens only, no
   new colors, checked at 320px.
   ========================================================================== */

/* -- #thread-header: the star toggle word, a single quiet button (no
   separate Save, one click = one POST, same "rail-click anywhere
   anytime" idiom the status rail two rules up already uses) -- sits as
   its own line above the address, quiet by default, cobalt only once
   starred so an un-starred job never draws the eye. -- */
.star-form{ margin: 2px 0 8px; }
.star-toggle{
  display: inline-flex;
  align-items: center;
  font-size: 12.5px;
  font-weight: 600;
  font-family: inherit;
  color: var(--text-faint);
  background: none;
  border: none;
  padding: 2px 0;
  cursor: pointer;
}
.star-toggle:hover{ color: var(--text-secondary); }
.star-toggle--starred{ color: var(--cobalt); }

/* -- #thread-photos: the per-photo "show on the progress page" check,
   own small variant of .check-field (pkg 7's approvals_on/stripe_enabled
   recipe) scaled down to fit a 90px-wide tile -- same accent-color
   checkbox, smaller type. -- */
.photo-public-form{ margin-top: 2px; }
.photo-public-check{ display: flex; align-items: flex-start; gap: var(--space-1); font-size: 11px; font-weight: 500; color: var(--text-faint); line-height: 1.25; }
.photo-public-check input{ flex-shrink: 0; width: 13px; height: 13px; margin-top: 1px; accent-color: var(--cobalt); }

/* -- #thread-progress: the share-link reveal -- byte-for-byte the same
   recipe as .quote-link-reveal/.billing-link-reveal, own class family. -- */
.progress-link-reveal{ margin-top: 4px; }
.progress-link-label{ font-size: 12.5px; font-weight: 500; color: var(--text-secondary); margin: 0 0 8px; }
.progress-link-value{
  display: block;
  width: 100%;
  font-size: 12.5px;
  font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
  color: var(--text-secondary);
  background: var(--app-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-control);
  padding: 10px 12px;
  overflow-wrap: anywhere;
}
.progress-link-actions{ display: flex; flex-wrap: wrap; gap: 10px; }
.progress-link-action{ font-size: 12.5px; font-weight: 600; color: var(--cobalt); }

/* -- #thread-progress: Enable / Revoke -- at most one is ever valid for
   the current state, same "one control at a time" row as .quote-actions/
   .billing-actions. -- */
.progress-actions{ margin-top: 16px; }
.progress-action-form{ flex-shrink: 0; }

/* ==========================================================================
   ---------- p2-6 ----------
   Activity feed (Builder B-SURFACE): office_activity.html's day-grouped
   ledger + the office_index.html Activity card. Reuses existing components
   verbatim wherever the house library already has the shape: .filter-row/
   .filter-chip (the pkg-4 chip idiom) for All/Money/Schedule/Crew, .chip/
   .chip--neutral for the per-row kind word, .card.empty for the §6.0 empty
   state, .drawer-card for the office-index entry -- nothing new for any
   of those. Two small pieces below, on purpose: the day-grouped row list
   (own class family, deliberately NOT sharing job_thread.html's
   .timeline-* classes even though the shape rhymes -- this is a cross-
   job ledger, not one job's own log, and the two should be free to drift
   independently) and the quiet "Older" paging link. A ledger, not a
   social feed: no avatars, no icons, dividers only. Zero '$' in any rule
   here (dollars, where they appear, live inside pre-composed .words
   sentences, never a styled token of their own). Tokens only, checked
   at 320px.
   ========================================================================== */

/* -- day groups: the house date-word head (same "%A, %B %-d" words the
   log timeline/print summary already use, just not the same CSS class
   family -- see the file-level note above) over a plain divided row
   list, quieter than a .card table on purpose. -- */
.activity-days{ display: flex; flex-direction: column; gap: var(--space-5); }
.activity-day-head{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); margin: 0 0 4px; }
.activity-day-rows{ display: flex; flex-direction: column; }

/* -- one row: a pre-composed sentence + a small kind chip + an optional
   time word. Rows are plain divided lines (border-top rhythm), not
   cards -- calm density for a feed that can run long. A job-linked row
   is an <a>; a non-job-linked row (an import completed, a pattern
   stamped) is the same look on a plain <div> -- .activity-row--static
   only turns the pointer/hover affordance off, everything else matches
   so the eye never has to learn two row shapes. -- */
.activity-row{
  display: block;
  padding: 12px 0;
  border-top: 1px solid var(--border);
  color: inherit;
  text-decoration: none;
}
.activity-day-rows > :first-child{ border-top: none; }
.activity-row--static{ cursor: default; }
.activity-row-words{ margin: 0 0 6px; font-size: 15px; font-weight: 500; color: var(--text-primary); }
a.activity-row:hover .activity-row-words{ color: var(--cobalt); }
.activity-row-meta{ display: flex; align-items: center; gap: 8px; }
.activity-row-time{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); }

/* -- the quiet "Older" link: one centered plain-text link, no button
   chrome -- pulling more history is a minor, occasional act, not a
   primary action. -- */
.activity-older{ text-align: center; margin-top: 24px; }
.activity-older a{ font-size: 12.5px; font-weight: 600; color: var(--cobalt); text-decoration: none; }
.activity-older a:hover{ text-decoration: underline; }

/* ==========================================================================
   ---------- p2-7 ----------
   Reminders (Builder B-SURFACE): office_reminders.html's kind-grouped
   due list + handled-today list, plus the office_index.html Reminders
   card. Own class family (.reminder-*), reusing existing components
   verbatim wherever the house library already has the shape: .card/
   .section-head for each kind group and the handled-today block,
   .card.empty for the 6.0 "Nothing due today." state, .chip/.chip--done/
   .chip--neutral for the handled state word, .drawer-card for the
   office-index entry -- nothing new for any of those. Calm on purpose:
   reminders are chores, not alarms, so nothing here uses red/amber or
   any weight heavier than the rest of the Office drawer -- nothing here
   is even styled the WEIGHT of a warning; both Send and Dismiss read as
   quiet text-links (same idiom as .quote-link-action/.activity-older a),
   not filled .btn chrome, because tapping either is a fast, low-stakes
   dismissal of one line item, not a decision. Checked at 320px.
   ========================================================================== */

/* -- one kind group's row list; rows are quiet bordered cards (own
   look, not .bill-row-link's hover-highlight card -- these rows carry
   two independent forms each, so unlike a bill row this is never
   itself a link). -- */
.reminder-groups{ display: flex; flex-direction: column; gap: 20px; }
.reminder-row-list{ display: flex; flex-direction: column; gap: var(--space-4); }
.reminder-row{
  background: var(--app-bg);
  border-radius: var(--radius-surface);
  padding: 14px 16px;
  min-width: 0;
}
.reminder-row-head{ display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; margin-bottom: 8px; }
.reminder-row-customer{ font-size: 15px; font-weight: 600; color: var(--slate-deep); overflow-wrap: anywhere; }
.reminder-row-job{ font-size: 12.5px; font-weight: 600; color: var(--text-secondary); text-decoration: none; overflow-wrap: anywhere; }
.reminder-row-job:hover{ color: var(--cobalt); }

/* -- the draft preview: same quiet blockquote recipe as .lead-detail
   (own class, own file section -- see that rule's own note on why each
   quiet-quote block stays free to drift independently). -- */
.reminder-draft{
  font-size: 14px;
  font-style: normal;
  color: var(--text-secondary);
  background: var(--white);
  border-radius: var(--radius-control);
  padding: 12px 14px;
  margin: 0 0 10px;
  overflow-wrap: anywhere;
}

.reminder-row-actions{ display: flex; flex-wrap: wrap; align-items: center; gap: 16px; }
.reminder-action-form{ display: contents; }
.reminder-send-btn, .reminder-dismiss-btn, .reminder-marksent-btn{
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
}
.reminder-send-btn{ color: var(--cobalt); }
.reminder-send-btn:hover{ text-decoration: underline; }
/* R0-S4: Mark sent gets the SAME green the 'Sent' chip below already uses
   (--mist-text/--mist-bg, chip--done) -- a quiet visual tie between "tap
   this" and "the state it produces", still text-link weight (never filled
   .btn chrome) like Send/Dismiss beside it. */
.reminder-marksent-btn{ color: var(--mist-text); }
.reminder-marksent-btn:hover{ text-decoration: underline; }
.reminder-dismiss-btn{ color: var(--text-faint); }
.reminder-dismiss-btn:hover{ color: var(--text-secondary); text-decoration: underline; }
.reminder-missing{ font-size: 12.5px; color: var(--text-faint); margin: 0; }

/* -- handled-today: a quieter list below the due groups -- same card-row
   recipe as .bill-row-list/.bill-row-link, own class family since these
   rows carry a state chip instead of a money figure. -- */
.reminder-handled-card{ margin-top: 20px; }
.reminder-handled-list{ display: flex; flex-direction: column; gap: 10px; }
.reminder-handled-row{
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-surface);
  padding: 12px 16px;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}
.reminder-handled-row:hover{ border-color: var(--cobalt); }
/* R1-T3 note: .reminder-handled-main moved from the OUTER <a> onto its
   own inner <a> so a dismissed row's Undo <form> (a real, CSRF-protected
   POST -- MISSING #2) can sit beside it without nesting a form inside an
   anchor, which is invalid HTML. color/text-decoration are new here for
   that reason; harmless on the row's own outer <div> too. */
.reminder-handled-main{ min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; color: inherit; text-decoration: none; }
.reminder-handled-customer{ font-size: 14px; font-weight: 600; color: var(--slate-deep); overflow-wrap: anywhere; }
.reminder-handled-job{ font-size: 12.5px; color: var(--text-secondary); overflow-wrap: anywhere; }
.reminder-handled-side{ display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.reminder-handled-time{ font-size: 12.5px; color: var(--text-secondary); }
.reminder-undo-form{ flex-shrink: 0; }
.reminder-undo-btn{
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--cobalt);
  cursor: pointer;
}
.reminder-undo-btn:hover{ text-decoration: underline; }

/* ==========================================================================
   ---------- p2-8 ----------
   Your-data export (Builder B-SURFACE): office_export.html's calm
   one-button page + the office_index.html "Your data" card. Almost
   nothing new -- .page-head/.page-sub, .card, .btn/.btn--primary,
   .form-error/.form-note, and .drawer-card/.drawer-card-desc all carry
   this page verbatim from the existing library. The only new rules below
   center the card's own content and give the lede sentence + button room
   to read as the page's one calm action (contract: "one cobalt button"),
   not a form-stack field. Checked at 320px.
   ========================================================================== */
.export-card{
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-4);
  max-width: 460px;
  margin: 0 auto;
  padding: 32px 24px;
}
.export-lede{ font-size: 15px; color: var(--text-secondary); margin: 0; max-width: 40ch; }
.export-card .btn{ padding: 13px 28px; }
.export-card .form-note{ margin: 0; }

/* ==========================================================================
   ---------- RV3 W2 -- crew home rebuild (contract SCOPE-IN W2 1-7) ----------
   mockups/crew-v3.html + crew-v4.html's round-4 deltas, Amendment A #1
   (WHITE worker surfaces -- no dark card anywhere), #3 (selected-card
   outline), #4 (log viewer order), #6 (green Done/Done-adding). Reuses
   every existing token/chip/card/btn primitive above -- .chip--scheduled
   and .chip--next/.chip--done were already sitting here UNWIRED for
   exactly this (see their own R2-U4 comment) rather than needing new
   color rules. Everything below is additive; nothing above this block
   was touched.
   ========================================================================== */

/* -- the active visit card (white per Amendment A #1 -- logging-card-
   colors.html's ".light" option, the one Karissa picked) -- */
.active-card{
  background: var(--white);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-surface);
  box-shadow: 0 2px 10px rgba(23,32,48,.07);
  padding: 16px;
  margin-bottom: 24px;
}
.active-card-tag{ font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--cobalt); }
.active-card-titlerow{ display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-top: 4px; }
.active-card-titlerow h2{ flex: 1; min-width: 0; font-size: 17px; font-weight: 700; color: var(--slate-deep); margin: 0; overflow-wrap: anywhere; }
.active-card-titlerow h2 .stop-number{ font-size: 12px; font-weight: 600; color: var(--text-faint); margin-left: 6px; }
.view-job-link{
  flex-shrink: 0; font-size: 11px; font-weight: 700; color: var(--cobalt);
  background: var(--cobalt-tint); border: 1px solid #C6D4F7; border-radius: 999px;
  padding: 5px 11px; text-decoration: none; white-space: nowrap; margin-top: 2px;
}
.active-card-sub{ font-size: 13px; color: var(--text-secondary); margin: 3px 0 0; overflow-wrap: anywhere; }
.active-card .stop-instructions--visit{ margin-top: 9px; }
.measure-bar{ margin: 12px 0 2px; }
.measure-bar .lbl{ display: flex; justify-content: space-between; font-size: 12.5px; color: var(--text-secondary); margin-bottom: 4px; }
.measure-bar .lbl b{ color: var(--slate-deep); font-weight: 700; }
.measure-bar .trk{ height: 7px; border-radius: 999px; background: var(--app-bg); overflow: hidden; }
.measure-bar .trk span{ display: block; height: 100%; background: var(--cobalt); }

/* -- RV4 W3: the crew-v4 middle -- cadence line, the billing-sheet bars,
   the day goal, the role line. Values lifted from crew-v4.html's own ROUND 9
   white-mode override block (mock lines 1200-1205) so the worker card matches
   what Karissa approved: dark ink on white, cobalt accents. .job-bar is the
   multi-part bar (one row per billable part -- yardage, manholes, laterals);
   .measure-bar above stays for a legacy single-measure job with no job_parts
   rows yet. All of it is absent on the many jobs that need no measure. -- */
.cadence-line{ margin: 6px 0 0; font-size: 12.5px; color: var(--cobalt); font-weight: 600; }
.day-goal-line{ margin: 6px 0 0; font-size: 12.5px; color: var(--slate-deep); font-weight: 600; }
.role-line{ margin: 8px 0 0; font-size: 12.5px; color: var(--text-secondary); }
.job-bars{ margin: 8px 0 2px; display: flex; flex-direction: column; gap: 6px; }
.job-bar{ display: flex; align-items: center; gap: 8px; font-size: 11.5px; color: #333E52; }
.job-bar .lbl{
  width: 76px; flex-shrink: 0; font-weight: 700; color: #5A6B85;
  text-transform: uppercase; letter-spacing: .04em; font-size: 10px;
}
.job-bar .trk{ flex: 1; height: 5px; border-radius: 3px; background: #E4E9F1; overflow: hidden; }
.job-bar .trk span{ display: block; height: 100%; border-radius: 3px; background: var(--cobalt); }
.job-bar .val{ white-space: nowrap; font-weight: 600; }
/* RV4 W3: the equipment detail list on a stop ("Bobcat S76 · shop → Chase
   Farm Rd"). Values from crew-v4.html's .cw-detaillist (mock lines 156-159),
   white-mode. Dashed top rule separates it from the title above. */
.detail-list{ margin-top: 9px; padding-top: 8px; border-top: 1px dashed var(--border); display: flex; flex-direction: column; gap: 5px; }
.detail-list .dl-row{ display: flex; justify-content: space-between; gap: 10px; font-size: 12px; color: var(--text-secondary); }
.detail-list .dl-item{ font-weight: 700; color: var(--text-primary); }
.detail-list .dl-route{ color: var(--text-faint); text-align: right; white-space: nowrap; }
.active-card-entries{
  margin: 12px 0 0; padding-left: 10px; border-left: 2px solid var(--cobalt-tint);
  display: flex; flex-direction: column; gap: 7px;
}
.active-card-entries .ac-entry{ display: flex; align-items: baseline; gap: 8px; font-size: 12.5px; color: var(--text-secondary); flex-wrap: wrap; cursor: pointer; }
.active-card-entries .ac-entry time{ color: var(--cobalt); font-weight: 700; flex-shrink: 0; }
.active-card-entries .ac-entry .ac-etxt{ flex: 1; min-width: 100px; }
.active-card-acts{ display: flex; gap: 8px; margin-top: 14px; }
.active-card-acts button{
  flex: 1; font-size: 13px; font-weight: 700; border: 1px solid var(--border); border-radius: var(--radius-control);
  padding: 10px 0; background: var(--app-bg); color: var(--slate-deep); font-family: inherit; cursor: pointer;
}
.active-card-acts button.is-primary{ background: var(--cobalt); border-color: var(--cobalt); color: var(--white); }
.done-btn{
  display: block; width: 100%; margin-top: 10px; padding: 13px 0; border: 0; border-radius: var(--radius-control);
  background: var(--mist-text); color: var(--white); font-weight: 800; font-size: 14.5px; font-family: inherit;
  text-align: center; cursor: pointer;
}

/* -- selected list card (Amendment A #3) -- */
.stop-card.is-active{ outline: 2px solid var(--cobalt); outline-offset: 2px; transform: scale(1.02); transform-origin: center; }
.stop-card--tappable{ cursor: pointer; display: block; text-decoration: none; }
.stop-card--done-slim{ cursor: default; }
/* RV4 W3: the done card is a flex ROW, so the note has to break to its own
   line -- flex-basis:100% + wrap on the parent. Without this the note would
   squeeze in beside the chip and truncate. */
.stop-card--done-slim{ flex-wrap: wrap; }
.stop-card--done-note{ font-size: 12.5px; color: var(--text-secondary); margin: 6px 0 0; flex: 0 0 100%; }

/* -- Logged today / logged-for-a-date panel -- */
.logged-panel{ padding: 4px 16px; }
.logged-row{
  display: flex; flex-wrap: wrap; align-items: center; gap: 5px 9px; font-size: 12.5px; color: var(--text-secondary);
  padding: 10px 0; border-top: 1px solid var(--app-bg); cursor: pointer;
}
.logged-row:first-child{ border-top: 0; }
.logged-row time{ color: var(--text-primary); font-weight: 700; white-space: nowrap; flex-shrink: 0; }
.logged-row .logged-txt{ flex: 1; min-width: 120px; }
.logged-row .logged-photos{ color: var(--text-faint); white-space: nowrap; }
.logged-panel .empty-block{ padding: 12px 0; }

/* -- bottom sheet overlay (New log / Done / maps chooser) -- */
.sheet-overlay{
  position: fixed; inset: 0; background: rgba(15,23,42,.52); display: flex; align-items: flex-end;
  justify-content: center; z-index: 50;
}
.sheet-overlay[hidden]{ display: none; }
.sheet{
  width: 100%; max-width: 480px; background: var(--white); border-radius: 18px 18px 0 0;
  padding: 18px 18px 22px; box-shadow: 0 -8px 30px rgba(32,40,50,.25); max-height: 92vh; overflow-y: auto;
}
.sheet h3{ font-size: 16px; margin: 0 0 2px; color: var(--slate-deep); }
.sheet .sheet-sub{ font-size: 12.5px; color: var(--text-faint); margin: 0 0 14px; }
.seg{ display: flex; gap: 6px; margin-bottom: 14px; }
.seg button{
  flex: 1; padding: 10px 0; border-radius: var(--radius-control); border: 1px solid var(--border);
  background: var(--white); font-size: 13px; font-weight: 700; color: var(--text-secondary); font-family: inherit; cursor: pointer;
}
.seg button.on{ background: var(--slate); border-color: var(--slate); color: var(--white); }
.sheet-note{
  width: 100%; min-height: 56px; border: 1px solid var(--border); border-radius: var(--radius-control);
  padding: 10px 12px; font-size: 13.5px; color: var(--text-primary); margin-bottom: 14px; font-family: inherit; resize: vertical;
}
.sheet-photobtns{ display: flex; gap: 8px; margin-bottom: 14px; }
.sheet-photobtns button{
  flex: 1; border: 1px dashed var(--border); background: var(--app-bg); border-radius: var(--radius-control);
  padding: 14px 8px; font-size: 12.5px; font-weight: 700; color: var(--text-secondary); font-family: inherit; cursor: pointer;
}
.sheet-thumbs{ display: grid; grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); gap: 8px; margin-bottom: 10px; }
.sheet-thumb{ aspect-ratio: 4/3; border-radius: 7px; overflow: hidden; position: relative; background: var(--app-bg); }
.sheet-thumb img{ width: 100%; height: 100%; object-fit: cover; display: block; }
.sheet-thumb i{ position: absolute; left: 5px; bottom: 5px; font-style: normal; font-size: 10px; background: rgba(35,44,54,.75); color: var(--white); border-radius: 4px; padding: 1px 6px; }
.sheet-send{
  width: 100%; padding: 13px 0; border: 0; border-radius: var(--radius-control); background: var(--cobalt); color: var(--white);
  font-weight: 800; font-size: 14.5px; font-family: inherit; cursor: pointer;
}
.sheet-send:disabled{ background: var(--slate-chip-bg); color: var(--text-faint); cursor: default; }
.sheet-send--green{ background: var(--mist-text); }
.sheet-close{
  width: 100%; padding: 13px 0; border: 0; border-radius: var(--radius-control); background: var(--slate); color: var(--white);
  font-weight: 800; font-size: 14px; font-family: inherit; cursor: pointer; margin-top: 16px;
}
.sheet-signal-chip{ display: block; background: var(--slate-chip-bg); color: var(--slate-chip-text); font-size: 12px; font-weight: 700; padding: 9px 11px; border-radius: var(--radius-control); margin-bottom: 14px; }

/* -- Done sheet's amount row -- */
/* RV4 W3: the Done-sheet bar picker (multi-bar jobs only). Same shell as
   the amount row so the two read as one block. */
.done-barrow{
  display: flex; align-items: center; justify-content: space-between; gap: 10px; border: 1px solid var(--border);
  border-radius: var(--radius-control); padding: 9px 12px; background: var(--app-bg); margin-bottom: 10px;
}
.done-barrow label{ font-size: 13px; font-weight: 700; color: var(--text-primary); }
.done-barrow select{
  font-family: inherit; font-size: 14px; font-weight: 600; color: var(--text-primary);
  border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; background: var(--white); max-width: 60%;
}
.done-amtrow{
  display: flex; align-items: center; justify-content: space-between; gap: 10px; border: 1px solid var(--border);
  border-radius: var(--radius-control); padding: 9px 12px; background: var(--app-bg); margin-bottom: 14px;
}
.done-amtrow label{ font-size: 13px; font-weight: 700; color: var(--text-primary); }
.done-amtinput{ display: flex; align-items: baseline; gap: 6px; }
.done-amtinput input{
  width: 60px; text-align: center; font-size: 15px; font-weight: 800; border: 1px solid var(--border);
  border-radius: 6px; padding: 6px 4px; font-family: inherit; color: var(--text-primary); background: var(--white);
}
.done-amtinput .unit{ font-size: 11px; font-weight: 700; color: var(--text-secondary); }
.sheet-label{ display: block; font-size: 12.5px; font-weight: 700; color: var(--text-secondary); margin-bottom: 6px; }
.done-photos-head{ font-size: 12px; font-weight: 700; color: var(--text-secondary); margin-bottom: 8px; }
.done-photos-head .hint{ font-weight: 500; color: var(--text-faint); }

/* -- undo toast (Done -> 15s undo) -- */
.undo-toast{
  position: fixed; left: 50%; transform: translateX(-50%); bottom: 88px; background: var(--slate-deep); color: var(--white);
  font-size: 12.5px; font-weight: 600; padding: 10px 16px; border-radius: 999px; box-shadow: 0 6px 18px rgba(0,0,0,.28);
  display: flex; gap: 10px; align-items: center; z-index: 60; white-space: nowrap;
}
.undo-toast[hidden]{ display: none; }
.undo-toast button{ background: none; border: none; padding: 0; color: #8FB3FF; font-weight: 700; font-family: inherit; cursor: pointer; }

/* -- full-screen log viewer (Amendment A #4: header+Close, then note/
   who/when/tag, photos BELOW) -- */
.viewer-overlay{ position: fixed; inset: 0; background: var(--white); z-index: 55; overflow-y: auto; }
.viewer-overlay[hidden]{ display: none; }
.viewer{ padding: 16px 16px 24px; max-width: 560px; margin: 0 auto; }
.viewer-top{ display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 14px; }
.viewer-kicker{ font-size: 11px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-faint); margin: 0; }
.viewer-closebtn{
  flex-shrink: 0; padding: 8px 18px; border: 0; border-radius: var(--radius-control); background: var(--slate);
  color: var(--white); font-weight: 700; font-size: 13px; font-family: inherit; cursor: pointer;
}
.viewer-note{
  background: var(--white); border: 1px solid var(--border); border-radius: var(--radius-surface);
  padding: 12px 14px; font-size: 13.5px; color: var(--text-primary); line-height: 1.5; margin: 4px 0 16px;
}
.viewer-meta{ font-size: 13px; color: var(--text-secondary); margin-bottom: 6px; }
.viewer-meta b{ color: var(--text-primary); }
.viewer-status{ display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--text-secondary); margin-bottom: 16px; }
.viewer-lock{
  display: flex; gap: 8px; align-items: flex-start; background: var(--slate-chip-bg); color: var(--slate-chip-text);
  font-size: 12px; font-weight: 600; padding: 10px 12px; border-radius: var(--radius-control); margin-bottom: 18px; line-height: 1.45;
}
.viewer-photos{ display: flex; flex-direction: column; gap: 10px; }
.viewer-photo{ border-radius: var(--radius-surface); overflow: hidden; position: relative; background: var(--app-bg); }
.viewer-photo img{ width: 100%; display: block; }
.viewer-photo i{ position: absolute; left: 8px; bottom: 8px; font-style: normal; font-size: 11px; background: rgba(35,44,54,.75); color: var(--white); border-radius: 4px; padding: 2px 7px; }

/* -- maps chooser rows -- */
.map-row{ display: flex; align-items: center; gap: 11px; padding: 12px 2px; border-top: 1px solid var(--app-bg); font-size: 14.5px; font-weight: 700; color: var(--text-primary); cursor: pointer; }
.map-row:first-of-type{ border-top: 0; }
.map-icon{ width: 32px; height: 32px; border-radius: 9px; background: var(--app-bg); display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
.map-remember{ display: flex; align-items: flex-start; gap: 9px; font-size: 12.5px; color: var(--text-secondary); margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--app-bg); }
.map-remember input{ margin-top: 2px; }

/* -- locked/unlocked past-day banners -- */
.lock-banner{
  margin: 0 0 16px; background: var(--slate-chip-bg); color: var(--slate-chip-text); font-size: 12.5px; font-weight: 600;
  padding: 10px 12px; border-radius: var(--radius-control); display: flex; gap: 8px; align-items: center;
}
.unlock-banner{
  margin: 0 0 16px; background: var(--cobalt-tint); color: var(--cobalt); font-size: 12.5px; font-weight: 700;
  padding: 10px 12px; border-radius: var(--radius-control); display: flex; gap: 8px; align-items: center;
}
.reopen-acts{ display: flex; gap: 8px; margin-top: 9px; }
.reopen-btn{
  background: var(--cobalt); color: var(--white); border: 0; border-radius: var(--radius-control); padding: 9px 14px;
  font-weight: 700; font-size: 12.5px; font-family: inherit; cursor: pointer;
}
.reopen-btn--ghost{ background: #EDF0F5; color: #28324A; }
.done-adding-btn{
  display: block; width: 78%; margin: 18px auto 0; padding: 15px 0; border: 0; border-radius: 10px;
  background: var(--mist-text); color: var(--white); font-weight: 800; font-size: 16px; font-family: inherit;
  text-align: center; cursor: pointer; box-shadow: 0 3px 10px rgba(31,122,77,.35);
}

/* -- all-caught-up card (S13) -- */
.alldone-card{ margin: 0 0 24px; background: var(--mist-bg); border-radius: var(--radius-surface); padding: 18px 16px; text-align: center; }
.alldone-card .tag2{ font-size: 11px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--mist-text); }
.alldone-card h2{ font-size: 17px; color: var(--mist-text); margin: 6px 0 0; }
.alldone-card .sub2{ font-size: 12.5px; color: #2f7a53; margin: 3px 0 0; }
