:root{
  --bg:#0e1116; --panel:#151a22; --panel2:#1b2129; --border:#262e39;
  --text:#e7ebf0; --muted:#8b95a5; --accent:#3e8bff; --accent2:#63d29a;
  --danger:#e05a5a; --radius:10px;

  /* Field labels used to be --muted, the same grey as help text and disabled captions. In a
     dense form everything then reads as one wall of grey and you cannot see where one box
     stops and the next starts. Labels get their own, brighter tone. */
  --label:#c3ccd8;
  /* Sunk below the card it sits on, with an edge bright enough to read as an edge. Set too
     close to the panel colour, a row of inputs looks like one continuous grey block. */
  --field-bg:#0b0f15;
  --field-border:#3d4857;

  --active-bg:#1c2836; --active-border:#2c4f7c;
  --notice-bg:#1c2836; --notice-border:#2c4f7c; --notice-text:#bcd4f5;
  --warn-bg:#2c2016; --warn-border:#7c5a1f; --warn-text:#f0cd8a;
  --good-bg:#152a1e; --good-border:#1f6b3c; --good-text:#9fe0b7;
  --shadow:rgba(0,0,0,.35);
}

/* Light theme. Set on <html> by the server from a cookie, so the page arrives already the
   right way round -- switching it in JavaScript after load gives a flash of the wrong theme
   on every single navigation. */
:root[data-theme="light"]{
  --bg:#f4f6f9; --panel:#ffffff; --panel2:#f0f3f7; --border:#d4dae2;
  --text:#1a1f28; --muted:#5f6b7a; --accent:#1f6fb2; --accent2:#1b7a3d;
  --danger:#c0392b;

  --label:#2f3945;
  --field-bg:#ffffff;
  --field-border:#b9c2cd;

  --active-bg:#e3edf9; --active-border:#9dc2e8;
  --notice-bg:#e8f1fb; --notice-border:#a8c8ea; --notice-text:#1c4a76;
  --warn-bg:#fdf4e3; --warn-border:#d9b661; --warn-text:#6b4e00;
  --good-bg:#e8f6ed; --good-border:#8fc9a6; --good-text:#17612f;
  --shadow:rgba(15,25,40,.10);
}
*{box-sizing:border-box;}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  background:var(--bg);color:var(--text);font-size:14px;}
a{color:inherit;text-decoration:none;}
.shell{display:flex;min-height:100vh;}
.sidebar{width:220px;flex-shrink:0;background:var(--panel);border-right:1px solid var(--border);
  display:flex;flex-direction:column;padding:18px 12px;position:sticky;top:0;height:100vh;}
.brand{display:flex;align-items:center;gap:10px;padding:6px 8px 20px;}
.brand-mark{width:34px;height:34px;border-radius:8px;background:linear-gradient(135deg,var(--accent),#7c5cff);
  display:flex;align-items:center;justify-content:center;font-weight:700;font-size:13px;}
.brand-name{font-weight:600;font-size:14px;}
.brand-sub{font-size:11px;color:var(--muted);}
/* The sidebar is pinned to 100vh, so the link list must scroll on its own or anything past
   the fold is simply unreachable. On a phone that cut off everything after Compliance --
   Alerts, Owner's Report, Ask AI, Audit Log, Users, Settings. min-height:0 is the part that
   is easy to miss: without it a flex child refuses to shrink below its content height and
   overflow-y never engages. The brand stays pinned above and the footer below; only the
   links scroll. */
.nav{display:flex;flex-direction:column;gap:2px;flex:1;min-height:0;overflow-y:auto;
  -webkit-overflow-scrolling:touch;scrollbar-width:thin;}
.navlink{padding:9px 10px;border-radius:8px;color:var(--muted);font-size:13.5px;font-weight:500;
  display:flex;align-items:center;justify-content:space-between;}
.navlink:hover{background:var(--panel2);color:var(--text);}
.navlink.active{background:var(--active-bg);color:var(--accent);}
.nav-count{background:var(--danger);color:#fff;border-radius:999px;font-size:10px;padding:1px 6px;font-weight:700;}
.sidebar-footer{display:flex;flex-direction:column;gap:2px;border-top:1px solid var(--border);padding-top:10px;margin-top:10px;}
.sidebar-footer .navlink{color:var(--accent2);font-size:12.5px;}
.content{flex:1;display:flex;flex-direction:column;min-width:0;}
.topbar{padding:22px 32px 0;}
.topbar h1{margin:0 0 18px;font-size:22px;font-weight:700;}
.page{padding:0 32px 40px;display:flex;flex-direction:column;gap:20px;}
.card{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;}
.card-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--border);}
.card-head h2{margin:0;font-size:15px;font-weight:600;}
/* The title shares a flex row with the card's action buttons. Without this the title is the
   part that gives way, and a card called "Invoice INV-J1-1" was broken mid-number. Now the row
   wraps onto a second line instead, which is the right thing to lose on a narrow screen. */
.card-head{flex-wrap:wrap;gap:8px;}
.card-head h2{overflow-wrap:break-word;min-width:0;}
.card-body{padding:16px 18px;}
.card-actions{display:flex;gap:8px;}
.table{width:100%;border-collapse:collapse;font-size:13.5px;}
.table th{text-align:left;color:var(--muted);font-weight:600;font-size:11.5px;text-transform:uppercase;
  letter-spacing:.04em;padding:8px 10px;border-bottom:1px solid var(--border);}
.table td{padding:10px;border-bottom:1px solid var(--border);vertical-align:top;}
.table tr:last-child td{border-bottom:none;}
.table tr:hover td{background:var(--panel2);}
.empty{color:var(--muted);font-size:13px;padding:6px 0;}
.badge{display:inline-block;padding:2px 9px;border-radius:999px;font-size:11.5px;font-weight:600;white-space:nowrap;}
.btn{display:inline-flex;align-items:center;gap:6px;background:var(--accent);color:#fff;border:none;
  padding:8px 14px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;}
/* A button label is a control, not prose: it says what pressing it does, and it has to be
   readable as one thing. Without this, a button in a squeezed table cell was set one letter per
   line -- "Take payment" occupied seven lines in a 31px column, which is how most of this app
   looked on a phone. */
.btn{white-space:nowrap;}
.btn:hover{opacity:.9;}
.btn.secondary{background:var(--panel2);color:var(--text);border:1px solid var(--border);}
.btn.danger{background:var(--danger);}
.btn.small{padding:5px 10px;font-size:12px;}
.btn.ghost{background:transparent;border:1px solid var(--border);color:var(--muted);}
.grid{display:grid;gap:16px;}
.grid.cols-2{grid-template-columns:1fr 1fr;}
.grid.cols-3{grid-template-columns:repeat(3,1fr);}
.grid.cols-4{grid-template-columns:repeat(4,1fr);}
@media (max-width:900px){.grid.cols-2,.grid.cols-3,.grid.cols-4{grid-template-columns:1fr;}}
.stat{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;}
/* A figure is one thing. "$7,211.80" split after the comma reads as two numbers, and on a
   375px screen that is exactly what was happening in every stat tile. */
.stat .n{font-size:26px;font-weight:700;white-space:nowrap;}
.stat .l{color:var(--muted);font-size:12.5px;margin-top:2px;}
form.formcard{display:flex;flex-direction:column;gap:14px;}
.field{display:flex;flex-direction:column;gap:5px;}
.field label{font-size:12.5px;color:var(--label);font-weight:600;letter-spacing:.01em;}
/* Inputs carry their own background and a stronger edge than the surrounding panel, so each
   box reads as a box rather than merging into the card behind it. */
input[type=text],input[type=email],input[type=number],input[type=date],input[type=password],
select,textarea{background:var(--field-bg);border:1px solid var(--field-border);color:var(--text);
  padding:9px 11px;border-radius:8px;font-size:13.5px;font-family:inherit;width:100%;}
textarea{resize:vertical;min-height:70px;}
/* Fields on one row share the width, but only down to a point: below about 190px each they go
   onto their own line instead of shrinking further. Without the wrap, four fields on a 375px
   phone were 82px each -- a date input and a select that narrow cannot be used, and the helper
   text under them was being broken mid-word. On a wide screen nothing changes, because there is
   room for every field to sit above its basis. */
.row{display:flex;gap:12px;flex-wrap:wrap;}
.row > *{flex:1 1 190px;}
.pillbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:4px;}
.pill{padding:6px 12px;border-radius:999px;background:var(--panel2);border:1px solid var(--border);
  color:var(--muted);font-size:12.5px;font-weight:600;}
.pill.active{background:var(--active-bg);color:var(--accent);border-color:var(--active-border);}
/* One column per stage, however many stages there are. The fixed repeat(7)/repeat(4) this
   replaced was one short for both boards (installs have 8 stages, service calls 5), so the last
   column -- Closed -- wrapped onto a second row under Awaiting Parts. */
.kanban{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(180px,1fr);gap:12px;overflow-x:auto;}
.kcol{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);min-height:120px;}
.kcol-head{padding:10px 12px;border-bottom:1px solid var(--border);font-size:12px;font-weight:700;
  text-transform:uppercase;letter-spacing:.03em;color:var(--muted);}
.kcard{margin:10px;padding:10px;background:var(--panel2);border:1px solid var(--border);border-radius:8px;
  font-size:12.5px;display:flex;flex-direction:column;gap:6px;}
.kcard .t{font-weight:600;font-size:13px;}
.kcard .m{color:var(--muted);font-size:11.5px;}
.notice{padding:12px 16px;border-radius:8px;background:var(--notice-bg);
  border:1px solid var(--notice-border);color:var(--notice-text);font-size:13px;}
.notice.warn{background:var(--warn-bg);border-color:var(--warn-border);color:var(--warn-text);}
.notice.good{background:var(--good-bg);border-color:var(--good-border);color:var(--good-text);}
.section-title{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);font-weight:700;margin:4px 0;}
.muted{color:var(--muted);}
.small{font-size:12px;}
.hr{border:none;border-top:1px solid var(--border);margin:8px 0;}
.breadcrumb{color:var(--muted);font-size:12.5px;margin-bottom:6px;}
.breadcrumb a{color:var(--accent);}
.tag-row{display:flex;gap:6px;flex-wrap:wrap;}

.topbar-row{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;}
.topbar-row h1{margin:0;}
.searchbar{display:flex;gap:8px;flex:1;max-width:460px;min-width:220px;}
.searchbar input{flex:1;}
.rating-box{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:8px;background:var(--panel2);
  border:1px solid var(--border);}
.rating-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;}
.search-group{margin-bottom:4px;}
.search-group h3{font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em;margin:0 0 8px;}

.cal-controls{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-bottom:14px;}
.cal-nav{display:flex;align-items:center;gap:8px;}
.cal-label{font-weight:700;font-size:14px;margin-left:6px;}
/* minmax(0,1fr), not 1fr. A bare 1fr cannot shrink below its widest unbreakable content, so one
   day holding a long job title took 518px of a 1029px calendar and pushed the rest off the right
   edge -- and a job cannot be dragged to a day nobody can scroll to while dragging. Titles are
   truncated in the chip instead (see .jobchip .jc-title). */
.cal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px;}
.cal-headcell{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);
  text-align:center;padding:4px 0;}
.daycell{background:var(--panel);border:1px solid var(--border);border-radius:8px;min-height:110px;padding:6px;
  display:flex;flex-direction:column;gap:4px;min-width:0;}
.cal-grid.week-view .daycell{min-height:260px;}
.daycell.dim{opacity:.4;}
.daycell.is-today{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent) inset;}
.daynum{font-size:12px;font-weight:700;color:var(--muted);padding:2px 4px;}
.daychips{display:flex;flex-direction:column;gap:4px;overflow-y:auto;max-height:220px;}
.jobchip{background:var(--panel2);border:1px solid var(--border);border-left:3px solid var(--accent);
  border-radius:6px;font-size:11.5px;cursor:grab;}
.jobchip.type-service{border-left-color:var(--accent2);}
/* The link fills the chip so the whole thing is a click target, not just the title text.
   The padding lives here rather than on .jobchip for that reason. */
.jobchip .jc-link{display:block;padding:5px 7px;text-decoration:none;color:inherit;}
.jobchip .jc-link:hover{background:var(--panel);border-radius:5px;}
.jobchip .jc-title{font-weight:600;display:block;}
.jobchip .jc-time{color:var(--muted);font-weight:500;}
.jobchip .jc-meta{display:block;color:var(--muted);font-size:10.5px;margin-top:2px;}
/* A chip is a one-line summary of a job. Truncated with an ellipsis it still reads; wrapped
   inside a narrow day cell it does not. */
.jobchip .jc-title,.jobchip .jc-meta{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
/* Same treatment as .table-wrap, and for the same reason: the card clips its own overflow, so
   without a scrolling layer inside it the far side of the calendar is unreachable. */
.cal-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:thin;}
.cal-scroll:focus{outline:2px solid var(--accent);outline-offset:2px;}
.cal-scroll:focus:not(:focus-visible){outline:none;}
.jobchip:active{cursor:grabbing;opacity:.7;}
.tray{display:flex;flex-wrap:wrap;gap:8px;min-height:40px;}
.tray .jobchip{width:220px;}

.photo-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;}
.photo-thumb{background:var(--panel2);border:1px solid var(--border);border-radius:8px;overflow:hidden;}
.photo-thumb img{width:100%;height:110px;object-fit:cover;display:block;}
.pt-caption{padding:6px 8px;font-size:11px;}
/* Signing happens in a full-screen sheet. The page behind it is pinned while it is open, so a
   stroke that wanders off the pad cannot scroll anything -- see signature_block_html. */
.sig-overlay{position:fixed;inset:0;z-index:2000;background:var(--panel);
  display:flex;flex-direction:column;touch-action:none;overscroll-behavior:contain;}
.sig-overlay[hidden]{display:none;}
body.sig-open{overflow:hidden;}
.sig-sheet{display:flex;flex-direction:column;flex:1;padding:12px;gap:10px;min-height:0;}
.sig-sheet-head{display:flex;justify-content:space-between;align-items:center;font-size:15px;}
.sig-canvas-wrap{position:relative;flex:1;min-height:200px;}
.sig-baseline{position:absolute;left:14px;right:14px;bottom:34px;border-top:1px solid #bbb;
  pointer-events:none;color:#999;font-size:18px;}
.sig-baseline span{position:absolute;left:0;top:-13px;}
.sig-sheet-actions{display:flex;gap:10px;}
.sig-sheet-actions .btn{flex:1;padding:14px;font-size:15px;}
.sigpad{background:#fff;border-radius:8px;border:1px solid var(--border);
  width:100%;height:100%;display:block;touch-action:none;overscroll-behavior:contain;
  cursor:crosshair;}
.sig-done{margin-bottom:10px;}
.sig-preview{background:#fff;border-radius:8px;padding:6px;max-width:400px;width:100%;margin-top:8px;}

/* ---------- responsive desktop sidebar ---------- */
.menu-toggle{display:none;background:var(--panel2);border:1px solid var(--border);color:var(--text);
  border-radius:8px;padding:8px 11px;font-size:16px;cursor:pointer;}
@media (max-width: 880px){
  .menu-toggle{display:inline-flex;}
  .sidebar{position:fixed;left:0;top:0;bottom:0;z-index:40;transform:translateX(-100%);
    transition:transform .18s ease;box-shadow:2px 0 12px rgba(0,0,0,.4);}
  .sidebar.open{transform:translateX(0);}
  .content{width:100%;}
  .topbar{padding:16px 16px 0;}
  .page{padding:0 16px 32px;}
  .searchbar{max-width:none;}
  .kanban,.kanban.service{grid-auto-flow:row;grid-auto-columns:auto;grid-template-columns:1fr;}
  .daycell{min-height:88px;padding:5px;}
  .daychips{max-height:120px;}
  .jobchip{font-size:10px;}
}
.sidebar-backdrop{display:none;}
.sidebar-backdrop.show{display:block;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:30;}

/* ---------- mobile tech app ---------- */
.mobile-body{margin:0;background:var(--bg);color:var(--text);}
.m-topbar{position:sticky;top:0;z-index:20;background:var(--panel);border-bottom:1px solid var(--border);
  display:flex;align-items:center;gap:10px;padding:12px 14px;}
.m-topbar h1{flex:1;font-size:16px;margin:0;text-align:center;}
.m-back,.m-home{color:var(--accent);font-size:18px;font-weight:700;padding:4px 8px;flex-shrink:0;}
.m-home{font-size:12px;}
.m-page{padding:14px;display:flex;flex-direction:column;gap:12px;max-width:640px;margin:0 auto;padding-bottom:60px;}
.m-tech-card{display:flex;align-items:center;justify-content:space-between;background:var(--panel);
  border:1px solid var(--border);border-radius:12px;padding:18px;font-size:16px;font-weight:700;}
.m-job-card{background:var(--panel);border:1px solid var(--border);border-left:4px solid var(--accent);
  border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:4px;}
.m-job-card.type-service{border-left-color:var(--accent2);}
.m-job-card .t{font-size:15px;font-weight:700;}
.m-job-card .m2{color:var(--muted);font-size:12.5px;}
.m-section{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);font-weight:700;
  margin:6px 0 -2px;}
.m-btn-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
.m-btn-grid form,.m-btn-grid a{width:100%;}
.m-btn-grid .btn{width:100%;justify-content:center;padding:12px;font-size:13.5px;}
.mobile-body .card{border-radius:12px;}
.mobile-body input,.mobile-body select,.mobile-body textarea{font-size:16px;padding:11px;}
.mobile-body .btn{padding:11px 16px;}
.m-tabs{display:flex;gap:6px;background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:4px;}
.m-tab{flex:1;text-align:center;padding:9px 4px;border-radius:8px;font-size:12.5px;font-weight:600;color:var(--muted);text-decoration:none;}
.m-tab.active{background:var(--accent);color:#fff;}
.m-job-card .m-tech-line{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:8px;padding-top:8px;border-top:1px dashed var(--border);}
.m-job-card .m-reassign{display:flex;gap:6px;margin-top:6px;}
.m-job-card .m-reassign select{flex:1;font-size:13px;padding:7px;}
.m-search-box{width:100%;}
.m-cust-card{display:block;background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:12px 14px;text-decoration:none;color:inherit;}
.m-cust-card .t{font-size:15px;font-weight:700;}
.m-cust-card .m2{color:var(--muted);font-size:12.5px;margin-top:2px;}

/* ---------- search / address suggestion dropdowns ---------- */
.searchbar{position:relative;}
.suggest-box{display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--panel);
  border:1px solid var(--border);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.4);z-index:50;
  max-height:360px;overflow-y:auto;}
.suggest-box.show{display:block;}
.suggest-group{padding:6px 0;}
.suggest-group + .suggest-group{border-top:1px solid var(--border);}
.sg-title{font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);font-weight:700;
  padding:6px 12px 2px;}
.suggest-row{display:flex;justify-content:space-between;gap:10px;padding:8px 12px;font-size:13px;color:var(--text);}
.suggest-row:hover{background:var(--panel2);}
.sr-sub{color:var(--muted);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:45%;}

/* ---------- the parts list while typing an estimate line ----------
   Scoped to the parts box on purpose: the same classes carry the global search dropdown in the
   top bar, and that one is a different shape with different constraints.

   The part name is the thing being read, so it gets the room and wraps rather than being cut
   off. "Cat6 outdoor rated cable (1000ft spool)" and "Cat6 riser rated cable (1000ft spool)"
   are the same string truncated, and picking between them by guessing is the wrong part
   ordered. */
.parts-suggest-box{right:auto;min-width:100%;width:max-content;max-width:min(560px, 88vw);}
.parts-suggest-box .suggest-row{align-items:baseline;}
.parts-suggest-box .sr-label{flex:1;min-width:0;overflow-wrap:break-word;}
.parts-suggest-box .sr-sub{white-space:nowrap;overflow:visible;max-width:none;flex:0 0 auto;}
.addr-suggest-box{max-height:220px;}
.addr-row{cursor:pointer;font-size:12.5px;}
.field{position:relative;}

.card-alert .card{border-color:var(--warn-border);box-shadow:0 0 0 1px var(--warn-border);}
.card-alert .card-head{background:var(--warn-bg);}

/* ---------- customer self-service portal ---------- */
.portal-body{margin:0;background:var(--bg);color:var(--text);min-height:100vh;}
.p-topbar{display:flex;align-items:center;justify-content:space-between;padding:14px 24px;
  border-bottom:1px solid var(--border);background:var(--panel);position:sticky;top:0;z-index:10;}
.p-topbar-right{display:flex;align-items:center;gap:10px;font-size:13px;}
.p-cust{color:var(--muted);}
.p-page{max-width:820px;margin:0 auto;padding:28px 20px 60px;}
.p-login-wrap{max-width:380px;margin:60px auto;}
.p-balance{display:flex;gap:18px;flex-wrap:wrap;margin-bottom:18px;}
.p-balance .stat{flex:1;min-width:140px;}

.plain-table { width: 100%; border-collapse: collapse; }
.plain-table td { border: none; padding: 2px 0; }

/* ---------------------------------------------------------------- Site visit modal
   The rest of the app is dark-themed, but this dialog is deliberately light: it is a focused
   data-entry surface and reads more easily on white. That means every colour inside has to be
   set explicitly -- the global :root text colour is near-white, so anything left to inherit
   would be white-on-white and invisible. The input rules below exist for the same reason:
   without them the global dark input styling bleeds in and looks broken on a white panel. */
.vmodal[hidden] { display: none; }
.vmodal {
  position: fixed; inset: 0; z-index: 900;
  display: flex; align-items: flex-start; justify-content: center;
  padding: 40px 16px; overflow-y: auto;
}
.vmodal-backdrop { position: fixed; inset: 0; background: rgba(5, 8, 12, .62); }
.vmodal-box {
  position: relative; background: #fff; color: #1a1f28;
  border-radius: 10px; width: 100%; max-width: 640px;
  box-shadow: 0 20px 60px rgba(0,0,0,.5);
}
.vmodal-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 15px 20px; border-bottom: 1px solid #e6e9ef; font-size: 15px;
}
.vmodal-x {
  background: none; border: 0; font-size: 24px; line-height: 1; cursor: pointer;
  color: #8a93a0; padding: 0 4px;
}
.vmodal-x:hover { color: #1a1f28; }
.vmodal-body { padding: 20px; background: #fff; }
.vmodal-foot {
  display: flex; justify-content: space-between; gap: 10px;
  padding: 15px 20px; border-top: 1px solid #e6e9ef; background: #fff;
  border-radius: 0 0 10px 10px;
}

/* Every text colour inside the dialog is explicit -- see note above. */
.vmodal, .vmodal label, .vmodal span, .vmodal div, .vmodal strong { color: #1a1f28; }
.vmodal .muted, .vmodal .small.muted { color: #6b7480; }

.vmodal input[type=text], .vmodal input[type=email], .vmodal input[type=number],
.vmodal input[type=date], .vmodal input[type=time], .vmodal input[type=tel],
.vmodal select, .vmodal textarea {
  background: #fff; border: 1px solid #ccd2da; color: #1a1f28;
  padding: 8px 10px; border-radius: 6px; font-size: 14px; width: auto;
}
.vmodal input[type=date], .vmodal input[type=time] { min-width: 130px; }
.vmodal textarea { width: 100%; min-height: 90px; }
.vmodal input:focus, .vmodal select:focus, .vmodal textarea:focus {
  outline: none; border-color: #3e8bff; box-shadow: 0 0 0 2px rgba(62,139,255,.18);
}
.vmodal input[type=checkbox] { width: auto; margin-right: 7px; vertical-align: middle; }

.vrow { display: flex; gap: 18px; align-items: flex-start; margin-bottom: 18px; }
.vrow > label { flex: 0 0 160px; font-weight: 600; padding-top: 8px; font-size: 14px; }
.vrow > div { flex: 1; min-width: 0; }

/* Tech list: one per line with the name beside its box, so it is obvious who is ticked. */
.vtechs {
  border: 1px solid #ccd2da; border-radius: 6px; padding: 4px 0;
  max-height: 170px; overflow-y: auto; background: #fff;
}
.vtechs label {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 12px; cursor: pointer; font-size: 14px; color: #1a1f28;
}
.vtechs label:hover { background: #f1f5fb; }
.vtechs input[type=checkbox] { margin: 0; }

.vmodal .btn { color: #fff; }
.vmodal .btn.secondary { background: #eef1f5; color: #38414d; border: 1px solid #ccd2da; }

@media (max-width: 640px) {
  .vrow { flex-direction: column; gap: 5px; }
  .vrow > label { flex: none; padding-top: 0; }
  .vmodal { padding: 12px; }
}

/* ---------- quick add (top right) ---------- */
/* <details> rather than a scripted menu: it opens without JavaScript, closes on Escape, and
   is keyboard reachable for free. */
.quick-add{position:relative;}
.quick-add summary{list-style:none;cursor:pointer;white-space:nowrap;}
.quick-add summary::-webkit-details-marker{display:none;}
.quick-add-menu{position:absolute;right:0;top:calc(100% + 6px);z-index:50;min-width:190px;
  background:var(--panel2);border:1px solid var(--border);border-radius:10px;padding:6px;
  box-shadow:0 12px 30px rgba(0,0,0,.45);display:flex;flex-direction:column;gap:2px;}
.quick-add-menu a{padding:9px 11px;border-radius:7px;color:var(--text);font-size:13.5px;
  white-space:nowrap;}
.quick-add-menu a:hover{background:var(--panel);color:var(--accent);}
@media (max-width: 880px){
  .quick-add-menu{right:auto;left:0;}
}

/* ---- Document design centre -------------------------------------------------------------
   Controls on the left, a live preview of the real document on the right. Designing against
   a picture is the point; a colour box on its own is guesswork. */
.design-split{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px;align-items:start;}
.design-preview{position:sticky;top:14px;}
.design-preview iframe{width:100%;height:78vh;border:1px solid var(--border);border-radius:8px;
  background:#fff;}
.design-blocks{list-style:none;margin:0;padding:0;}
.design-block{border:1px solid var(--border);border-radius:8px;background:var(--panel2);
  margin-bottom:6px;padding:8px 10px;cursor:grab;}
.design-block.dragging{opacity:.5;border-style:dashed;}
.design-block-head{display:flex;align-items:center;gap:8px;}
.design-block-head label{display:flex;align-items:center;gap:6px;margin:0;}
.drag-grip{color:var(--muted);cursor:grab;user-select:none;font-size:13px;}
.design-block-more summary{cursor:pointer;margin-top:6px;}
.design-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;
  margin-top:8px;}
.design-grid label{display:flex;flex-direction:column;gap:4px;font-size:11.5px;
  color:var(--label);font-weight:600;background:var(--panel);border:1px solid var(--border);
  border-radius:7px;padding:8px 9px;}
.design-grid input,.design-grid select{width:100%;}

@media (max-width:1100px){
  .design-split{grid-template-columns:1fr;}
  .design-preview{position:static;}
  .design-preview iframe{height:60vh;}
}


/* ---- Theme toggle ------------------------------------------------------------------------ */
.theme-toggle{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);
  background:var(--panel2);color:var(--muted);border-radius:8px;padding:6px 10px;font-size:12.5px;
  font-weight:600;white-space:nowrap;cursor:pointer;}
.theme-toggle:hover{color:var(--text);border-color:var(--field-border);}

/* ---- Form legibility --------------------------------------------------------------------- */
/* Section headings inside a card were the same muted grey as everything else, so a long form
   read as one undifferentiated block. */
.section-title{color:var(--label);font-weight:700;}

/* A group of related fields on one row gets a hairline between them, so the eye can see where
   one box ends and the next begins without having to read the labels. */
.row > .field{padding-right:12px;}
.row > .field + .field{border-left:1px solid var(--border);padding-left:12px;}
/* The hairline separates fields sitting side by side. Once they stack it is a line down the
   left of a full-width field, which reads as a quote bar rather than a divider. */
@media (max-width:700px){
  .row > .field{padding-right:0;}
  .row > .field + .field{border-left:none;padding-left:0;}
}

input[type=text]:focus,input[type=email]:focus,input[type=number]:focus,input[type=date]:focus,
input[type=password]:focus,select:focus,textarea:focus{
  outline:none;border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent);}

/* The printable documents are deliberately left on white in both themes -- they are paper. */


/* ---- Status badges ------------------------------------------------------------------------
   The colour used to be written inline on every badge, which meant it could not change with
   the theme. A hue chosen to glow against near-black washes out on near-white: the amber
   badges measured 2.73:1 in light mode. Each status now carries a CSS variable instead, and
   the light theme darkens it. */
.badge{background:color-mix(in srgb, var(--badge-c, var(--muted)) 14%, transparent);
  color:var(--badge-c, var(--muted));
  border:1px solid color-mix(in srgb, var(--badge-c, var(--muted)) 40%, transparent);}
.badge-lead{--badge-c:#7c8797;}
.badge-estimate-sent{--badge-c:#b8860b;}
.badge-won{--badge-c:#1b7a3d;}
.badge-active-customer{--badge-c:#1b7a3d;}
.badge-lost{--badge-c:#a12b2b;}
.badge-draft{--badge-c:#7c8797;}
.badge-sent{--badge-c:#b8860b;}
.badge-signed{--badge-c:#1b7a3d;}
.badge-declined{--badge-c:#a12b2b;}
.badge-awaiting-parts{--badge-c:#a12b2b;}
.badge-parts-ordered{--badge-c:#b8860b;}
.badge-parts-received{--badge-c:#1f6fb2;}
.badge-scheduled{--badge-c:#6a3fb5;}
.badge-in-progress{--badge-c:#b8860b;}
.badge-completed{--badge-c:#1b7a3d;}
.badge-invoiced{--badge-c:#1f6fb2;}
.badge-closed{--badge-c:#555b63;}
.badge-needed{--badge-c:#a12b2b;}
.badge-ordered{--badge-c:#b8860b;}
.badge-received{--badge-c:#1b7a3d;}
.badge-paid{--badge-c:#1b7a3d;}
.badge-overdue{--badge-c:#a12b2b;}
.badge-auto-draft-scheduled{--badge-c:#6a3fb5;}
.badge-active{--badge-c:#1b7a3d;}
.badge-paused{--badge-c:#b8860b;}
.badge-cancelled{--badge-c:#555b63;}
/* Voided invoices read as withdrawn, not as a failure or a warning: grey like cancelled,
   because it is a document taken back rather than something that went wrong. */
.badge-void{--badge-c:#555b63;}
/* Bad debt is a loss, not a cancellation: it reads in the same red as overdue, because that is
   what it is -- an overdue invoice we have stopped expecting. */
.badge-written-off{--badge-c:#a12b2b;}
.badge-bad-debt{--badge-c:#a12b2b;}
.badge-inactive{--badge-c:#555b63;}
.badge-refund{--badge-c:#6a3fb5;}

:root[data-theme="light"] .badge-lead{--badge-c:#525b68;}
:root[data-theme="light"] .badge-estimate-sent{--badge-c:#7d5906;}
:root[data-theme="light"] .badge-won{--badge-c:#12592c;}
:root[data-theme="light"] .badge-active-customer{--badge-c:#12592c;}
:root[data-theme="light"] .badge-lost{--badge-c:#8a2020;}
:root[data-theme="light"] .badge-draft{--badge-c:#525b68;}
:root[data-theme="light"] .badge-sent{--badge-c:#7d5906;}
:root[data-theme="light"] .badge-signed{--badge-c:#12592c;}
:root[data-theme="light"] .badge-declined{--badge-c:#8a2020;}
:root[data-theme="light"] .badge-awaiting-parts{--badge-c:#8a2020;}
:root[data-theme="light"] .badge-parts-ordered{--badge-c:#7d5906;}
:root[data-theme="light"] .badge-parts-received{--badge-c:#155080;}
:root[data-theme="light"] .badge-scheduled{--badge-c:#4f2c92;}
:root[data-theme="light"] .badge-in-progress{--badge-c:#7d5906;}
:root[data-theme="light"] .badge-completed{--badge-c:#12592c;}
:root[data-theme="light"] .badge-invoiced{--badge-c:#155080;}
:root[data-theme="light"] .badge-closed{--badge-c:#414750;}
:root[data-theme="light"] .badge-needed{--badge-c:#8a2020;}
:root[data-theme="light"] .badge-ordered{--badge-c:#7d5906;}
:root[data-theme="light"] .badge-received{--badge-c:#12592c;}
:root[data-theme="light"] .badge-paid{--badge-c:#12592c;}
:root[data-theme="light"] .badge-overdue{--badge-c:#8a2020;}
:root[data-theme="light"] .badge-auto-draft-scheduled{--badge-c:#4f2c92;}
:root[data-theme="light"] .badge-active{--badge-c:#12592c;}
:root[data-theme="light"] .badge-paused{--badge-c:#7d5906;}
:root[data-theme="light"] .badge-cancelled{--badge-c:#414750;}
:root[data-theme="light"] .badge-void{--badge-c:#414750;}
:root[data-theme="light"] .badge-written-off{--badge-c:#8c2020;}
:root[data-theme="light"] .badge-bad-debt{--badge-c:#8c2020;}
:root[data-theme="light"] .badge-inactive{--badge-c:#414750;}
:root[data-theme="light"] .badge-refund{--badge-c:#4f2c92;}

/* ---- Trend charts -------------------------------------------------------------------------
   Bars are divs. No charting library, for the same reason the rest of the app has no
   dependencies: it has to keep working with nothing to install and nothing to update. */
.trend-chart{display:flex;align-items:flex-end;gap:3px;height:150px;margin:16px 0 6px;
  padding:8px 4px 0;background:var(--panel2);border:1px solid var(--border);border-radius:8px;
  overflow-x:auto;}
.tbar{flex:1;min-width:14px;display:flex;flex-direction:column;justify-content:flex-end;
  align-items:center;height:100%;gap:4px;}
.tbar-fill{width:100%;background:linear-gradient(180deg,var(--accent),color-mix(in srgb,var(--accent) 55%,transparent));
  border-radius:3px 3px 0 0;min-height:2px;transition:opacity .12s;}
.tbar:hover .tbar-fill{opacity:.75;}
.tbar-label{font-size:9px;color:var(--muted);white-space:nowrap;}
.grid.cols-4{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;}
@media (max-width:700px){ .trend-chart{height:120px;} .tbar-label{display:none;} }

/* Reorder / remove controls on estimate and preset build forms. */
.item-row-tools{display:flex;gap:2px;align-items:center;}
.item-row-tools .btn{padding:4px 7px;min-width:26px;line-height:1;}
.item-row-tools .btn[disabled]{opacity:.3;cursor:default;}

/* ---------- big content in small boxes ----------
   A card is a fixed-width box with overflow:hidden (it needs that for its rounded corners), so
   anything wider than the card used to be clipped with no way to reach it. On a phone that made
   the right-hand columns of every table -- which is where the action buttons live -- invisible
   and unreachable, not merely awkward. These are the four ways out, by shape of content:
   wide -> scroll sideways, tall -> scroll inside or collapse, long words -> wrap. */

/* Wide. The wrapper scrolls rather than the card, so the corners stay rounded. tabindex makes
   it reachable by keyboard, since a scroll region nobody can focus is its own dead end. */
.table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:thin;}
.table-wrap:focus{outline:2px solid var(--accent);outline-offset:2px;}
.table-wrap:focus:not(:focus-visible){outline:none;}
/* A hint that there is more to the right: a soft shadow that disappears once scrolled to the
   end. Without it a table that scrolls looks identical to one that does not. */
.table-wrap{background:
  linear-gradient(to right, var(--panel) 30%, rgba(0,0,0,0)) left center / 24px 100% no-repeat,
  linear-gradient(to left, var(--panel) 30%, rgba(0,0,0,0)) right center / 24px 100% no-repeat,
  linear-gradient(to right, rgba(0,0,0,.22), rgba(0,0,0,0)) left center / 10px 100% no-repeat,
  linear-gradient(to left, rgba(0,0,0,.22), rgba(0,0,0,0)) right center / 10px 100% no-repeat;
  background-attachment:local, local, scroll, scroll;}

/* Tall. Caps a long list at roughly a screenful and scrolls inside, so one busy customer cannot
   push everything below it off the page. */
.card-scroll{max-height:420px;overflow-y:auto;-webkit-overflow-scrolling:touch;scrollbar-width:thin;}
.card-scroll.short{max-height:260px;}
@media (max-width:880px){ .card-scroll{max-height:300px;} .card-scroll.short{max-height:200px;} }

/* Collapsible. For sections that are usually noise and occasionally the thing you need. */
details.box{border:1px solid var(--border);border-radius:var(--radius);background:var(--panel2);
  margin-top:10px;}
details.box>summary{cursor:pointer;padding:10px 14px;font-size:13px;font-weight:600;
  list-style:none;display:flex;align-items:center;gap:8px;}
details.box>summary::-webkit-details-marker{display:none;}
details.box>summary::before{content:"\25B8";display:inline-block;transition:transform .15s ease;
  color:var(--muted);}
details.box[open]>summary::before{transform:rotate(90deg);}
details.box>summary:hover{background:var(--panel);}
details.box .box-inner{padding:0 14px 12px;}
.box-count{color:var(--muted);font-weight:500;}

/* Long words. An unbroken serial number or a pasted URL used to set the minimum width of the
   whole table and push every other column off the screen. */
/* `anywhere` was the wrong tool here and it broke the whole app on a phone. It is the one
   value of overflow-wrap that counts as a break opportunity when the browser works out a box's
   minimum width, so .table (width:100%) could shrink every column to a single character rather
   than overflow. Measured at 375px: name columns at 29px, "Commercial" set 2.5 letters per
   line, and the sideways-scroll wrapper doing nothing because nothing ever overflowed.

   `break-word` breaks a word only when it genuinely will not fit on a line of its own, and does
   not shrink the minimum width. Long emails and URLs still wrap; ordinary words stop being
   chopped up, the table goes back to being wider than its box, and the wrapper scrolls it. */
.table td{overflow-wrap:break-word;}

/* A cell holding one unbroken token -- an invoice number, a phone number, a date, an amount --
   is a value, not prose. "555-330-1190" split across three lines at its hyphens is unreadable
   and reads as three separate numbers. render.py's table() marks these; prose cells in the same
   table are untouched and still wrap. */
.table td.atomic{white-space:nowrap;}

/* And the other half of the same idea: a cell that is prose needs room to be prose in. An equal
   share of a 375px card gave the Description column on an estimate 93px, narrower than the
   Orderable Part column beside it. Prose cells claim a readable minimum and the table scrolls,
   which is what the wrapper is for.

   Narrow screens only, and that scoping is not cosmetic. Applied everywhere it forced the
   Invoices table to 1512px inside a 1103px box on a 1440px display -- introducing sideways
   scrolling on desktop, where there was no problem to fix and plenty of room to spread into. */
@media (max-width:900px){
  .table td:not(.atomic){min-width:9em;}
  .table td .clamp{min-width:13em;}
}
/* Headings do not break mid-word. Without this the column titles on a narrow screen were set
   one letter per line -- "N U M B E R" down the page -- which pushed the header row of the
   portal's invoice table to 138px tall and made it genuinely unreadable on a phone.

   It also revives the scroll wrapper above, which had been doing nothing on wide tables:
   .table is width:100%, so a table with too many columns squeezed itself to fit instead of
   overflowing, and a wrapper that scrolls content which never overflows scrolls nothing. With
   the headings holding their width the table becomes wider than the box, and the sideways
   scroll that was written for exactly this case starts working. */
.table th{overflow-wrap:normal;word-break:keep-all;white-space:nowrap;}
.card-body p,.card-body div{overflow-wrap:break-word;}
/* Clamp a long free-text cell to a few lines; the full text stays in the title attribute. */
.clamp{display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;-webkit-box-orient:vertical;
  overflow:hidden;}
.clamp-1{-webkit-line-clamp:1;line-clamp:1;}

/* "Showing 20 of 143" footers under a truncated list. */
.more-note{padding:10px 2px 0;font-size:12px;color:var(--muted);
  display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;}

@media (max-width:880px){
  .card-body{padding:14px;}
  .card-head{padding:12px 14px;}
  .table td,.table th{padding:9px 8px;}
}

/* Rows past the preview stay in the DOM (so the columns keep their widths) but are not shown
   until the button is pressed. */
.table-wrap:not(.showing-all) .tr-extra{display:none;}

/* A clamped block that can be opened by clicking it. The cursor and the hover tint are the only
   hint that it does anything, which is enough for text that is obviously cut off mid-sentence. */
.js-expand{cursor:pointer;}
.js-expand:hover{background:var(--panel2);}
.js-expand.open{-webkit-line-clamp:unset;line-clamp:unset;display:block;}

/* Portal navigation. The portal previously had none: every page depended on being linked from
   another one, so anything not on the home page was unreachable. */
.p-nav {
  display: flex;
  gap: 4px;
  padding: 0 18px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
}
.p-nav-link {
  padding: 11px 14px;
  font-size: 14px;
  font-weight: 600;
  color: var(--muted);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  white-space: nowrap;
}
.p-nav-link:hover { color: var(--text); border-bottom-color: var(--border); }

/* ---------- signing page signature pad ----------
   The contract signing page is not the tech's fullscreen overlay, so it cannot reuse
   .sig-canvas-wrap: that is flex:1 inside a flex column and collapses to nothing in an ordinary
   card. This gives the pad its own explicit size. It shipped once with a class name that did not
   exist in this file at all, which meant an unstyled, invisible canvas -- present in the DOM,
   drawable by script, and to a customer simply not there. */
.sign-pad-wrap{position:relative;max-width:520px;}
.sign-pad{
  width:100%;
  height:170px;
  background:#fff;
  border:1px solid #b9c0c9;
  border-radius:10px;
  display:block;
  touch-action:none;          /* so a finger draws instead of scrolling the page */
  cursor:crosshair;
}
.sign-pad-line{
  position:absolute;left:18px;right:18px;bottom:34px;
  border-top:1px solid #c9ced6;pointer-events:none;
}
.sign-pad-line span{position:absolute;left:0;top:-12px;color:#9aa2ac;font-size:16px;}
.sign-pad-hint{color:var(--muted);font-size:12.5px;margin:6px 0 0;}
@media (max-width: 520px){
  .sign-pad{height:150px;}
}

/* Money on the signing page must not wrap. "$4,850.0" on one line and "0" on the next is the
   kind of thing that makes somebody stop and re-read the total of a contract they are about to
   sign, and on a narrow phone it happened to every figure in the table. */
.sign-doc .table td:not(:first-child),
.sign-doc .table th:not(:first-child){white-space:nowrap;}

/* The signature as drawn, shown to staff on the signed contract. White ground because the ink
   is dark: on the dark card it would be invisible, which is the mistake the signing pad itself
   shipped with once. */
.sig-svg{background:#fff;border:1px solid var(--border);border-radius:8px;
  padding:6px;max-width:100%;height:auto;display:block;}

/* Electronic Records & Signature Consent -- step 1 of signing.
   Sized to be read, not skimmed past. The disclosure is the thing that makes the signature
   enforceable, so it is set at normal body size with real headings and real spacing rather than
   the small print that "legal text" usually gets. It scrolls inside its own panel on a phone
   instead of pushing the consent checkbox off the bottom of a very long page. */
.esign-title{font-size:19px;font-weight:700;margin:2px 0 12px;}
.esign-panel{
  border:1px solid var(--border);border-radius:10px;background:var(--panel2);
  padding:18px 20px;max-height:60vh;overflow-y:auto;-webkit-overflow-scrolling:touch;
  font-size:15px;line-height:1.65;
}
.esign-panel:focus{outline:2px solid var(--accent);outline-offset:2px;}
.esign-panel .esign-h{
  font-size:13px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  margin:22px 0 8px;padding-bottom:6px;border-bottom:1px solid var(--border);
}
.esign-panel .esign-h:first-child{margin-top:0;}
.esign-panel .esign-p{margin:0 0 12px;}
.esign-panel .esign-list{margin:0 0 12px;padding-left:22px;}
.esign-panel .esign-list li{margin-bottom:7px;}
/* Outside the panel and visually separate from it: ticking this is an act, not a line of the
   document above. */
.esign-consent{margin-top:18px;padding:16px;border:1px solid var(--border);border-radius:10px;}
.esign-check{display:flex;align-items:flex-start;gap:12px;font-size:15px;line-height:1.55;
  cursor:pointer;}
.esign-check input[type=checkbox]{width:20px;height:20px;flex:0 0 auto;margin-top:2px;cursor:pointer;}
.esign-consent .btn[disabled]{opacity:.45;cursor:not-allowed;}
@media (max-width:880px){
  .esign-panel{max-height:52vh;padding:15px 16px;font-size:15px;}
  .esign-title{font-size:17px;}
}

/* Signed Contracts card on a customer.
   The contract cell holds a reference, two document buttons and a delivery state. Laid out as a
   wrapping flex row rather than <br>-joined blocks: stacking them made the row taller than the
   table's own height and clipped the Certificate button off the bottom. */
.badge-danger{--badge-c:#a12b2b;}
.badge-muted{--badge-c:#7c8797;}
.contract-cell{display:flex;flex-direction:column;gap:7px;min-width:210px;}
.contract-actions{display:flex;flex-wrap:wrap;align-items:center;gap:7px;}
/* On a phone the four columns become one block per contract. A signature, a name, a date and
   two document buttons cannot share 375px: squeezed to fit, the headings wrapped to a single
   letter per line and the signature drew as a dot. The headings go because each value says
   what it is -- a drawn signature, a person's name, a date, a contract number. */
.contracts-table .sig-svg{max-width:100%;height:auto;}
@media (max-width:880px){
  .contracts-table thead{display:none;}
  .contracts-table table,.contracts-table tbody,.contracts-table tr,.contracts-table td{
    display:block;width:auto;}
  .contracts-table tr{padding:14px 0;border-bottom:1px solid var(--border);}
  .contracts-table tr:last-child{border-bottom:none;}
  .contracts-table td{padding:4px 0;border:none;}
  .contracts-table td:empty{display:none;}
  .contracts-table .contract-cell{min-width:0;}
}

/* A customer's signed agreements in the portal. Blocks rather than table rows: five columns of
   number, name, date, reference and two buttons cannot share 375px, and this is read on a
   phone more often than not. */
.agreement{padding:14px 0;border-top:1px solid var(--border);}
.agreement:first-of-type{border-top:none;}
.agreement-head{font-weight:700;font-size:15px;margin-bottom:4px;}
.agreement-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px;}

/* The text-message thread on a customer. A conversation, not a table: four columns of direction,
   timestamp, name and body cannot share 375px, and at that width the headings wrapped to two
   letters a line. Inbound left, outbound right, so direction reads before the words do. */
.sms-thread{display:flex;flex-direction:column;gap:10px;max-height:460px;overflow-y:auto;
  -webkit-overflow-scrolling:touch;padding-right:4px;}
.sms-msg{max-width:80%;padding:9px 12px;border-radius:12px;border:1px solid var(--border);}
.sms-msg.in{align-self:flex-start;background:var(--panel2);border-bottom-left-radius:3px;}
.sms-msg.out{align-self:flex-end;background:color-mix(in srgb, var(--accent) 12%, transparent);
  border-bottom-right-radius:3px;}
.sms-meta{font-size:11.5px;color:var(--muted);margin-bottom:3px;}
.sms-body{font-size:14px;line-height:1.5;white-space:pre-wrap;word-break:break-word;}
@media (max-width:880px){ .sms-msg{max-width:92%;} }

/* The text-message question on the signing page. Two radios on a screen the customer has already
   stopped at, not a fifth step: somebody who gives up at the last moment costs far more than a
   messaging permission is worth. Boxed so it reads as a separate question from the signature
   above it, and neither option is preselected. */
.sms-choice{margin:18px 0 4px;padding:14px 16px;border:1px solid var(--border);border-radius:10px;
  background:var(--panel2);}
.sms-choice-q{font-weight:700;font-size:15px;}
.sms-choice-opt{display:flex;align-items:center;gap:10px;font-size:15px;padding:7px 0;
  cursor:pointer;}
.sms-choice-opt input[type=radio]{width:19px;height:19px;flex:0 0 auto;cursor:pointer;}

/* ---------- adding a line to an estimate ----------
   Tabs rather than three stacked forms: they are alternatives, and stacking them makes an
   already long page longer on the phone this is used on. */
.add-tab-bar{display:flex;gap:6px;flex-wrap:wrap;border-bottom:1px solid var(--border);
  padding-bottom:10px;}
.add-tab-panel{align-items:flex-end;}

/* The estimate's price while it is being written. Sticky at the bottom of the builder so it
   stays readable while scrolling a long list of lines -- the figure is the thing being watched. */
.running-total{display:flex;gap:18px;justify-content:flex-end;align-items:baseline;flex-wrap:wrap;
  margin-top:14px;padding:10px 14px;background:var(--panel2);border:1px solid var(--border);
  border-radius:8px;font-size:14px;}
.running-total .rt-grand{font-size:17px;font-weight:700;}

/* ---------- payroll: allowances and shift corrections ----------
   Compact inline forms in table cells, so a week can be corrected without a page per shift. */
.allowance-cell{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}
.allowance-cell input[type=number]{max-width:86px;}
.shift-fix{gap:6px;align-items:center;}
.shift-fix input[type=time]{max-width:120px;}

/* ---------- the clock, in the mobile tech app ----------
   First card on a tech's own page, because clocking in is the first thing they do. Sized for a
   thumb: the button is full width and tall enough to hit without looking. */
.m-clock{margin:10px 14px;padding:14px;border:1px solid var(--border);border-radius:12px;
  background:var(--panel);}
.m-clock.on{border-color:#1b7a3d;background:#1b7a3d14;}
.m-clock-state{font-size:15px;font-weight:600;}
.m-clock-week{color:var(--muted);font-size:12.5px;margin-top:2px;}
.m-clock-break{display:flex;align-items:center;gap:8px;margin:10px 0 0;font-size:13px;
  color:var(--muted);}
.m-clock-break input{max-width:90px;}
.m-clock-btn{width:100%;justify-content:center;margin-top:10px;padding:14px;font-size:15px;}
.m-day-row{display:flex;align-items:center;gap:10px;padding:10px 14px;
  border-bottom:1px solid var(--border);}
.m-day-name{flex:1;font-size:13.5px;}
.m-day-hours{font-size:13.5px;color:var(--muted);white-space:nowrap;}

/* ---------------------------------------------------------------- on-site forms
   The acceptance form and the training checklist, filled in on a phone while standing in
   somebody's hallway. Every tick is its own row with a 44px target: laid out inline they ran
   together mid-label ("Installation Complete [] System Tested and Functional" on one wrapping
   line), which is a mis-tap waiting to happen on a form the customer is signing. */
.form-section { margin: 16px 0; }
.form-section h4 { margin: 0 0 4px; font-size: 15px; }
.form-tick {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 11px 6px; min-height: 44px; cursor: pointer;
  border-bottom: 1px solid var(--border);
}
.form-tick:last-child { border-bottom: 0; }
.form-tick input[type="checkbox"], .form-tick input[type="radio"] {
  width: 22px; height: 22px; margin: 0; flex: 0 0 auto; accent-color: var(--accent);
}
.form-tick span { line-height: 1.35; }
.form-question { margin: 14px 0; padding-bottom: 4px; border-bottom: 1px solid var(--border); }
.form-question p { margin: 0 0 8px; line-height: 1.4; }
/* Yes/No side by side: two large targets rather than two words with dots in front. */
.form-options { display: flex; gap: 10px; flex-wrap: wrap; }
.form-options .form-tick {
  flex: 1 1 120px; border: 1px solid var(--border); border-radius: 8px; border-bottom-width: 1px;
  align-items: center; justify-content: flex-start;
}

/* N/A on an acceptance section, and the compulsory question on the checklist. */
.form-na { font-weight: 600; border-bottom: 1px solid var(--border); }
.form-section-items { transition: opacity .12s ease; }
.form-section-items.is-na { opacity: .45; }
.form-statement {
  margin: 14px 0; padding: 10px 12px; border-left: 3px solid var(--border);
  background: rgba(127, 127, 127, .06); border-radius: 0 6px 6px 0;
}
.form-statement p { margin: 0; line-height: 1.4; }
.form-required {
  display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 999px;
  font-size: 11px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase;
  background: rgba(184, 134, 11, .16); color: #b8860b; vertical-align: middle;
}
.form-required-q { border-left: 3px solid #b8860b; padding-left: 10px; }

/* Who is signed in on the phone, and the way out. Neither existed: a tech handed a phone still
   signed in as somebody else could not tell, and could not fix it. */
.m-footer {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 22px 12px calc(18px + env(safe-area-inset-bottom));
  padding: 14px 16px; border-top: 1px solid var(--border);
  font-size: 13px; color: var(--muted);
}
.m-signout {
  flex: 0 0 auto; padding: 10px 16px; min-height: 44px; display: inline-flex;
  align-items: center; border: 1px solid var(--border); border-radius: 8px;
  font-weight: 600; text-decoration: none; color: var(--fg);
}

/* ---------------------------------------------------------------- typing on a phone
   iOS Safari zooms the whole page when a form control smaller than 16px takes focus. At 13.5px
   that means every field in the tech app -- the login, the on-site forms, a work note -- made
   the page lurch and rescale the moment somebody tapped it, and left them zoomed in afterwards.
   16px on small screens is the documented way to stop it, and it is a better size to type into
   one-handed anyway. Desktop keeps the denser 13.5px, where there is no such behaviour. */
@media (max-width: 700px) {
  input[type=text],input[type=email],input[type=number],input[type=date],input[type=password],
  input[type=tel],input[type=search],select,textarea { font-size: 16px; padding: 11px 12px; }
  /* The one button on a login screen should not be a 31px target. */
  .p-login-wrap .btn { min-height: 46px; padding: 12px 22px; font-size: 15px; }
}

/* Which day of a multi-day booking this chip is. */
.jc-run {
  display: inline-block; margin-right: 6px; padding: 0 6px; border-radius: 999px;
  background: rgba(127,127,127,.18); font-weight: 600; font-size: 10px;
  text-transform: uppercase; letter-spacing: .03em;
}

/* ---------------------------------------------------------------- tech colours on the calendar
   Colour is a shortcut, never the message: the tech's name sits beside the dot on every chip and
   in the legend, so a colour-blind reader or a black-and-white print loses the shortcut and
   nothing else. The wash is deliberately faint -- the chip has to stay readable in both themes,
   and a calendar of ten saturated blocks is harder to read than one of ten labels. */
.jobchip.has-tech { background: rgba(var(--tech-rgb), .13); }
.jobchip.has-tech .jc-link:hover { background: rgba(var(--tech-rgb), .22); }
.tech-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  background: rgb(var(--tech-rgb)); margin-right: 5px; vertical-align: baseline;
  /* A ring so a pale dot still reads against a pale chip. */
  box-shadow: 0 0 0 1px rgba(var(--tech-rgb), .45);
}
.tech-legend {
  display: flex; flex-wrap: wrap; gap: 6px 16px; align-items: center;
  margin: 0 0 10px; padding: 8px 10px;
  border: 1px solid var(--border); border-radius: 8px; background: var(--panel2);
  font-size: 12px; color: var(--muted);
}
.tech-key { display: inline-flex; align-items: center; white-space: nowrap; }
.kcol-head .tech-dot { margin-right: 6px; }

/* Picking an account by searching for it, rather than scrolling a dropdown of five thousand.
   Each hit is a whole form so it works without JavaScript; it is styled to read as a list. */
.cust-hits{margin-top:4px;}
.cust-hit{margin:0;}
.cust-hit-btn{display:block;width:100%;text-align:left;background:var(--panel2);color:var(--text);
  border:1px solid var(--line);border-radius:9px;padding:11px 12px;margin-bottom:7px;cursor:pointer;
  font:inherit;line-height:1.35;}
.cust-hit-btn:hover{border-color:var(--accent);}
.cust-hit-btn .t{display:block;font-weight:600;}
.cust-hit-btn .m2{display:block;font-size:12.5px;color:var(--muted);margin-top:2px;}
@media (max-width: 700px){
  /* A tap target somebody can hit while holding a ladder. */
  .cust-hit-btn{padding:14px 12px;}
}

/* The job type, said in a word on the tech's job list. Colour is the same signal the card border
   already carries, so a reader who cannot see it loses nothing. */
.m-type-tag{display:inline-block;font-size:11px;font-weight:700;letter-spacing:.02em;padding:2px 7px;
  border-radius:999px;margin-right:6px;border:1px solid var(--line);color:var(--muted);}
.m-type-tag.type-install{color:#1f6fb2;border-color:#1f6fb255;background:#1f6fb214;}
.m-type-tag.type-service{color:#b8860b;border-color:#b8860b55;background:#b8860b14;}

/* Google address lookup. The element draws its own box and the Google Maps attribution; this only
   sizes it and matches the app's theme (dark unless the light theme is chosen). */
.addr-lookup { width: 100%; flex-basis: 100%; margin: 0 0 6px; }
.addr-lookup gmp-place-autocomplete { width: 100%; color-scheme: dark; }
html[data-theme="light"] .addr-lookup gmp-place-autocomplete { color-scheme: light; }

/* Drag to reorder. The handle is the only thing that starts a drag. */
.drag-handle { cursor: grab; user-select: none; color: var(--muted); font-size: 18px; line-height: 1;
  padding: 4px 6px; display: inline-block; }
.drag-handle:hover { color: var(--text); }
.item-row > .drag-handle { align-self: center; flex: 0 0 auto; }
[data-reorder-list] > .dragging { opacity: .45; }
[data-reorder-list] > [draggable] .drag-handle { cursor: grabbing; }

/* Phones only. 38px a day was not a narrow calendar, it was an unreadable one: job titles came out
   at under two letters per line. On a phone the day keeps a readable width and the calendar
   scrolls sideways inside .cal-scroll. Anything wider -- the office's screens -- fits all seven
   days across, so a job can be dragged to any of them. */
@media (max-width:600px){
  .cal-grid{grid-template-columns:repeat(7,minmax(104px,1fr));min-width:756px;}
}

/* A button that has been pressed and is waiting on the server. See "every button shows it was
   pressed" in static/app.js. */
button.is-busy, .btn.is-busy{opacity:.75;cursor:progress;}
.is-busy::after{content:"";display:inline-block;width:.75em;height:.75em;margin-left:.45em;
  border:2px solid currentColor;border-right-color:transparent;border-radius:50%;
  vertical-align:-.1em;animation:busy-spin .7s linear infinite;}
@keyframes busy-spin{to{transform:rotate(360deg);}}

/* Review & Send: a draft invoice previewed as the customer will see it. */
.review-dialog{width:min(920px,96vw);max-height:94vh;padding:14px 16px;border:1px solid var(--border);
  border-radius:12px;background:var(--panel);color:var(--text);}
.review-dialog::backdrop{background:rgba(0,0,0,.55);}
.review-head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:6px;}
.review-frame{display:block;width:100%;height:62vh;border:1px solid var(--border);border-radius:8px;background:#fff;}
.review-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;}
/* Why Send is unavailable. Inside the window, not a page banner. */
.review-blocked{margin:0 0 8px;padding:8px 10px;border-radius:8px;border:1px solid #b8860b55;
  background:#b8860b1a;color:var(--text);font-size:13px;}

/* ---- jobs board: drag a card to another stage ---- */
.kanban-hint { margin: 0 0 10px; }
.kcard[draggable="true"] { cursor: grab; }
.kcard[draggable="true"]:active { cursor: grabbing; }
.kcard.dragging { opacity: .45; }
.kcard.moving { opacity: .6; pointer-events: none; }
.kcard.just-moved { box-shadow: 0 0 0 2px var(--accent); }
.kcol.drop-target { outline: 2px dashed var(--accent); outline-offset: -4px; }
