/* ═══════════════════════════════════════════════════════════════════════════
   THEME TOKENS  — single source of truth. Dark + Light via data-theme.
   Brand colours (--primary = brown, --primary-2 = gold) and the heading/body
   font pairing are pulled straight from royalfloors.com.au's own stylesheet,
   not invented — see the "Design" notes for where each value comes from.
   ═══════════════════════════════════════════════════════════════════════════ */
:root, [data-theme="dark"]{
  --bg:#0b0e1a; --surface:#14172a; --surface-2:#1b1f36; --surface-3:#232842;
  --border:rgba(255,255,255,0.08); --border-2:rgba(255,255,255,0.14);
  --text:#e7ecf5; --text-dim:#9aa6c0; --text-muted:#5b6580;
  --primary:#8a5a2e; --primary-2:#c5853a; --primary-soft:rgba(197,133,58,0.16);
  --success:#10b981; --success-soft:rgba(16,185,129,0.14);
  --danger:#ef4444;  --danger-soft:rgba(239,68,68,0.14);
  --warning:#f59e0b; --warning-soft:rgba(245,158,11,0.14);
  --info:#3b82f6;    --info-soft:rgba(59,130,246,0.14);
  --shadow:0 10px 30px rgba(0,0,0,0.45); --shadow-sm:0 4px 12px rgba(0,0,0,0.3);
  --input-bg:rgba(255,255,255,0.05);
  --font-heading:'Abhaya Libre',Georgia,serif; --font-body:'Albert Sans',system-ui,sans-serif;
  /* Appointment-type colours (Calendar) - one hue per type, independent of
     theme. Solid, darker/richer swatches (not a translucent tint over the
     surface - that's what made them read as washed-out) so they're the same
     legible colour in both light and dark app themes. Each type also gets a
     matching *-text variable, auto-picked for contrast (see contrastText() in
     13-calendar.js) so a custom colour from Settings never produces unreadable
     text. These are overridden at runtime by a generated <style> block once
     Appointment Settings loads (see injectApptColourVars in 13-calendar.js) -
     the values here are just the shipped defaults / pre-load fallback. */
  --type-measurement:#9a3412; --type-measurement-text:#fdf2ec; --type-measurement-soft:rgba(154,52,18,.20);
  --type-quote:#5b21b6; --type-quote-text:#f4eefd; --type-quote-soft:rgba(91,33,182,.20);
  --type-carpet:#065f46; --type-carpet-text:#eafbf4; --type-carpet-soft:rgba(6,95,70,.20);
  --type-hardfloor:#1e3a8a; --type-hardfloor-text:#eef2fd; --type-hardfloor-soft:rgba(30,58,138,.20);
  --type-pickup:#854d0e; --type-pickup-text:#fdf3e4; --type-pickup-soft:rgba(133,77,14,.20);
  --type-followup:#9d174d; --type-followup-text:#fdeef4; --type-followup-soft:rgba(157,23,77,.20);
  --type-invoice:#1e293b; --type-invoice-text:#eef1f6; --type-invoice-soft:rgba(30,41,59,.20);
  --type-default:#334155; --type-default-text:#eef1f6; --type-default-soft:rgba(51,65,85,.20);
  --type-unavailable:#44403c; --type-unavailable-text:#f4f2ef; --type-unavailable-soft:rgba(68,64,60,.20);
  /* Appointment STATUS colours - deliberately separate from .b-ordered/
     .b-complete/.b-lost (those are the generic app-wide badge palette used on
     Jobs/Quotes/etc; changing them would recolour pages far outside the
     calendar). Booked/Completed/Cancelled are the only three statuses this
     app supports (see schema.sql's CHECK constraint), same "keep the fixed
     set, make its look configurable" choice as appointment types. */
  --appt-status-booked:#1e3a8a; --appt-status-booked-text:#eef2fd;
  --appt-status-completed:#065f46; --appt-status-completed-text:#eafbf4;
  --appt-status-cancelled:#7f1d1d; --appt-status-cancelled-text:#fdeeee;
}
[data-theme="light"]{
  --bg:#f7f4ef; --surface:#ffffff; --surface-2:#faf8f4; --surface-3:#f2ede3;
  --border:#e7e0d3; --border-2:#dcd2bf;
  --text:#241705; --text-dim:#5c4a2f; --text-muted:#9c8b6f;
  --primary:#452205; --primary-2:#c5853a; --primary-soft:rgba(69,34,5,0.09);
  --success:#059669; --success-soft:rgba(16,185,129,0.12);
  --danger:#dc2626;  --danger-soft:rgba(239,68,68,0.10);
  --warning:#d97706; --warning-soft:rgba(245,158,11,0.14);
  --info:#2563eb;    --info-soft:rgba(59,130,246,0.12);
  --shadow:0 10px 30px rgba(36,23,5,0.10); --shadow-sm:0 4px 12px rgba(36,23,5,0.07);
  --input-bg:#f4f0e8;
}
@import url('https://fonts.googleapis.com/css2?family=Abhaya+Libre:wght@500;600;700;800&family=Albert+Sans:wght@400;500;600;700;800&display=swap');
*{box-sizing:border-box;margin:0;padding:0}
html,body,#root{height:100%}
body{font-family:var(--font-body);background:var(--bg);color:var(--text);overflow:hidden}
h1,h2,h3,h4,h5,h6,.card-h,.modal-h,.stat-val,.top-title,.login-brand{font-family:var(--font-heading)}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-thumb{background:var(--border-2);border-radius:8px}
::-webkit-scrollbar-track{background:transparent}

/* height:100% (not 100vh) deliberately — html/body/#root above are height:100%,
   and vh units don't compose with the CSS `zoom` the auto-scale script in
   index.html applies to <html> on big monitors: a zoomed 100vh renders taller
   than the real window with nowhere to scroll (body/.app both overflow:hidden),
   silently clipping whatever sits at the bottom — long job lists, the "Send to
   installer" card — on any screen where that script scales up. height:100%
   resolves inside the zoomed coordinate space instead and stays pixel-accurate. */
.app{display:flex;height:100%;overflow:hidden}

/* ── SIDEBAR NAV ── */
.nav{width:250px;flex-shrink:0;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
.nav-brand{display:flex;align-items:center;justify-content:center;padding:20px 16px 16px}
/* Workspace switcher — a segmented Customers/Insurance toggle under the logo.
   The active segment gets the same beige-and-gold treatment as an active nav
   row so the two read as one system. */
.ws-switch{display:flex;gap:4px;margin:0 12px 6px;padding:4px;background:var(--surface-2);border:1px solid var(--border);border-radius:11px}
.ws-switch-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:8px 6px;border:0;border-radius:8px;background:transparent;color:var(--text-dim);font-size:12.5px;font-weight:600;cursor:pointer;transition:.12s;white-space:nowrap}
.ws-switch-btn:hover{color:var(--text)}
.ws-switch-btn.on{background:var(--primary-soft);color:var(--primary);font-weight:700;box-shadow:0 1px 2px rgba(0,0,0,.06)}
.nav-scroll{flex:1;overflow-y:auto;padding:6px 12px}
.nav-section{font-size:10.5px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.1em;padding:16px 12px 7px}
.nav-item{display:flex;align-items:center;gap:12px;padding:11px 13px;border-radius:10px;cursor:pointer;font-size:14px;font-weight:500;color:var(--text-dim);transition:.12s;user-select:none;margin-bottom:2px}
.nav-item:hover{background:var(--surface-2);color:var(--text)}
/* Active page: soft beige pill with a gold accent bar on its left edge (an
   inset shadow, not a border, so the row's text never shifts sideways). */
.nav-item.on{background:var(--primary-soft);color:var(--primary);font-weight:700;box-shadow:inset 3px 0 0 var(--primary-2)}
.nav-ico{width:19px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.nav-badge{margin-left:auto;font-size:11px;font-weight:700;background:var(--surface-3);color:var(--text-dim);padding:2px 9px;border-radius:12px}
/* Footer: round avatar + stacked name/role + logout button on the right */
.nav-foot{padding:12px;border-top:1px solid var(--border);display:flex;align-items:center;gap:10px}
.nav-foot-avatar{border-radius:50%}
.nav-foot-id{flex:1;min-width:0}
.nav-foot-name{font-size:13px;font-weight:700;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.nav-foot-role{font-size:11px;color:var(--text-muted)}
/* Mobile drawer controls: the hamburger lives in the topbar and the backdrop
   dims/closes the off-canvas nav. Both are inert on desktop (see @media). */
.topbar .nav-toggle{display:none}   /* .topbar prefix beats .icon-btn's own display:flex */
.bottom-nav{display:none}           /* mobile-only; shown in the 820px media query */
.nav-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:490;animation:fadein .15s ease}
@keyframes fadein{from{opacity:0}to{opacity:1}}

/* ── MAIN ── */
.main{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
.topbar{height:56px;flex-shrink:0;border-bottom:1px solid var(--border);background:var(--surface);display:flex;align-items:center;gap:12px;padding:0 18px}
.top-title{font-size:15px;font-weight:700}
.top-search{flex:1;max-width:440px;position:relative}
.top-search input{width:100%;background:var(--input-bg);border:1px solid var(--border);border-radius:9px;padding:8px 12px 8px 32px;color:var(--text);font-size:13px;outline:none;font-family:inherit}
.top-search input:focus{border-color:var(--primary)}
.top-search .si{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:13px}
.top-search .kbd{position:absolute;right:8px;top:50%;transform:translateY(-50%);font-size:10px;color:var(--text-muted);border:1px solid var(--border);border-radius:5px;padding:1px 6px}
.top-right{margin-left:auto;display:flex;align-items:center;gap:8px}
.icon-btn{width:34px;height:34px;border-radius:7px;border:1px solid var(--border);background:var(--surface-2);color:var(--text-dim);cursor:pointer;font-size:15px;display:flex;align-items:center;justify-content:center;transition:.12s}
.icon-btn:hover{background:var(--surface-3);color:var(--text)}
/* Office/admin topbar notification bell + dropdown (see NotificationBell in 19-shell.js). */
.notif-wrap{position:relative;display:inline-flex}
.notif-panel{position:absolute;top:calc(100% + 8px);right:0;width:340px;max-width:calc(100vw - 32px);
  background:var(--surface);border:1px solid var(--border);border-radius:12px;box-shadow:var(--shadow);
  z-index:3000;overflow:hidden}
.notif-panel-hd{padding:11px 14px;font-weight:800;font-size:13px;color:var(--text);border-bottom:1px solid var(--border)}
.notif-panel-body{max-height:min(60vh,420px);overflow-y:auto}
.notif-empty{padding:22px 14px;text-align:center;color:var(--text-muted);font-size:13px}
.notif-row{display:flex;gap:10px;align-items:flex-start;padding:11px 14px;border-bottom:1px solid var(--border)}
.notif-row:last-child{border-bottom:0}
.notif-row.clickable{cursor:pointer;transition:background .12s}
.notif-row.clickable:hover{background:var(--surface-2)}
.notif-ico{flex:none;width:28px;height:28px;border-radius:7px;display:flex;align-items:center;justify-content:center;
  background:var(--primary-soft);color:var(--primary-2)}
.notif-row.alert .notif-ico{background:var(--danger-soft);color:var(--danger)}
.notif-title{font-weight:700;font-size:13px;color:var(--text);line-height:1.35}
.notif-body{font-size:12px;color:var(--text-dim);margin-top:2px;line-height:1.4}
.notif-time{font-size:10.5px;color:var(--text-muted);margin-top:3px}
.page{flex:1;overflow-y:auto;overflow-x:hidden;padding:22px 20px}
.page-narrow{max-width:1500px;margin:0 auto}

/* ── PAGE HEADER (PageHead primitive): title + subtitle + right-aligned action ── */
.page-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-bottom:18px}
.page-head-title{font-size:22px;font-weight:800;letter-spacing:-.4px;display:flex;align-items:center;gap:9px;line-height:1.15}
.page-head-title svg{color:var(--primary-2)}
.page-head-sub{font-size:13px;color:var(--text-muted);margin-top:4px}
.page-head-actions{display:flex;align-items:center;gap:9px;flex-wrap:wrap}

/* ── LOADING STATE (Loading primitive) ── */
.spinner{width:18px;height:18px;border:2.5px solid var(--border-2);border-top-color:var(--primary-2);border-radius:50%;animation:spin .7s linear infinite;display:inline-block;flex-shrink:0}
.loading-state{display:flex;align-items:center;justify-content:center;gap:10px;color:var(--text-muted);font-size:13.5px;font-weight:500}

/* ── BUTTONS ── */
.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid transparent;transition:.13s;font-family:inherit;white-space:nowrap}
.btn:disabled{opacity:.45;cursor:default}
.btn-primary{background:linear-gradient(135deg,var(--primary),var(--primary-2));color:#fff;box-shadow:0 2px 8px rgba(0,0,0,0.14)}
.btn-primary:hover:not(:disabled){filter:brightness(1.08)}
.btn-ghost{background:var(--surface-2);color:var(--text-dim);border-color:var(--border)}
.btn-ghost:hover{background:var(--surface-3);color:var(--text)}
.btn-danger{background:var(--danger-soft);color:var(--danger);border-color:transparent}
.btn-danger:hover{background:var(--danger);color:#fff}
.btn-success{background:var(--success-soft);color:var(--success)}
.btn-success:hover{background:var(--success);color:#fff}
.btn-sm{padding:6px 11px;font-size:12px;border-radius:7px}
.btn-block{width:100%;justify-content:center}
.btn:active:not(:disabled){transform:translateY(1px)}
/* Keyboard-only focus rings (a11y) — mouse clicks stay clean, Tab shows a ring. */
.btn:focus-visible,.icon-btn:focus-visible,.tab:focus-visible,.stab:focus-visible,.nav-item:focus-visible,.chk:focus-visible,.sel-plain:focus-visible{outline:2px solid var(--primary-2);outline-offset:2px}
.inp:focus-visible,.sel:focus-visible,.ta:focus-visible{box-shadow:0 0 0 3px var(--primary-soft)}

/* ── SAVE BAR (explicit Save button + unsaved-changes indicator) ── */
.save-bar{position:sticky;top:0;z-index:60;display:flex;align-items:center;justify-content:space-between;gap:14px;background:var(--surface);border:1px solid var(--border-2);border-radius:14px;padding:11px 16px;margin-bottom:16px;box-shadow:var(--shadow-sm)}
.save-status{display:flex;align-items:center;gap:8px;font-size:12.5px;font-weight:600;color:var(--text-dim)}
.save-status .dot{width:8px;height:8px;border-radius:50%;background:var(--success);flex-shrink:0}
.save-status.dirty{color:var(--warning)}
.save-status.dirty .dot{background:var(--warning)}
.save-status.error{color:var(--danger)}
.save-status.error .dot{background:var(--danger)}

/* ── CARDS / LAYOUT ── */
.card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:20px;margin-bottom:16px;box-shadow:var(--shadow-sm)}
.card-h{font-size:13.5px;font-weight:700;margin-bottom:16px;display:flex;align-items:center;gap:8px;letter-spacing:-.1px}
.sec-hd{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:14px;flex-wrap:wrap}
.grid{display:grid;gap:14px}
.g2{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}
.g3{grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}
.g4{grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.spread{display:flex;align-items:center;justify-content:space-between;gap:10px}
.muted{color:var(--text-muted)} .dim{color:var(--text-dim)}
.hr{border:none;border-top:1px solid var(--border);margin:16px 0}

/* ── FORM FIELDS ── */
.field{display:flex;flex-direction:column;gap:5px}
.field.full{grid-column:1/-1}
.lbl{font-size:10.5px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em}
.inp,.sel,.ta{background:var(--input-bg);border:1px solid var(--border);border-radius:6px;padding:9px 11px;color:var(--text);font-size:13px;font-family:inherit;outline:none;transition:.13s;width:100%}
.inp:focus,.sel:focus,.ta:focus{border-color:var(--primary);background:transparent}
.inp::placeholder,.ta::placeholder{color:var(--text-muted)}
.inp:read-only{opacity:.7}
.ta{resize:vertical;min-height:72px;line-height:1.5}
.sel{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath fill='%2394a3b8' d='M5 7L1 3h8z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}
.inp.invalid,.sel.invalid{border-color:var(--danger)!important;background:var(--danger-soft)!important}
.inp.valid,.sel.valid{border-color:var(--success)!important}
.field-err{color:var(--danger);font-size:11px;font-weight:600}

/* ── CUSTOMER PAGE CONTROLS (search + filter + sort) ──
   Uses the app's own theme tokens (not literal hex) so this still looks right
   in dark mode - the light theme's --surface/--border/--text already ARE the
   warm white/beige/dark-brown palette this was designed to match. */
.customer-controls{display:flex;align-items:center;gap:14px;padding:16px;background:var(--surface);border:1px solid var(--border);border-radius:16px;box-shadow:var(--shadow-sm);margin-bottom:16px;flex-wrap:wrap}
.customer-filter-control,.customer-sort-control{height:48px;min-width:210px;display:flex;align-items:center;gap:10px;border:1px solid var(--border);border-radius:11px;background:var(--surface-2);color:var(--text);padding:0 14px;font-size:13.5px;font-weight:600}
.customer-filter-control svg,.customer-sort-control svg{color:var(--primary-2);flex-shrink:0}
.customer-filter-control .chevron,.customer-sort-control .chevron{margin-left:auto;color:var(--text-muted)}
.customer-filter-control:focus-within,.customer-sort-control:focus-within{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-soft)}
.sel-plain{border:none;background:transparent;color:inherit;font:inherit;font-weight:inherit;outline:none;cursor:pointer;appearance:none;flex:1;padding:0}
@media (max-width:768px){
  .customer-controls{flex-direction:column;align-items:stretch}
  .customer-filter-control,.customer-sort-control{width:100%;min-width:0}
}

/* ── JOB DESCRIPTION CHECKLIST (Job Sheet) ── */
.jd-list{border:1px solid var(--border);border-radius:12px;background:var(--surface-2);padding:2px 14px}
.jd-item{padding:12px 0;border-bottom:1px solid var(--border)}
.jd-item:last-child{border-bottom:none}
.jd-q{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;font-size:13.5px;font-weight:600;color:var(--text)}
.jd-detail{margin-top:11px;display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(190px,1fr))}

/* ── TAGS ── */
.tags{display:flex;flex-wrap:wrap;gap:5px;align-items:center}
.tag{display:inline-flex;align-items:center;gap:5px;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600;background:var(--primary-soft);color:var(--primary)}
.tag x{cursor:pointer;font-weight:800;opacity:.7}
.tag x:hover{opacity:1}
.tag-add{border:1px dashed var(--border-2);background:transparent;color:var(--text-muted);border-radius:20px;padding:2px 9px;font-size:11px;cursor:pointer;font-family:inherit}

/* ── BADGES / STATUS ── */
.badge{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:20px;font-size:11px;font-weight:700}
.b-lead{background:var(--primary-soft);color:var(--primary)}
/* .b-quoted now carries "Measure and Quote" (the status was renamed); the class
   name is kept so historical badges and the review-status palette below stay
   on the same colour. */
.b-quoted{background:var(--warning-soft);color:var(--warning)}
/* Check Measure = quote accepted, final measure booked, nothing ordered yet.
   Violet sits between the amber of Measure and Quote and the blue of Ordered,
   so the three consecutive lanes never read as the same colour at a glance. */
.b-checkmeasure{background:rgba(139,92,246,0.16);color:var(--primary-2)}
.b-ordered{background:var(--info-soft);color:var(--info)}
.b-jobsheetsent{background:rgba(20,184,166,0.16);color:#0d9488}
/* .b-installed is retired (the Installed status was replaced by Finished) but
   is kept so any historical badge still renders rather than falling back to
   the Lead colour. */
.b-installed{background:rgba(139,92,246,0.16);color:var(--primary-2)}
.b-complete{background:var(--success-soft);color:var(--success)}
/* Finished = archived. Deliberately quieter than Complete: it is an end state,
   not an achievement to highlight. */
.b-finished{background:rgba(100,116,139,0.16);color:#475569}
.b-lost{background:var(--danger-soft);color:var(--danger)}
/* Follow-up = a parked quote waiting to be chased. Amber-leaning like Measure and Quote
   (it is still a live opportunity) but distinct, so a parked quote reads
   differently from one still on the board. */
.b-followup{background:rgba(245,158,11,0.16);color:#b45309}
.dot{width:8px;height:8px;border-radius:50%;display:inline-block}

/* ── STATS ── */
.stat{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:18px 20px;box-shadow:var(--shadow-sm)}
.stat-lbl{font-size:11px;color:var(--text-muted);font-weight:600;margin-bottom:6px;display:flex;align-items:center;gap:6px}
.stat-val{font-size:26px;font-weight:800;letter-spacing:-.5px}
/* An editable stat value (e.g. Costing's Grand total): looks like the plain
   number until focused, where it reveals a boxed brand-tinted field so it's
   discoverable as something you can type into. */
.stat-val-input{width:100%;border:1px solid transparent;background:transparent;border-radius:8px;
  padding:2px 4px;margin:-2px -4px;font:inherit;letter-spacing:inherit;color:inherit;outline:none;cursor:text}
.stat-val-input:hover{border-color:var(--border-2)}
.stat-val-input:focus{border-color:var(--primary-2);background:var(--input-bg);box-shadow:0 0 0 3px var(--primary-soft)}
.stat-sub{font-size:11px;color:var(--text-dim);margin-top:4px}

/* ── TABLES ── */
.tw{overflow-x:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--border);border-radius:12px}
.tbl{width:100%;border-collapse:collapse;font-size:12.5px}
.tbl th{background:var(--surface-2);color:var(--text-muted);font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:11px 14px;text-align:left;border-bottom:1px solid var(--border);white-space:nowrap}
.tbl td{padding:10px 14px;border-bottom:1px solid var(--border);vertical-align:middle}
.tbl tr:last-child td{border-bottom:none}
.tbl tbody tr{cursor:default}
.tbl tr.clickable{cursor:pointer}
.tbl tr.clickable:hover td{background:var(--surface-2)}
.tbl td .inp,.tbl td .sel{padding:6px 8px;font-size:12px}
.tbl-empty{text-align:center;padding:26px;color:var(--text-muted)}
.rt{font-weight:700;color:var(--primary);white-space:nowrap}

/* ── LIST ROWS (customers/jobs) ── */
.lrow{display:flex;align-items:center;gap:14px;padding:14px 16px;border:1px solid var(--border);border-radius:12px;background:var(--surface);margin-bottom:9px;cursor:pointer;transition:.12s}
.lrow:hover{border-color:var(--border-2);background:var(--surface-2);box-shadow:var(--shadow-sm)}
.avatar{width:40px;height:40px;border-radius:11px;background:linear-gradient(135deg,var(--primary),var(--primary-2));display:flex;align-items:center;justify-content:center;font-weight:700;color:#fff;font-size:14px;flex-shrink:0}
.avatar.sm{width:30px;height:30px;font-size:12px;border-radius:9px}

/* ── TABS ── */
.tabs{display:flex;gap:3px;border-bottom:1px solid var(--border);margin-bottom:18px;overflow-x:auto}
.tab{padding:10px 15px;font-size:13px;font-weight:600;color:var(--text-muted);cursor:pointer;background:none;border:none;border-bottom:2px solid transparent;font-family:inherit;white-space:nowrap;display:flex;align-items:center;gap:6px}
.tab:hover{color:var(--text-dim)}
.tab.on{color:var(--primary);border-bottom-color:var(--primary)}
.tab-dot{width:8px;height:8px;border-radius:50%;background:var(--danger);display:inline-block;box-shadow:0 0 0 2px var(--surface)}
/* Two-column tab layout: left group hugs the left edge, right group the right. */
.tabs-split{gap:16px}
.tab-group{display:flex;gap:3px}
/* Small "new from installer" flag on a jobs-list card */
.kcard-flag{display:inline-flex;align-items:center;gap:4px;font-size:10.5px;font-weight:700;color:var(--danger);background:var(--danger-soft);border-radius:20px;padding:1px 8px}
.subtabs{display:flex;gap:7px;margin-bottom:16px}
.stab{padding:8px 16px;border-radius:9px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid var(--border);background:var(--surface-2);color:var(--text-dim);font-family:inherit;display:inline-flex;align-items:center;gap:6px}
.stab.on{background:var(--primary-soft);color:var(--primary);border-color:var(--border-2);font-weight:700}

/* ── AREA / LENGTH MEASUREMENT TOGGLE (Settings → Price Book) ──
   The two units are deliberately given DIFFERENT active colours, not just an
   "on" state: at a glance across a long category list, brown = billed by area,
   gold = billed by length. Locked (view mode) keeps the active colour but drops
   the affordance — you can still read the unit, you just can't change it. */
/* Per-category measurement unit (Area / Length / Sheet / Bag). The selected
   button is marked by a single `.on` class and tinted from its `data-unit`, so
   adding a fifth unit later means one new rule here — not editing a chain of
   :not() selectors on every state, which is what the two-unit version needed.
   Area and Length keep the brand brown/gold; Sheet and Bag take their own hues
   so the unit is readable at a glance down a long list of categories. */
.unit-seg{display:inline-flex;border:1px solid var(--border);border-radius:7px;overflow:hidden;flex-shrink:0}
.unit-seg button{border:0;background:transparent;padding:3px 9px;font-size:11.5px;font-weight:600;color:var(--text-muted);cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;gap:4px;white-space:nowrap}
.unit-seg button:hover:not(:disabled):not(.on){color:var(--text-dim);background:var(--surface-3)}
.unit-seg button.on{color:#fff}
.unit-seg button.on[data-unit="area"]{background:linear-gradient(135deg,var(--primary),var(--primary-2))}
.unit-seg button.on[data-unit="length"]{background:var(--primary-2);color:#2b1a06;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
.unit-seg button.on[data-unit="sheet"]{background:var(--info)}
.unit-seg button.on[data-unit="bag"]{background:var(--type-quote)}
.unit-seg button.on[data-unit="tub"]{background:var(--warning);color:#2b1a06}
.unit-seg.locked button{cursor:default}
.unit-seg.locked button:not(.on){opacity:.45}

/* ── QUOTE: internal margin columns ──
   Cost and Profit % live on the Quote tab beside the customer-facing price, so
   they are tinted and rule-separated to make it visually obvious which numbers
   the customer never sees. The Quote tab is admin/office only and these fields
   are never printed, emailed or sent to an installer. */
.tbl th.qcol-int,.tbl td.qcol-int{background:var(--primary-soft)}
.tbl th.qcol-int:first-of-type,.tbl td.qcol-int:first-of-type{border-left:1px solid var(--border-2)}
.tbl th.qcol-int+th:not(.qcol-int),.tbl td.qcol-int+td:not(.qcol-int){border-left:1px solid var(--border-2)}
.qmargin{min-width:240px;flex:0 1 300px;border:1px dashed var(--border-2);border-radius:10px;padding:10px 12px;background:var(--primary-soft)}
.qmargin .lbl{display:flex;align-items:center;gap:5px;color:var(--text-muted)}

/* A price-book category card. `.editing` lifts it out of the list so it's
   obvious which one is unlocked while every other category stays read-only. */
.pb-cat{border:1px solid var(--border);border-radius:11px;padding:12px 13px;margin-bottom:14px;background:var(--surface-2)}
.pb-cat.editing{border-color:var(--primary-2);background:var(--surface);box-shadow:0 0 0 3px var(--primary-soft)}
.pb-cat.is-archived-type{opacity:.6}
.pb-cat-name{font-weight:700;font-size:13.5px;color:var(--text)}
.pb-locked-note{font-size:11px;color:var(--text-muted)}

/* ── ORDER: SUPPLIER SECTIONS ──
   Each supplier on an order is its own purchase order, so its card gets a left
   edge in the brand colour to read as one self-contained document in a stack of
   them. The unassigned tray is warned rather than branded: those lines aren't on
   anybody's order yet, and that's a state to leave, not to settle in. */
.ord-supplier{border-left:3px solid var(--primary-2)}
.ord-loose{border-left:3px solid var(--warning);background:var(--warning-soft)}
.ord-loose .tbl th{background:transparent}

/* ── INSTALLER INVOICE: installer-added extra line items ──
   Distinguished from the original job payment lines by BOTH a soft gold
   background/border (readable in light and dark - --primary-soft/--primary-2
   are theme-aware) AND a visible text label, never colour alone. Used on the
   installer's own Invoice tab (25-installer-invoice.js) and reused identically
   on the office Installer Invoices review page (26-installer-invoices-admin.js)
   so both sides read the same way. */
.installer-extra-row{border:1px solid var(--border);border-left:3px solid var(--primary-2);background:var(--primary-soft);
  border-radius:10px;padding:12px 14px;margin-bottom:10px}
.installer-extra-row-main{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
.installer-extra-pill{display:inline-flex;align-items:center;padding:2px 9px;border-radius:20px;font-size:10.5px;font-weight:700;
  background:var(--primary);color:#fff}

/* ── SETTINGS → ITEM DESCRIPTION ──
   One row per item: its name pinned left, its template and the rendered preview
   on the right. The name column is fixed-width so a long list of templates reads
   as one column of prose rather than a ragged edge. */
.idesc-row{display:flex;gap:14px;align-items:flex-start;padding:11px 0;border-bottom:1px solid var(--border)}
.idesc-row:last-child{border-bottom:none;padding-bottom:2px}
.idesc-item{flex:0 0 190px;padding-top:6px;min-width:0}
.idesc-item-name{font-weight:700;font-size:13px;color:var(--text);word-break:break-word}
/* The preview is what the customer will read, so it's set in the quote's own
   voice — quiet, italic, and clearly output rather than another input. */
.idesc-preview{margin-top:6px;font-size:11.5px;line-height:1.55;color:var(--text-muted);font-style:italic;
  border-left:2px solid var(--primary-2);padding-left:9px}
@media (max-width:720px){
  .idesc-row{flex-direction:column;gap:6px}
  .idesc-item{flex:none;padding-top:0}
}

/* ── KANBAN ──
   Fixed 6-up grid, not a horizontally-scrolling row: the columns share the
   available width and shrink together instead of pushing the board wider
   than the screen. (Phones get the old swipeable single-row behaviour back
   — see the scroll-snap override in the mobile media query below — because
   6 columns genuinely can't fit a phone width no matter how much they shrink.) */
/* One column per PIPELINE status (public/js/00-helpers.js) - keep the count
   here in step with that list. The 150px floor is the point below which a card
   stops being readable: on a narrower screen the board scrolls sideways rather
   than squeezing all seven lanes in. */
.kanban{display:grid;grid-template-columns:repeat(7,minmax(150px,1fr));gap:14px;padding-bottom:8px;align-items:start;overflow-x:auto}
.kcol{min-width:0;background:var(--surface-2);border:1px solid var(--border);border-radius:14px;padding:12px;min-height:120px}
.kcol.drag{border-color:var(--primary);background:var(--primary-soft)}
/* Wraps so a long status name ("Measure and Quote") keeps its column total on
   screen instead of squeezing it out of a narrow lane. */
.kcol-h{display:flex;align-items:center;justify-content:space-between;gap:4px;flex-wrap:wrap;font-size:12px;font-weight:700;margin-bottom:10px;padding:0 3px}
.kcard{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:11px 12px;margin-bottom:8px;cursor:grab;box-shadow:var(--shadow-sm)}
.kcard:hover{border-color:var(--border-2)}
.kcard:active{cursor:grabbing}
.kcard-t{font-size:13px;font-weight:600;margin-bottom:3px}
.kcard-m{font-size:11px;color:var(--text-muted)}

/* ── SCHEDULER ── */
/* Page split: a fixed filter rail beside the grid, which takes the rest. */
.cal-layout{display:grid;grid-template-columns:232px minmax(0,1fr);gap:20px;align-items:start}
.cal-layout-full{grid-template-columns:minmax(0,1fr)}
.cal-main{min-width:0}
.cal-hd{display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-bottom:14px}
.cal-hd-left,.cal-hd-right{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.cal-title{font-size:21px;font-weight:800;margin:0 6px 0 0;letter-spacing:-.02em}
.cal-period{font-weight:700;font-size:14px;color:var(--text-dim);margin-left:4px}
/* Segmented control - one pill-shaped tray, the active view raised out of it. */
.cal-views{display:flex;gap:2px;background:var(--surface-2);border:1px solid var(--border);border-radius:9px;padding:3px}
.cal-view-btn{border:0;background:transparent;color:var(--text-muted);font:inherit;font-size:12.5px;font-weight:600;text-transform:capitalize;padding:5px 13px;border-radius:7px;cursor:pointer;transition:.12s}
.cal-view-btn:hover{color:var(--text)}
.cal-view-btn.on{background:var(--primary);color:#fff}

/* ── Calendar sidebar ── */
.cal-side{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:14px;position:sticky;top:8px}
.mini-cal-hd{display:flex;align-items:center;justify-content:space-between;gap:4px;margin-bottom:8px}
.mini-cal-title{font-size:12.5px;font-weight:700}
.mini-cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:1px}
.mini-cal-dow{font-size:9.5px;font-weight:700;color:var(--text-muted);text-align:center;padding-bottom:4px}
.mini-cal-day{border:0;background:transparent;color:var(--text);font:inherit;font-size:11px;aspect-ratio:1;border-radius:6px;cursor:pointer;transition:.1s}
.mini-cal-day:hover{background:var(--surface-2)}
.mini-cal-day.dim{color:var(--text-muted);opacity:.5}
.mini-cal-day.today{font-weight:800;color:var(--primary)}
.mini-cal-day.on{background:var(--primary);color:#fff;font-weight:700}
.cal-side-sec{margin-top:16px;border-top:1px solid var(--border);padding-top:12px}
.cal-side-hd{font-size:10.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--text-muted);margin-bottom:7px}
.cal-filter{display:flex;align-items:center;gap:8px;padding:5px 6px;border-radius:7px;cursor:pointer;font-size:12px;color:var(--text-dim);transition:.1s}
.cal-filter:hover{background:var(--surface-2)}
.cal-filter.on{color:var(--text)}
.cal-filter input{margin:0;flex:none;cursor:pointer}
.cal-swatch{width:9px;height:9px;border-radius:3px;flex:none;background:var(--type-default)}
.cal-swatch-plain{background:var(--border-2)}
.cal-swatch.appt-type-measurement{background:var(--type-measurement)}
.cal-swatch.appt-type-quote{background:var(--type-quote)}
.cal-swatch.appt-type-carpet{background:var(--type-carpet)}
.cal-swatch.appt-type-hardfloor{background:var(--type-hardfloor)}
.cal-swatch.appt-type-pickup{background:var(--type-pickup)}
.cal-swatch.appt-type-followup{background:var(--type-followup)}
.cal-swatch.appt-type-invoice{background:var(--type-invoice)}
.cal-swatch.appt-type-unavailable{background:var(--type-unavailable)}
.cal-filter-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cal-filter-count{font-size:10.5px;font-weight:700;color:var(--text-muted);flex:none}
.cal-side-clear{width:100%;margin-top:12px}

.sched-filters{margin-bottom:14px}
.sched-filters .sel,.sched-filters .inp{width:auto;min-width:150px}
/* Month grid: one bordered block, cells divided by their own hairlines rather
   than gaps, so it reads as a single sheet the way a wall calendar does. */
.sched-month-wrap{background:var(--surface);border:1px solid var(--border);border-radius:14px;overflow:hidden}
.sched-dow-row{display:grid;grid-template-columns:repeat(7,1fr);background:var(--surface-2);border-bottom:1px solid var(--border)}
.sched-dow{padding:9px 4px;font-size:11px;font-weight:700;color:var(--text-muted);text-align:center}
.sched-month-grid{display:flex;flex-direction:column}
/* One .sched-week-row per calendar week, stacked vertically - NOT one big
   42-cell grid. Every appointment gets exactly one DOM node per week it
   touches, sized via grid-column span across the days it covers, so a
   multi-day booking is one real connected box (no per-day segments to stitch
   together with borders/negative margins) and it is immediately impossible
   for another event to force part of it onto a different lane, because
   there's only ever one box for the whole week's span. Lane packing (which
   row within the week a given event/segment sits on) is computed once per
   week in JS - see weekLanes() in 13-calendar.js. */
/* grid-auto-rows (not a fixed grid-template-rows) for EVERY row, including the
   day-number row - a spanning item's min-height (.sched-daybg's 90px comfort
   minimum below) can only be distributed into intrinsically-sized ("auto")
   tracks by the grid algorithm; a fixed-length track ignores it and the item
   just overflows past the row instead of growing it. */
.sched-week-row{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));grid-auto-rows:min-content;align-content:start;min-height:90px;row-gap:3px;padding-bottom:4px;position:relative;border-bottom:1px solid var(--border)}
.sched-week-row:last-child{border-bottom:0}
/* min-width:0 is load-bearing: without it, a cell holding an unbreakable
   pill label (white-space:nowrap) reports that label's full width as its
   min-content size, which drags its whole 1fr column wider than the rest
   and throws every row in the grid out of alignment with the Mon-Sun header. */
/* Absolutely positioned (not a grid item) so its height always matches the
   week row's actual rendered box - including the row's own min-height floor
   on an empty week - rather than only the grid tracks its number/pills
   happen to occupy. A spanning grid item's own min-height can only grow
   INTRINSICALLY-sized tracks it spans (and unpredictably dumps the whole
   requirement into whichever track the algorithm picks first); this avoids
   that entirely. */
.sched-daybg{position:absolute;top:0;bottom:0;min-width:0;cursor:pointer;transition:.12s;border-right:1px solid var(--border);background:var(--surface);z-index:0}
.sched-daybg:hover{background:var(--surface-2)}
.sched-daybg.dim{background:var(--surface-2);opacity:.55}
.sched-daybg.today{background:var(--primary-soft)}
.sched-daynum{grid-row:1;min-width:0;display:flex;align-items:center;justify-content:flex-end;padding:5px 5px 0;pointer-events:none;z-index:1}
.sched-daynum span{font-size:12px;font-weight:700;width:20px;height:20px;display:flex;align-items:center;justify-content:center;flex:none;border-radius:50%}
.sched-daynum.today span{background:var(--primary);color:#fff}
.sched-pill{font-size:11px;font-weight:600;line-height:1.35;padding:3px 7px;margin:0 3px;border-radius:6px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;z-index:1;display:flex;align-items:center;gap:3px}
.sched-pill:hover{filter:brightness(1.14)}
.sched-pill:active{filter:brightness(.9)}
.sched-pill:focus-visible{outline:2px solid var(--primary-2);outline-offset:1px;z-index:2}
.sched-pill.is-dragging{opacity:.55;outline:2px dashed var(--primary-2);outline-offset:1px}
/* A multi-day booking is ONE pill spanning every day-column it covers, so it
   already reads as a single connected bar. These modifiers only fire on the
   edge(s) that keep going past this week's row (wraps to the next/previous
   week) or past the visible 42-day grid entirely - the true start/end of the
   booking always keeps its rounded corner + margin. */
.sched-pill.mday-flush-left{margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}
.sched-pill.mday-flush-right{margin-right:0;border-top-right-radius:0;border-bottom-right-radius:0}
.sched-pill .mday-cont{flex:none;opacity:.8;font-weight:800}

/* ── APPOINTMENT TYPE COLOURS ── one hue per type (see each --type-X /
   --type-X-text / --type-X-soft trio in the theme tokens above). Every
   element that renders one appointment (.sched-pill, .appt-card, the
   installer mobile grid's .inst-month-cell-pill/-dot) carries an
   apptTypeClass() class below that just points three generic custom
   properties - --appt-accent/-text/-soft - at that type's own trio. A custom
   (admin-added) type has no such class, so it sets the same three names
   directly as an inline style instead (see customTypeAccentStyle() in
   13-calendar.js). Either way, everything past this block only ever reads
   --appt-accent/-text/-soft, never var(--type-X) directly, so it doesn't
   care which of the two supplied them. */
.appt-type-measurement{--appt-accent:var(--type-measurement);--appt-accent-text:var(--type-measurement-text);--appt-accent-soft:var(--type-measurement-soft)}
.appt-type-quote{--appt-accent:var(--type-quote);--appt-accent-text:var(--type-quote-text);--appt-accent-soft:var(--type-quote-soft)}
.appt-type-carpet{--appt-accent:var(--type-carpet);--appt-accent-text:var(--type-carpet-text);--appt-accent-soft:var(--type-carpet-soft)}
.appt-type-hardfloor{--appt-accent:var(--type-hardfloor);--appt-accent-text:var(--type-hardfloor-text);--appt-accent-soft:var(--type-hardfloor-soft)}
.appt-type-pickup{--appt-accent:var(--type-pickup);--appt-accent-text:var(--type-pickup-text);--appt-accent-soft:var(--type-pickup-soft)}
.appt-type-followup{--appt-accent:var(--type-followup);--appt-accent-text:var(--type-followup-text);--appt-accent-soft:var(--type-followup-soft)}
.appt-type-invoice{--appt-accent:var(--type-invoice);--appt-accent-text:var(--type-invoice-text);--appt-accent-soft:var(--type-invoice-soft)}
.appt-type-default{--appt-accent:var(--type-default);--appt-accent-text:var(--type-default-text);--appt-accent-soft:var(--type-default-soft)}
.appt-type-unavailable{--appt-accent:var(--type-unavailable);--appt-accent-text:var(--type-unavailable-text);--appt-accent-soft:var(--type-unavailable-soft)}
/* Dots stay solid-filled regardless of confirmation status (see the
   .is-confirmed/.is-unconfirmed rules below, which this deliberately isn't
   part of) - at 4px an outline ring wouldn't read as anything, it's only an
   "N more appointments" overflow hint, not the appointment itself. */
.inst-month-cell-dot{background:var(--appt-accent)}

/* ── CONFIRMATION STATUS: fill vs outline ──────────────────────────────────
   Confirmed solid-fills the whole block in its type colour with contrasting
   text (var(--appt-accent-text), picked for contrast against that colour -
   see contrastText() in 13-calendar.js). Unconfirmed shows only a coloured
   frame over a soft tint of the same colour, with the colour itself as the
   text/border, so the type is still obvious at a glance without reading as
   locked-in yet. Same two rules cover the month grid's pills, the week/day/
   agenda cards, and the installer mobile grid's single-appointment pill -
   .is-cancelled/.is-completed (still global, unscoped classes) layer their
   strikethrough/dimming on top of either state without touching which one it
   is, and hover/focus/drag (below) never reset background or border, so the
   confirmed/unconfirmed read stays legible through every interaction state. */
.sched-pill.is-confirmed,.appt-card.is-confirmed,.inst-month-cell-pill.is-confirmed{background:var(--appt-accent);color:var(--appt-accent-text)}
.sched-pill.is-unconfirmed,.appt-card.is-unconfirmed,.inst-month-cell-pill.is-unconfirmed{background:var(--appt-accent-soft);color:var(--appt-accent)}
.sched-pill.is-confirmed{box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
.sched-pill.is-unconfirmed{box-shadow:inset 0 0 0 1.5px var(--appt-accent)}
.inst-month-cell-pill.is-unconfirmed{box-shadow:inset 0 0 0 1px var(--appt-accent)}
/* .appt-card's frame is a real border (not an inset box-shadow like the
   pills above) because it already had one for layout - solid on both states
   so Confirmed's fill and Unconfirmed's outline are the exact same box size. */
.appt-card.is-confirmed,.appt-card.is-unconfirmed{border-color:var(--appt-accent)}
.is-cancelled{text-decoration:line-through;opacity:.55}
.is-completed{opacity:.82}
.sched-week-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:10px}
.sched-week-col{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:10px;min-height:220px;min-width:0}
.sched-week-hd{font-size:12px;font-weight:700;margin-bottom:8px;cursor:pointer;padding-bottom:6px;border-bottom:1px solid var(--border)}
.appt-card{position:relative;border:1.5px solid transparent;border-radius:10px;padding:9px 10px;margin-bottom:7px;cursor:pointer;transition:.12s}
/* Elevate on hover/focus/drag instead of resetting background/border colour
   the way a plain neutral card would - resetting either here would wipe out
   the confirmed (solid fill) vs unconfirmed (coloured outline) read this
   card exists to show, right when the user is looking straight at it. */
.appt-card:hover{filter:brightness(1.05)}
.appt-card:focus-visible{outline:2px solid var(--primary-2);outline-offset:1px}
.appt-card.is-dragging{opacity:.55;outline:2px dashed var(--primary-2);outline-offset:1px}
/* Assignee colour strip - sits just right of the type-colour border (left:3px
   is that border's own width), so the two accents read as distinct stripes. */
.appt-card-staff-strip{position:absolute;left:3px;top:0;bottom:0;width:4px;border-radius:3px}
.appt-card-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;gap:8px}
/* Time/sub read at reduced opacity of the card's OWN text colour (inherit),
   never a fixed grey - a fixed --text-dim/--text-muted grey is tuned for the
   app's neutral surfaces and would go muddy-on-colour (confirmed) or vanish
   outright against an accent-coloured (unconfirmed) title of a similar tone. */
.appt-card-time{font-size:11px;font-weight:700;color:inherit;opacity:.82}
.appt-card-title{font-size:13px;font-weight:600;margin-bottom:2px;display:flex;align-items:center;gap:6px;color:inherit}
.appt-card-sub{font-size:11.5px;color:inherit;opacity:.82}
/* Tap-to-call customer number on an appointment card. Inherits the card's own
   text colour for the same reason .appt-card-sub does (a fixed grey goes muddy
   against the confirmed/unconfirmed card fills), and gets a 24px min-height so
   it stays a usable touch target on the installer/estimator phone app, where
   it is the fastest way to ring the customer. */
.appt-card-phone{display:inline-flex;align-items:center;gap:4px;min-height:24px;margin-top:2px;font-size:11.5px;font-weight:600;color:inherit;opacity:.9;text-decoration:none}
.appt-card-phone:hover{opacity:1;text-decoration:underline}
/* The same number in the read-only appointment modal: styled as an .inp so it
   lines up with the disabled fields beside it, but is a live tel: link. */
.appt-phone-link{display:flex;align-items:center;font-weight:600;text-decoration:none;color:var(--text)}
.appt-phone-link:hover{text-decoration:underline}
.appt-card-mday{font-size:10px;font-weight:700;color:inherit;background:rgba(127,127,127,.22);border:1px solid currentColor;border-radius:20px;padding:1px 7px;flex:none;opacity:.9}
/* Appointment STATUS badge - dedicated colours (see --appt-status-* in the
   theme tokens), deliberately not the app-wide .b-ordered/.b-complete/.b-lost
   badge classes those would also recolour every Job/Quote status pill. */
.badge.appt-status-booked{background:var(--appt-status-booked);color:var(--appt-status-booked-text)}
.badge.appt-status-completed{background:var(--appt-status-completed);color:var(--appt-status-completed-text)}
.badge.appt-status-cancelled{background:var(--appt-status-cancelled);color:var(--appt-status-cancelled-text)}
/* General Settings → "Colour calendar by" = Status (instead of the default
   Type) - same three colours as the status badge above, applied to the
   pill/card itself instead. */
.sched-pill.by-status-booked{background:var(--appt-status-booked);color:var(--appt-status-booked-text)}
.sched-pill.by-status-completed{background:var(--appt-status-completed);color:var(--appt-status-completed-text)}
.sched-pill.by-status-cancelled{background:var(--appt-status-cancelled);color:var(--appt-status-cancelled-text)}
.appt-card.by-status-booked{border-left-color:var(--appt-status-booked)}
.appt-card.by-status-completed{border-left-color:var(--appt-status-completed)}
.appt-card.by-status-cancelled{border-left-color:var(--appt-status-cancelled)}

/* ── AGENDA / LIST VIEW ── date rail + timeline + event bars */
.agenda{margin-top:4px}
.agenda-row{display:flex;gap:14px}
.agenda-rail{width:44px;flex-shrink:0;display:flex;flex-direction:column;align-items:center;position:relative}
.agenda-daynum{font-size:20px;font-weight:800;line-height:1;color:var(--text)}
.agenda-daynum.is-today{color:var(--primary-2)}
.agenda-dow{font-size:10px;font-weight:700;letter-spacing:.06em;color:var(--text-muted);margin-top:3px}
.agenda-dot{width:9px;height:9px;border-radius:50%;background:var(--border-2);margin-top:9px}
.agenda-dot.is-today{background:var(--primary-2);box-shadow:0 0 0 3px var(--primary-soft)}
.agenda-line{flex:1;width:2px;background:var(--border);margin-top:5px;min-height:20px}
.agenda-events{flex:1;min-width:0;padding-bottom:16px;display:flex;flex-direction:column;gap:8px}
.agenda-events .appt-card{margin-bottom:0}

/* ── TOTALS BOX ── */
.totals{background:var(--surface-2);border:1px solid var(--border);border-radius:12px;padding:16px;margin-top:12px}
.trow{display:flex;justify-content:space-between;padding:5px 0;font-size:13px;color:var(--text-dim);border-bottom:1px solid var(--border)}
.trow:last-child{border-bottom:none}
.tgrand{display:flex;justify-content:space-between;font-size:18px;font-weight:800;margin-top:8px;padding-top:10px;border-top:1px solid var(--border-2)}
.tgrand b{color:var(--primary)}

/* ── ADDRESS AUTOCOMPLETE ── */
.addr-wrap{position:relative}
.addr-sug{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--surface);border:1px solid var(--border-2);border-radius:10px;z-index:300;overflow:hidden;box-shadow:var(--shadow);max-height:290px;overflow-y:auto}
/* Results stay readable while the next lot loads, just visibly stale. */
.addr-sug.busy .addr-item{opacity:.55}
.addr-item{padding:10px 13px;cursor:pointer;font-size:12.5px;border-bottom:1px solid var(--border)}
.addr-item:last-child{border-bottom:none}
.addr-item:hover,.addr-item.on{background:var(--primary-soft)}
.addr-item.on{opacity:1}
.addr-main{font-weight:600}
.addr-sub{font-size:11px;color:var(--text-muted);margin-top:1px}
.addr-loading{padding:11px 13px;font-size:12px;color:var(--text-muted);display:flex;gap:8px;align-items:center}
.addr-hint{font-size:11px;color:var(--text-muted);margin:0 0 10px 2px}
.gps-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;background:var(--success-soft);border:none;border-radius:8px;color:var(--success);font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
.gps-btn:disabled{opacity:.5}
.inp:disabled,.sel:disabled{opacity:.65;cursor:not-allowed;background:var(--surface-2)}
/* Delivery Address card while linked to Billing — the "Using billing
   address" text + link icon (rendered by the caller) carry the meaning;
   this border is a secondary, non-colour-only visual cue only. */
.addr-fields-disabled{border-left:3px solid var(--border-2);padding-left:12px}
.addr-linked-note{display:flex;align-items:center;gap:7px;font-size:12.5px;font-weight:600;color:var(--text-dim);background:var(--surface-2);border:1px solid var(--border);border-radius:9px;padding:9px 12px;margin-bottom:12px}
.spin{width:11px;height:11px;border:2px solid var(--text-muted);border-top-color:var(--primary);border-radius:50%;animation:spin .7s linear infinite;display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}

/* ── MODAL ── */
/* z-index 520 — a modal is the topmost thing on screen, so it must sit above
   ALL the fixed app chrome (.inst-action-bar 470, .bottom-nav 480,
   .nav-backdrop 490, .nav 500). At the old 400 the installer/estimator mobile
   bars covered the dialog's bottom edge, so its action row - the Upload button
   on the photo-upload modals - was unreachable behind them. Stays below
   .qb-pop-fixed (550), which opens FROM inside modals, and .toasts (600). */
.overlay{position:fixed;inset:0;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);display:flex;align-items:flex-start;justify-content:center;z-index:520;padding:40px 16px;overflow-y:auto}
.modal{background:var(--surface);border:1px solid var(--border-2);border-radius:16px;box-shadow:var(--shadow);width:100%;max-width:540px;padding:26px}
.modal.wide{max-width:720px}
.modal-h{font-size:17px;font-weight:800;margin-bottom:4px}
.modal-sub{font-size:12.5px;color:var(--text-muted);margin-bottom:18px}
.modal-acts{display:flex;gap:9px;justify-content:flex-end;margin-top:20px}
/* The PDF preview fills the dialog: A4 is portrait, so height is what matters,
   and the viewer's own toolbar handles zoom and paging. */
.pdf-frame{width:100%;height:min(70vh,760px);border:1px solid var(--border);border-radius:10px;background:var(--surface-2);display:block}
.pdf-frame-loading{display:flex;align-items:center;justify-content:center;font-size:13px}

/* ── DRAWER (Appointment Settings) ── side panel on desktop, full-width
   bottom-up sheet on mobile. Reuses .overlay's dimmed backdrop; only the
   panel itself differs from .modal (slides in from the right, own scroll
   region for the tab body, sticky header/footer). */
.drawer-overlay{justify-content:flex-end;padding:0;align-items:stretch}
.drawer{background:var(--surface);border-left:1px solid var(--border-2);box-shadow:var(--shadow);width:100%;max-width:560px;display:flex;flex-direction:column;height:100%;animation:drawer-in .18s ease-out}
@keyframes drawer-in{from{transform:translateX(24px);opacity:.4}to{transform:translateX(0);opacity:1}}
.drawer-h{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:20px 22px 14px;border-bottom:1px solid var(--border);flex:none}
.drawer-title{font-size:18px;font-weight:800;font-family:var(--font-heading)}
.drawer-sub{font-size:12.5px;color:var(--text-muted);margin-top:3px;max-width:440px}
.drawer-tabs{display:flex;gap:2px;padding:10px 14px 0;border-bottom:1px solid var(--border);flex:none;overflow-x:auto}
.drawer-tab{border:0;background:transparent;font:inherit;font-size:12.5px;font-weight:700;color:var(--text-muted);padding:9px 12px;border-radius:8px 8px 0 0;cursor:pointer;white-space:nowrap;border-bottom:2px solid transparent}
.drawer-tab:hover{color:var(--text-dim);background:var(--surface-2)}
.drawer-tab.on{color:var(--primary-2);border-bottom-color:var(--primary-2)}
.drawer-tab:focus-visible{outline:2px solid var(--primary-2);outline-offset:-1px}
.drawer-body{flex:1;min-height:0;overflow-y:auto;padding:18px 22px}
.drawer-acts{display:flex;gap:9px;justify-content:flex-end;padding:14px 22px;border-top:1px solid var(--border);flex:none}
.drawer-section{margin-bottom:22px}
.drawer-section:last-child{margin-bottom:0}
.drawer-section-hd{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);margin-bottom:10px}
.drawer-hint{font-size:11.5px;color:var(--text-muted);margin-top:3px;line-height:1.5}
/* One appointment-type / status row - colour swatch preview + collapsible
   detail fields underneath, so 8 types don't turn into an unreadable wall of
   inputs before you've picked one to edit. */
.appt-type-row{border:1px solid var(--border);border-radius:12px;margin-bottom:10px;overflow:hidden}
.appt-type-row-hd{display:flex;align-items:center;gap:10px;padding:11px 13px;cursor:pointer;background:var(--surface-2)}
.appt-type-row-hd:hover{background:var(--surface-3)}
.appt-type-swatch{width:16px;height:16px;border-radius:5px;flex:none;box-shadow:inset 0 0 0 1px rgba(0,0,0,.15)}
.appt-type-row-name{font-size:13px;font-weight:700;flex:1;min-width:0}
.appt-type-row-name .muted{font-weight:500;font-size:11.5px}
.appt-type-row-body{padding:14px 15px;border-top:1px solid var(--border)}
.appt-type-preview{display:inline-flex;align-items:center;padding:4px 10px;border-radius:6px;font-size:11px;font-weight:600;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
.color-field{display:flex;align-items:center;gap:8px}
.color-field input[type=color]{width:36px;height:32px;padding:2px;border:1px solid var(--border);border-radius:7px;background:var(--surface);cursor:pointer}
.color-field .inp{width:90px;font-family:monospace;font-size:12px}
.working-hours-row{display:grid;grid-template-columns:100px 1fr 90px 90px;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border)}
.working-hours-row:last-child{border-bottom:0}
.closed-date-row{display:flex;gap:8px;align-items:center;margin-bottom:6px}
.closed-date-row .inp{flex:1}
@media (max-width: 720px){
  .drawer{max-width:100%}
  .working-hours-row{grid-template-columns:74px 1fr 78px 78px;gap:6px;font-size:12px}
}

/* ── TOASTS ── */
.toasts{position:fixed;bottom:20px;right:20px;z-index:600;display:flex;flex-direction:column;gap:9px}
.toast{background:var(--surface);border:1px solid var(--border-2);border-left:3px solid var(--primary);border-radius:12px;padding:12px 16px;font-size:13px;font-weight:500;box-shadow:var(--shadow);min-width:230px;animation:slidein .2s ease}
.toast.success{border-left-color:var(--success)} .toast.error{border-left-color:var(--danger)} .toast.warning{border-left-color:var(--warning)}
@keyframes slidein{from{transform:translateX(30px);opacity:0}to{transform:none;opacity:1}}

/* ── MISC ── */
.empty{text-align:center;padding:60px 20px;color:var(--text-muted)}
.empty .ico{font-size:46px;opacity:.4;margin-bottom:12px}
.empty .t{font-size:16px;font-weight:600;color:var(--text-dim);margin-bottom:5px}
.pill{padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600;background:var(--surface-3);color:var(--text-dim)}
.chk{display:inline-flex;align-items:center;gap:7px;cursor:pointer;font-size:13px;color:var(--text-dim)}
.chk input{accent-color:var(--primary);width:15px;height:15px;cursor:pointer}
.timeline-item{display:flex;gap:11px;padding:9px 0;border-bottom:1px solid var(--border)}
.timeline-item:last-child{border-bottom:none}
.tl-dot{width:9px;height:9px;border-radius:50%;background:var(--primary);margin-top:5px;flex-shrink:0}
.tl-txt{font-size:13px} .tl-time{font-size:11px;color:var(--text-muted);margin-top:2px}
/* Photo tile: square media area with the caption in its own readable strip
   BELOW the image (normal text on a surface background), never overlaid in
   white on top of the photo. */
.photo{position:relative;border-radius:10px;overflow:hidden;border:1px solid var(--border);background:var(--surface-2);display:flex;flex-direction:column}
.photo img,.photo video{width:100%;aspect-ratio:1;object-fit:cover;display:block;background:#000;flex-shrink:0}
.photo .rm{position:absolute;top:5px;right:5px;background:rgba(0,0,0,.6);color:#fff;border:none;border-radius:6px;width:24px;height:24px;cursor:pointer}
.photo-caption{color:var(--text);font-size:12px;font-weight:500;padding:7px 9px;line-height:1.45;border-top:1px solid var(--border);background:var(--surface-2);word-break:break-word;flex:1}
/* Jobs → Photos: Estimator/Installer Photos subtab card - extends .photo
   with sender/recipient/sharing metadata + the manual share action. */
.photo-card .photo-meta{padding:7px 9px 4px;border-top:1px solid var(--border);background:var(--surface-2)}
.photo-card .photo-caption{padding:0 0 4px;border-top:none;background:transparent}
.photo-sub{color:var(--text-muted);font-size:10.5px;line-height:1.5;word-break:break-word}
.photo-shared{display:flex;gap:5px;flex-wrap:wrap;margin:3px 0}
.pill-share{background:var(--primary-soft);color:var(--primary);border:1px solid var(--border-2)}
.photo-share-done{display:inline-flex;align-items:center;gap:4px;margin:6px 9px 9px;background:var(--surface-3);color:var(--text-muted)}
.photo-share-btn{margin:6px 9px 9px;align-self:flex-start}
/* Thumbnails are cropped squares; clicking one opens the full picture in the
   lightbox below. The cursor + subtle hover zoom signal that they're tappable. */
.media-thumb-img{cursor:zoom-in;transition:transform .18s ease}
.media-thumb-img:hover{transform:scale(1.03)}
/* Small "expand" affordance overlaid on video thumbnails (their own controls
   own the tap, so they get an explicit button to open the full-size preview). */
.media-expand{position:absolute;top:5px;left:5px;display:inline-flex;align-items:center;justify-content:center;background:rgba(0,0,0,.6);color:#fff;border:none;border-radius:6px;width:26px;height:26px;cursor:pointer}
.media-expand:hover{background:rgba(0,0,0,.78)}
/* Full-screen media preview (lightbox). z-index 620 sits above modals (520)
   and toasts (600) so a preview opened from any screen is always on top. */
.lightbox{position:fixed;inset:0;z-index:620;background:rgba(0,0,0,.9);display:flex;align-items:center;justify-content:center;padding:24px;animation:fadein .15s ease}
.lb-stage{max-width:100%;max-height:100%;display:flex;flex-direction:column;align-items:center;gap:12px}
.lb-media{max-width:min(92vw,1400px);max-height:82vh;width:auto;height:auto;object-fit:contain;border-radius:8px;background:#000;box-shadow:0 12px 40px rgba(0,0,0,.5)}
.lb-caption{color:#fff;font-size:13.5px;line-height:1.5;text-align:center;max-width:min(92vw,900px);word-break:break-word}
.lb-count{color:rgba(255,255,255,.7);font-size:12px}
.lb-close{position:absolute;top:16px;right:16px;display:inline-flex;align-items:center;justify-content:center;width:42px;height:42px;border-radius:50%;background:rgba(255,255,255,.12);color:#fff;border:none;cursor:pointer;transition:background .15s ease}
.lb-close:hover{background:rgba(255,255,255,.24)}
.lb-nav{position:absolute;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;width:48px;height:48px;border-radius:50%;background:rgba(255,255,255,.12);color:#fff;border:none;cursor:pointer;transition:background .15s ease}
.lb-nav:hover{background:rgba(255,255,255,.24)}
.lb-prev{left:14px}
.lb-next{right:14px}
@media (max-width:640px){
  .lb-media{max-width:96vw;max-height:76vh}
  .lb-nav{width:40px;height:40px}
  .lb-prev{left:6px}
  .lb-next{right:6px}
}

/* Calendar appointment questionnaire (beneath Notes) */
.calq-group{border-top:1px solid var(--border);padding-top:14px}
.calq-item{margin-bottom:12px}
.calq-item-indented{margin-left:20px;padding-left:12px;border-left:2px solid var(--border-2)}
.calq-required{color:var(--danger)}
@media (max-width:560px){.calq-item-indented{margin-left:10px;padding-left:8px}}
.progress-steps{display:flex;gap:5px;align-items:center;flex-wrap:wrap;margin-top:6px}
.pstep{font-size:10px;padding:2px 9px;border-radius:5px;border:1px solid var(--border);color:var(--text-muted)}
.pstep.done{background:var(--primary-soft);border-color:var(--border-2);color:var(--primary)}
.parrow{font-size:9px;color:var(--text-muted)}
.bar-track{height:9px;background:var(--surface-3);border-radius:6px;overflow:hidden}
.bar-fill{height:100%;background:linear-gradient(90deg,var(--primary),var(--primary-2))}

/* ── LOGIN SCREEN ── premium light look: soft beige/gold radial shapes behind
   a centred white card. Deliberately fixed light tones (not theme vars) for
   the background blobs specifically, same reasoning as the print/document
   preview above - a sign-in page should look the same regardless of which
   in-app theme is currently selected. */
.login-wrap{height:100%;display:flex;align-items:center;justify-content:center;background:
  linear-gradient(180deg, rgba(28,18,4,0.18), rgba(28,18,4,0.24)),
  url('/assets/backgrounds/login-bg.png') center/cover no-repeat;
  background-color:#f4efe6}
.login-card{width:100%;max-width:400px;background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:36px 34px;box-shadow:0 20px 50px rgba(36,23,5,0.14)}
.login-brand{display:flex;align-items:center;justify-content:center;margin-bottom:18px}
.login-title{text-align:center;font-size:22px;font-weight:800;margin-bottom:6px;color:var(--text)}
.login-sub{text-align:center;color:var(--text-muted);font-size:12.5px;margin-bottom:24px}
.login-err{background:var(--danger-soft);color:var(--danger);border-radius:8px;padding:9px 12px;font-size:12.5px;margin-bottom:14px}
.login-input-wrap{position:relative}
.login-input-wrap .inp{padding-left:38px}
.login-input-icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:var(--text-muted);display:flex;pointer-events:none}
.login-eye-btn{position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--text-muted);cursor:pointer;padding:4px;display:flex}
.login-eye-btn:hover{color:var(--text)}
.login-row-between{display:flex;align-items:center;justify-content:space-between;margin:2px 0 18px;font-size:12.5px}
.login-row-between .chk{font-size:12.5px}
.login-forgot{color:var(--primary-2);font-weight:600;cursor:pointer;background:none;border:none;font-family:inherit;font-size:12.5px;padding:0}
.login-forgot:hover{text-decoration:underline}
.login-help{text-align:center;margin-top:20px;font-size:11.5px;color:var(--text-muted);line-height:1.6}

/* ── GLASSMORPHIC LOGIN CARD ──────────────────────────────────────────────────
   Frosted "floating panel" that sits over the branded background photo. All
   colours are explicit (not theme vars) so the glass reads the same regardless
   of the app theme. ── */
.login-card{
  background:linear-gradient(160deg, rgba(44,31,14,0.52), rgba(24,16,7,0.60));
  -webkit-backdrop-filter:blur(20px) saturate(1.15);
  backdrop-filter:blur(20px) saturate(1.15);
  border:1px solid rgba(255,255,255,0.16);
  border-radius:26px;
  padding:34px 34px 30px;
  box-shadow:0 30px 70px rgba(20,12,2,0.45), inset 0 1px 0 rgba(255,255,255,0.22);
}
.login-card .logo-plate.login{background:transparent;box-shadow:none;padding:0 0 2px;margin-bottom:6px}
.login-card .login-title{color:#fdf7ee;font-family:var(--font-heading);font-weight:700;font-size:26px;letter-spacing:.01em}
.login-card .login-sub{color:rgba(253,247,238,0.72);font-size:13px;margin-bottom:0}
.login-divider{width:64px;height:2px;margin:14px auto 22px;border-radius:2px;background:linear-gradient(90deg,transparent,var(--primary-2),transparent)}
.login-card .lbl{color:rgba(253,247,238,0.80)}
.login-card .inp{
  background:rgba(255,255,255,0.07);
  border:1px solid rgba(255,255,255,0.20);
  border-radius:11px;
  color:#fdf7ee;
  padding-top:11px;padding-bottom:11px;
}
.login-card .inp::placeholder{color:rgba(253,247,238,0.42)}
.login-card .inp:focus{background:rgba(255,255,255,0.12);border-color:var(--primary-2)}
.login-card .inp:focus-visible{box-shadow:0 0 0 3px rgba(197,133,58,0.30)}
.login-card .login-input-icon,.login-card .login-eye-btn{color:rgba(253,247,238,0.60)}
.login-card .login-eye-btn:hover{color:#fdf7ee}
.login-card .chk{color:rgba(253,247,238,0.78)}
.login-card .chk input{accent-color:var(--primary-2)}
.login-card .login-forgot{color:var(--primary-2)}
.login-card .login-submit{
  padding:13px 18px;border-radius:12px;font-size:14px;letter-spacing:.02em;margin-top:4px;
  box-shadow:0 8px 22px rgba(150,96,20,0.40);
}
.login-card .login-help{color:rgba(253,247,238,0.60)}

/* ── COMPANY LOGO (reusable everywhere: login, sidebar, print/PDF headers, previews) ──
   Always sized with object-fit:contain so the full logo shows — never cropped,
   stretched or recoloured. A white "plate" sits behind it so the logo's own
   colours stay accurate even when the surrounding UI is in dark mode. ── */
.logo-plate{background:#fff;border-radius:10px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 1px 3px rgba(0,0,0,0.12)}
.logo-plate.login{padding:14px 20px;margin-bottom:2px}
.logo-plate.nav{padding:12px 18px;border:1px solid var(--border);border-radius:14px}
.company-logo{max-width:180px;max-height:80px;width:auto;height:auto;object-fit:contain;display:block}
.login-logo{max-width:200px;max-height:110px;width:auto;height:auto;object-fit:contain;display:block}
.nav-logo{max-width:160px;max-height:62px;width:auto;height:auto;object-fit:contain;display:block}
/* ── DOCUMENT / PRINT PREVIEW  ──────────────────────────────────────────────
   Used by every in-app "this is exactly what prints" preview (Invoice
   preview, Job Sheet, etc). Deliberately fixed light "paper" colours instead
   of the app's dark/light theme variables — a printed document always looks
   like white paper with the brand's own brown/gold palette, regardless of
   which theme the office is using inside the app, so preview always matches
   the actual PDF/print output exactly. The same class names + look are
   mirrored in the print template's own stylesheet (see 06-print.js). ── */
.document-header{display:flex;align-items:center;gap:16px;padding-bottom:18px;margin-bottom:20px;border-bottom:2px solid #c5853a}
.document-header .brand-divider{width:1px;align-self:stretch;background:#e7e0d3}
.document-header .company-details{font-size:12.5px;color:#5c4a2f;line-height:1.9}
.document-header .company-details .abn{color:#9c8b6f}
.document-header .company-details b{color:#241705;font-size:14px}

/* Two-column "Customer Details / Job Details" style block used by every
   preview & print document - replaces the old text-align:right column,
   which pushed labels/values to the far edge and mangled long values (like
   a warehouse pickup address) into one unreadable line. */
.document-details-grid{display:grid;grid-template-columns:1fr 1fr;gap:28px;align-items:start;margin-bottom:20px}
.document-detail-card{border:1px solid #e7e0d3;border-radius:8px;padding:14px 16px;background:#fffdfa}
.document-detail-title{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:#9b6a2f;margin-bottom:10px}
.document-detail-row{display:grid;grid-template-columns:96px 1fr;gap:8px;margin-bottom:6px;line-height:1.4;font-size:12.5px}
.document-detail-row:last-child{margin-bottom:0}
.document-detail-label{font-weight:700;color:#241705}
.document-detail-value{color:#3a2c17;word-break:break-word;overflow-wrap:anywhere;white-space:normal}
.document-detail-value.multiline{white-space:pre-line}
@media (max-width:720px){ .document-details-grid{grid-template-columns:1fr} }

/* ── ROLE BADGES ── */
.role-admin{background:var(--danger-soft);color:var(--danger)}
.role-office{background:var(--info-soft);color:var(--info)}
.role-installer{background:var(--warning-soft);color:var(--warning)}

/* ═══════════════════════════════════════════════════════════════════════════
   MOBILE / TABLET  — the whole app must be usable in a phone browser (installers
   on site, staff on the go). Off-canvas nav, everything wide scrolls INSIDE its
   own container (the page body never scrolls sideways), modals fit and scroll,
   and tap targets are finger-sized. Purely presentational — no JS/logic touched.
   ═══════════════════════════════════════════════════════════════════════════ */
/* Mid-range desktop windows (a half-screen browser, a small laptop): the nav
   is still docked, but slim it down and tighten the page gutters so the content
   area doesn't get crushed before the full off-canvas breakpoint kicks in. */
@media (min-width:821px) and (max-width:1100px){
  .nav{width:210px}
  .nav-item{padding:10px 11px;gap:10px}
  .page{padding:18px 16px}
  .top-search{max-width:320px}
}
@media (max-width:820px){
  /* Off-canvas drawer + the topbar hamburger that opens it */
  .nav{position:fixed;z-index:500;height:100%;width:min(84vw,300px);transform:translateX(-100%);transition:transform .2s ease;box-shadow:var(--shadow)}
  .nav.open{transform:none}
  .topbar .nav-toggle{display:inline-flex}
  .nav-item{min-height:44px}

  /* Topbar: keep it from overflowing — drop the ⌘K hint, let search shrink */
  .topbar{padding:0 12px;gap:8px}
  .top-search{max-width:none}
  .top-search .kbd{display:none}
  .top-title{font-size:14px}

  /* Comfortable page gutters + clearance for the fixed bottom nav */
  .page{padding:16px 14px calc(80px + env(safe-area-inset-bottom))}

  /* Bottom nav: the everyday pages within thumb reach. Sits below the
     drawer's backdrop (z 490) so opening the menu dims it too. */
  .bottom-nav{display:flex;position:fixed;left:0;right:0;bottom:0;z-index:480;background:var(--surface);border-top:1px solid var(--border);padding-bottom:env(safe-area-inset-bottom);box-shadow:0 -4px 16px rgba(36,23,5,0.07)}
  .bn-item{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;min-height:58px;padding:8px 2px;background:none;border:none;font-family:inherit;font-size:10.5px;font-weight:600;color:var(--text-muted);cursor:pointer}
  .bn-item span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
  .bn-item.on{color:var(--primary-2)}
  .bn-item:active{background:var(--surface-2)}

  /* Kanban boards: seven columns can't shrink to fit a phone and stay readable,
     so on small screens go back to a horizontally-swipeable single row
     instead of the desktop grid. */
  .kanban{display:flex;overflow-x:auto;scroll-snap-type:x proximity;scroll-padding-left:14px}
  .kcol{scroll-snap-align:start;flex:0 0 84%}

  /* Tap targets: finger-sized inputs/buttons/tabs */
  .btn{min-height:40px}
  .btn-sm{min-height:34px}
  .inp,.sel,.ta{min-height:42px;font-size:16px}   /* 16px stops iOS zoom-on-focus */
  .tab,.stab{min-height:40px}

  /* Wide surfaces scroll within themselves, never the page */
  .tabs,.subtabs{overflow-x:auto;-webkit-overflow-scrolling:touch;flex-wrap:nowrap}
  .subtabs{padding-bottom:2px}
  .kanban{-webkit-overflow-scrolling:touch}
  .tbl th,.tbl td{padding:9px 11px}

  /* Modals fill the screen sensibly and scroll internally. The dialog itself
     is the scroll container, so .modal-acts sticks to its bottom edge: on a
     photo-upload modal (previews + description) the Cancel/Upload row is then
     visible and tappable from the moment it opens, instead of only after
     scrolling to the very end. Bottom padding clears the home indicator. */
  .overlay{padding:12px 12px calc(12px + env(safe-area-inset-bottom));align-items:flex-start}
  .modal,.modal.wide{max-width:100%;max-height:calc(100dvh - 24px - env(safe-area-inset-bottom));overflow-y:auto;padding:18px 18px 0}
  .modal-acts{flex-wrap:wrap;position:sticky;bottom:0;margin-top:16px;padding:12px 0 18px;background:var(--surface);border-top:1px solid var(--border)}
  /* The sticky row supplies its own bottom padding, so the dialog has none;
     modals rendered without an actions row need it back on their last child. */
  .modal>:last-child:not(.modal-acts){margin-bottom:18px}

  /* Calendar: the filter rail stops being a rail and stacks above the grid,
     where the mini month picker doubles as the fastest way to change date. */
  .cal-layout{grid-template-columns:1fr;gap:14px}
  .cal-side{position:static}
  .cal-hd{gap:8px}
  .cal-title{font-size:18px}
  .cal-period{width:100%;margin:0}
  /* Seven columns of pills can't fit a phone, so the grid keeps a usable
     minimum width and scrolls sideways rather than crushing the text. */
  .sched-month-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;border-radius:12px}
  .sched-dow-row,.sched-month-grid{min-width:660px}
  .sched-daybg{min-height:76px}
  .sched-week-grid{grid-template-columns:repeat(7,minmax(160px,1fr));overflow-x:auto;-webkit-overflow-scrolling:touch}

  /* Toasts span the width and sit above the bottom nav */
  .toasts{left:12px;right:12px;bottom:calc(70px + env(safe-area-inset-bottom))}
  .toast{min-width:0}
}
@media (max-width:560px){
  .page{padding:13px 11px calc(80px + env(safe-area-inset-bottom))}
  .card{padding:15px;border-radius:12px}
  .page-head-title{font-size:19px}
  .stat-val{font-size:22px}
  /* Stack every auto-fit grid into a single column so nothing gets squeezed */
  .g2,.g3,.g4{grid-template-columns:1fr}
  /* Action rows: let buttons wrap instead of overflowing; modal actions
     become full-width stacked buttons that are easy to hit */
  .sec-hd,.modal-acts,.save-bar{flex-wrap:wrap}
  .modal-acts .btn{flex:1 1 auto;justify-content:center}
  .save-bar{padding:10px 12px}
  /* Full-width primary actions read better on a narrow screen */
  .page-head-actions{width:100%}
  .page-head-actions .btn{flex:1;justify-content:center}
}

/* INSTALLER APP - a distinct, more "native"-feeling skin for the installer's
   mobile shell (InstallerShell in 19-shell.js tags its root with this class).
   Same screens/components used everywhere else in the site - kanban board,
   job detail, photo upload, sign-off, calendar - just restyled: a plain
   sans-serif face instead of the office's brand serif heading font, elevated
   rounded cards instead of flat panels, a livelier bottom nav, and tactile
   press feedback, so it reads as "an app" rather than "the website on a
   phone." Every rule is scoped under .installer-app so none of it touches
   the office/admin desktop experience. */
.installer-app{--font-heading:var(--font-body);flex-direction:column}
.installer-app .top-title{font-weight:800;font-size:16px;letter-spacing:-.01em}

.installer-app .topbar{border-bottom:none;box-shadow:0 2px 10px rgba(36,23,5,0.06)}
[data-theme="dark"] .installer-app .topbar{box-shadow:0 2px 10px rgba(0,0,0,0.3)}

/* Per-screen header (InstallerPageHeader) - the brand logo (or a back button
   on Job Detail) + title/subtitle + right-side icon actions, repeated at the
   top of every installer screen since there's no shared .topbar anymore. */
.inst-header{display:flex;align-items:center;gap:12px;margin-bottom:16px}
.inst-header-logo{width:38px;height:38px;object-fit:contain;flex-shrink:0}
.inst-header-text{flex:1;min-width:0}
.inst-header-title{font-weight:800;font-size:17px;letter-spacing:-.01em}
.inst-header-sub{font-size:12px;color:var(--text-muted);margin-top:1px}
.inst-header-actions{display:flex;gap:8px;flex-shrink:0}

/* Notification-bell unread badge. */
.inst-badge{position:absolute;top:-3px;right:-3px;min-width:16px;height:16px;padding:0 3px;border-radius:8px;
  background:var(--danger);color:#fff;font-size:9.5px;font-weight:800;display:flex;align-items:center;justify-content:center;line-height:1}

/* Profile card (Home + More) and the Home stat-tile grid beneath it. */
.inst-profile-card{display:flex;align-items:center;gap:12px}
.inst-stat-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:16px}
.inst-stat-tile{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:10px 6px;
  display:flex;flex-direction:column;align-items:center;gap:4px;box-shadow:0 2px 8px rgba(36,23,5,0.05);text-align:center}
[data-theme="dark"] .inst-stat-tile{box-shadow:0 2px 8px rgba(0,0,0,0.3)}
.inst-stat-tile b{font-size:17px}
.inst-stat-tile span{font-size:10.5px;color:var(--text-muted)}

.inst-link{font-size:12px;color:var(--primary-2);font-weight:700;cursor:pointer}

/* Today's Schedule row. */
.inst-appt-row{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--border)}
.inst-appt-row:last-child{border-bottom:none}
.inst-appt-time{font-size:12px;font-weight:800;color:var(--primary);width:58px;flex-shrink:0}

/* Quick Actions 2x2 grid. */
.inst-quick-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.inst-quick-tile{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:16px 6px;
  border:1px solid var(--border);border-radius:14px;background:var(--surface-2);color:var(--text);font-family:inherit;font-size:12px;font-weight:600;cursor:pointer}
.inst-quick-tile:disabled{opacity:.5;cursor:default}

/* Recent Updates row. */
.inst-update-row{display:flex;align-items:flex-start;gap:10px;padding:8px 0;border-bottom:1px solid var(--border)}
.inst-update-row:last-child{border-bottom:none}

/* More tab rows (theme toggle, log out). */
.inst-more-row{display:flex;align-items:center;gap:12px;width:100%;padding:12px 4px;border:none;background:none;
  color:var(--text);font-family:inherit;font-size:14px;font-weight:600;cursor:pointer;border-bottom:1px solid var(--border);text-align:left}
.inst-more-row:last-child{border-bottom:none}

/* My Jobs: status-count pills above the stacked sections. */
.inst-status-tabs{display:flex;gap:8px;margin-bottom:4px}
.inst-status-tab{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;padding:10px 6px;
  background:var(--surface);border:1px solid var(--border);border-radius:14px;font-size:11.5px;font-weight:700;color:var(--text-muted)}
.inst-status-tab span:first-child{font-size:12px}
.inst-status-count{font-size:15px;font-weight:800;color:var(--text)}

/* Stacked job sections (replaces the office's side-by-side kanban columns
   for this view) - same drag-and-drop target, just full-width and vertical. */
.inst-job-section{margin-bottom:16px}
.inst-job-section.drag{outline:2px dashed var(--primary-2);outline-offset:4px;border-radius:18px}
.inst-job-section-empty{display:flex;flex-direction:column;align-items:center;gap:4px;padding:22px 10px;
  border:1.5px dashed var(--border-2);border-radius:16px;color:var(--text-muted);font-size:12.5px}

/* Full-width job card - used on the Jobs list, Home's Next Job, and Job
   Detail's summary. */
.inst-job-card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:14px;margin-bottom:10px;
  box-shadow:0 3px 10px rgba(36,23,5,0.06);cursor:grab}
[data-theme="dark"] .inst-job-card{box-shadow:0 3px 10px rgba(0,0,0,0.3)}
.inst-job-ico{width:30px;height:30px;border-radius:9px;background:var(--primary-soft);color:var(--primary);display:flex;align-items:center;justify-content:center}
.inst-job-title{font-weight:800;font-size:14.5px;margin:8px 0 4px}
.inst-job-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text-muted);margin-bottom:2px}
.inst-job-actions{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}
.inst-job-actions .btn{flex:1;justify-content:center;min-width:0}

/* Job Detail summary card. */
.inst-job-summary-ico{width:36px;height:36px;border-radius:10px;background:var(--primary-soft);color:var(--primary);
  display:flex;align-items:center;justify-content:center;flex-shrink:0}

/* Job Detail's persistent bottom action bar. It sits ON TOP of .bottom-nav,
   whose height grows by the safe-area inset on notched phones - so the offset
   has to include that inset too, or the bar overlaps the nav's top row. The
   bar itself no longer touches the screen edge, so it needs no inset padding
   of its own. Height (~111px + inset) is what .inst-detail-page clears. */
.inst-action-bar{position:fixed;left:0;right:0;bottom:calc(64px + env(safe-area-inset-bottom));z-index:470;display:flex;background:var(--surface);
  border-top:1px solid var(--border);box-shadow:0 -4px 16px rgba(36,23,5,0.07);padding:8px 4px}
.inst-action-btn{flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;padding:6px 2px;border:none;background:none;
  color:var(--text);font-family:inherit;font-size:9.5px;font-weight:600;cursor:pointer;text-decoration:none;text-align:center}
.inst-action-btn.primary{color:var(--primary)}
/* Bottom clearance on the installer/estimator Job Detail screens, on top of
   the .page gutter, for the two stacked fixed bars: bottom nav (64px + inset)
   plus the action bar itself (~111px). The old inline paddingBottom:84 left
   the page ~11px short, so the last thing on a tab - the Photos tab's "Upload
   after photos" button, say - sat just under the action bar's edge. */
.inst-detail-page{padding-bottom:calc(104px + env(safe-area-inset-bottom))}
.inst-action-btn.disabled{color:var(--text-muted);opacity:.5;pointer-events:none}

/* Calendar day-detail drawer beneath the month grid. */
.inst-day-drawer{margin-top:12px;border:1px solid var(--border);border-radius:16px;background:var(--surface);overflow:hidden}
.inst-day-drawer-handle{width:100%;display:flex;align-items:center;gap:8px;padding:12px 14px;border:none;background:none;
  color:var(--text);font-family:inherit;font-size:13px;font-weight:700;cursor:pointer}
.inst-day-drawer-count{margin-left:auto;font-size:11.5px;font-weight:600;color:var(--text-muted)}
.inst-day-drawer-body{padding:0 14px 14px}

/* Compact 7-column month grid, mobile app only (InstallerMonthGrid in
   13-calendar.js). Deliberately NOT reusing the office .sched-month-grid,
   which forces a 660px min-width for its spacious multi-line cells - that
   would silently force horizontal scrolling to reach Fri/Sat/Sun on a phone.
   grid-template-columns:repeat(7,1fr) with no min-width means this is always
   exactly as wide as its container, so all seven days stay on screen. */
.inst-month-grid-wrap{width:100%}
.inst-week-header{display:grid;grid-template-columns:repeat(7,1fr);gap:3px;margin-bottom:4px}
.inst-week-header > div{text-align:center;font-size:10.5px;font-weight:700;color:var(--text-muted)}
.inst-month-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:3px}
.inst-month-cell{aspect-ratio:1/1.05;min-width:0;border-radius:9px;background:var(--surface);
  border:1px solid var(--border);padding:3px 2px;cursor:pointer;display:flex;flex-direction:column;align-items:center;overflow:hidden}
.inst-month-cell.dim{opacity:.4}
.inst-month-cell.today{border-color:var(--primary-2);border-width:1.5px}
.inst-month-cell.selected{background:var(--primary-soft);border-color:var(--primary)}
.inst-month-cell-num{font-size:11px;font-weight:700;margin-bottom:2px}
.inst-month-cell-pill{width:100%;font-size:7.5px;line-height:1.3;padding:1px 2px;border-radius:4px;text-align:center;
  overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
.inst-month-cell-dots{display:flex;gap:2px;justify-content:center;margin-top:1px}
.inst-month-cell-dot{width:4px;height:4px;border-radius:50%}
/* Dots need a solid fill to read at 4px - override the soft/pale pill
   background the shared .appt-type-* rules give .inst-month-cell-dot below. */
.inst-month-cell-dot.appt-type-measurement{background:var(--type-measurement)}
.inst-month-cell-dot.appt-type-quote{background:var(--type-quote)}
.inst-month-cell-dot.appt-type-carpet{background:var(--type-carpet)}
.inst-month-cell-dot.appt-type-hardfloor{background:var(--type-hardfloor)}
.inst-month-cell-dot.appt-type-pickup{background:var(--type-pickup)}
.inst-month-cell-dot.appt-type-followup{background:var(--type-followup)}
.inst-month-cell-dot.appt-type-invoice{background:var(--type-invoice)}
.inst-month-cell-dot.appt-type-default{background:var(--type-default)}
.inst-month-cell-dot.appt-type-unavailable{background:var(--type-unavailable)}

/* Elevated, rounder cards - kanban columns/tiles and the .card panels used
   throughout job detail/photos/sign-off (all render inside this shell). */
.installer-app .kcol{border-radius:18px;background:var(--surface)}
.installer-app .kcard{border-radius:14px;box-shadow:0 3px 10px rgba(36,23,5,0.08);border-color:transparent}
[data-theme="dark"] .installer-app .kcard{box-shadow:0 3px 10px rgba(0,0,0,0.35)}
.installer-app .card{border-radius:18px;box-shadow:0 3px 12px rgba(36,23,5,0.07);border-color:transparent}
[data-theme="dark"] .installer-app .card{box-shadow:0 3px 12px rgba(0,0,0,0.35)}

/* Bottom nav: a filled pill behind the whole active tab (icon + label)
   instead of a plain colour change - closer to a native tab bar. */
.installer-app .bn-item{position:relative;transition:color .15s ease;z-index:1}
.installer-app .bn-item.on{color:var(--primary)}
.installer-app .bn-item.on::before{
  content:"";position:absolute;inset:2px 6px;border-radius:14px;background:var(--primary-soft);z-index:-1;
}

/* Buttons/cards: bigger radius + a short press animation for tactile feedback. */
.installer-app .btn{border-radius:12px}
.installer-app .btn,.installer-app .icon-btn,.installer-app .kcard{transition:transform .1s ease,box-shadow .15s ease,background .15s ease}
.installer-app .btn:active,.installer-app .icon-btn:active,.installer-app .kcard:active{transform:scale(.96)}

/* Page switches fade/slide in rather than snapping - reads as app navigation
   instead of an instant website page-swap. Targets the direct child of
   .page (the view's own root element) since that's what actually remounts
   on navigation (ErrorBoundary above it is keyed per view) - .page itself
   never unmounts, so an animation on .page directly would only ever play once. */
.installer-app .page > *{animation:installerPageIn .18s ease}
@keyframes installerPageIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

/* â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
   QUOTE BUILDER  (Quote tab â†’ Create Quote / Default quote)
   Everything here is namespaced under .qb. The builder is deliberately styled
   as a Xero-style DOCUMENT â€” light grey ground, white card, thin grey rules,
   Segoe UI, blue actions â€” rather than picking up the app's brown/gold theme
   tokens, so it keeps that look in both light and dark mode. That is the one
   place in this stylesheet where fixed colours beat the theme variables: the
   panel is meant to read like the piece of paper it prints to.
   â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â• */
/* The document itself keeps its own light palette (it's a piece of paper, not a
   panel), but anything that reads as an app control inside it — the item
   picker's actions and chips — uses the Royal Floors brown/gold instead.
   --qb-brand-ink is the darker brown so gold-on-white stays legible. */
/* --qb-blue was the Xero blue this tab shipped with; it now points at the
   brand brown so every accent (links, focus rings, the edit hover) reads like
   the rest of the site. The name is kept so the many usages below don't each
   need rewriting. The Save button gets the full brand gradient separately. */
.qb{--qb-blue:var(--primary);--qb-line:var(--border);--qb-ink:var(--text);--qb-dim:var(--text-dim);--qb-grey:var(--bg);
  --qb-brand:var(--primary-2);--qb-brand-ink:var(--primary);--qb-brand-soft:var(--primary-soft);
  font-family:var(--font-body);color:var(--qb-ink);background:var(--qb-grey);
  border:1px solid var(--qb-line);border-radius:14px;padding:16px}
.qb *{font-family:inherit}

/* Action bar - same sticky-card treatment as .save-bar elsewhere in the app. */
.qb-bar{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px;flex-wrap:wrap;
  background:var(--surface);border:1px solid var(--border-2);border-radius:14px;padding:11px 16px;box-shadow:var(--shadow-sm)}
.qb-bar-l{min-height:28px;display:flex;align-items:center}
.qb-bar-r{display:flex;gap:8px;flex-wrap:wrap}
.qb-btn{padding:8px 14px;border-radius:8px;border:1px solid var(--border);background:var(--surface-2);color:var(--qb-ink);
  font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;font-family:inherit;transition:.13s}
.qb-btn:hover{background:var(--surface-3)}
.qb-btn-primary{background:linear-gradient(135deg,var(--primary),var(--primary-2));border-color:transparent;color:#fff}
.qb-btn-primary:hover{filter:brightness(1.08)}
.qb-btn-sm{padding:4px 9px;font-size:12px}
.qb-btn:focus-visible,.qb-link:focus-visible{outline:2px solid var(--primary-2);outline-offset:2px}
/* "Send to <org>" selector in the toolbar - sits inline with the buttons. */
.qb-sendto{display:flex;align-items:center;gap:6px;white-space:nowrap}
.qb-sendto .qb-lbl{margin-bottom:0;font-size:12px}
.qb-sendto .qb-inp.boxed{width:auto;min-width:150px;padding:7px 10px;font-weight:600}
.qb-flash{background:var(--success-soft);color:var(--success);border:1px solid transparent;border-radius:8px;padding:5px 11px;font-size:12.5px;font-weight:600}

/* Card + header block */
.qb-card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:20px;margin-bottom:14px;box-shadow:var(--shadow-sm)}
.qb-card:last-child{margin-bottom:0}
.qb-sec-h{font-size:14px;font-weight:700;margin-bottom:10px}
.qb-note{font-size:12.5px;color:var(--qb-dim);line-height:1.6;margin-bottom:14px;max-width:70ch}
.qb-head{display:grid;grid-template-columns:minmax(260px,1fr) minmax(380px,1.35fr);gap:28px;
  padding-bottom:18px;border-bottom:1px solid var(--qb-line);margin-bottom:6px}
.qb-head-r{display:flex;flex-direction:column;gap:12px}
.qb-tax-select{display:flex;align-items:center;justify-content:flex-end;gap:10px}
.qb-tax-select .qb-inp{width:170px}
.qb-tax-select .qb-lbl{margin-bottom:0}
.qb-head-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
.qb-title-block{margin-top:10px}
.qb-lbl{display:block;font-size:11.5px;font-weight:600;color:var(--qb-dim);margin-bottom:4px}

/* Inputs. Borderless by default (a document grid reads better without a box
   on every cell), boxed where the field is a header field rather than a table
   cell - same visual language as the app's real .inp, tuned for a dense grid. */
.qb-inp{width:100%;border:1px solid transparent;background:transparent;border-radius:6px;
  padding:6px 7px;font-size:13px;color:var(--qb-ink);outline:none;resize:none;font-family:inherit}
.qb-inp:hover{border-color:var(--qb-line)}
.qb-inp:focus{border-color:var(--qb-blue);background:var(--surface);box-shadow:0 0 0 2px var(--primary-soft)}
.qb-inp.boxed{border-color:var(--qb-line);background:var(--input-bg)}
.qb-inp.r{text-align:right}
.qb-inp::placeholder{color:var(--text-muted)}
.qb-ta{overflow:hidden;line-height:1.5;min-height:30px}
select.qb-inp,.qb-sel{cursor:pointer}

/* Contact chip + autocomplete popovers */
.qb-contact-chip{display:inline-flex;align-items:center;gap:10px;background:var(--surface-2);border:1px solid var(--qb-line);
  border-radius:9px;padding:7px 9px;max-width:100%}
.qb-avatar{width:30px;height:30px;flex-shrink:0;border-radius:10px;background:linear-gradient(135deg,var(--primary),var(--primary-2));color:#fff;
  display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700}
.qb-avatar.sm{width:24px;height:24px;font-size:10.5px;border-radius:8px}
.qb-contact-name{font-size:13.5px;font-weight:600;line-height:1.3;overflow:hidden}
.qb-contact-sub,.qb-pop-sub{display:block;font-size:11.5px;font-weight:400;color:var(--qb-dim)}
.qb-chip-x,.qb-row-x{border:none;background:transparent;color:var(--qb-dim);font-size:18px;line-height:1;
  cursor:pointer;padding:0 4px;border-radius:5px}
.qb-chip-x:hover,.qb-row-x:hover{color:var(--danger);background:var(--danger-soft)}
.qb-pop-wrap{position:relative}
.qb-pop{position:absolute;z-index:40;top:calc(100% + 3px);left:0;min-width:240px;background:var(--surface);
  border:1px solid var(--border-2);border-radius:10px;box-shadow:var(--shadow);padding:5px;max-width:340px}
.qb-pop.wide{min-width:320px}
/* Item picker menu — placed from the button's rect so the table's horizontal
   scroller can't clip it. See CqItemPicker. It's a flex column with its
   max-height set inline (from place(), clamped to the viewport): the search
   box, "no item" row and footnote are flex-shrink:0 so only the scrollable
   list itself gives up height when space is tight. */
/* z-index 550 — above sticky in-page chrome (.save-bar 60, .inst-action-bar
   470, .bottom-nav 480, .nav-backdrop 490, .nav 500, .overlay 520) so the menu
   never opens underneath any of it - including the modal it may itself be
   opened from - but below .toasts (600) so a toast can still surface on top. */
.qb-pop-fixed{position:fixed;z-index:550;display:flex;flex-direction:column;overflow:hidden}
.qb-pop-list{flex:1 1 auto;min-height:60px;max-height:240px;overflow-y:auto}
.qb-pop-search{width:100%;flex-shrink:0;border:1px solid var(--qb-line);border-radius:6px;padding:6px 8px;font-size:13px;
  margin-bottom:5px;outline:none;background:var(--input-bg);color:var(--qb-ink);font-family:inherit}
.qb-pop-search:focus{border-color:var(--qb-blue)}
.qb-pop-row{display:flex;align-items:center;gap:9px;width:100%;flex-shrink:0;text-align:left;border:none;background:none;
  padding:7px 9px;border-radius:6px;font-size:13px;color:var(--qb-ink);cursor:pointer;font-family:inherit}
.qb-pop-row:hover,.qb-pop-row.qb-pop-hi{background:var(--qb-brand-soft)}
.qb-pop-row.grow{flex:1;justify-content:space-between}
/* "＋ Create new item" — pinned above the divider, in the brand gold rather
   than the document blue, so it reads as an action and not another item. */
.qb-pop-new{color:var(--qb-brand-ink);font-weight:600}
.qb-pop-plus{font-size:15px;line-height:1;margin-right:-3px}
.qb-pop-div{height:1px;flex-shrink:0;background:var(--qb-line);margin:4px 2px 5px}
/* Library rows: code chip + name + a one-line preview of the description. */
.qb-pop-lib{align-items:flex-start}
.qb-pop-lib-txt{min-width:0;flex:1;line-height:1.35}
.qb-pop-code{flex-shrink:0;min-width:24px;margin-top:1px;padding:1px 6px;border-radius:9px;text-align:center;
  background:var(--qb-brand-soft);color:var(--qb-brand-ink);font-size:11px;font-weight:700}
.qb-pop-1line{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px}
.qb-pop-item{display:flex;align-items:center}
.qb-pop-edit{border:none;background:none;color:var(--qb-dim);cursor:pointer;padding:5px 7px;border-radius:5px}
.qb-pop-edit:hover{background:var(--qb-brand-soft);color:var(--qb-blue)}
.qb-pop-price{color:var(--qb-dim);font-size:12px;white-space:nowrap}
.qb-pop-empty,.qb-empty{padding:14px 10px;font-size:12.5px;color:var(--qb-dim);text-align:center}
.qb-cell-btn{width:100%;text-align:left;border:1px solid transparent;background:transparent;border-radius:6px;
  padding:6px 7px;font-size:13px;color:var(--qb-ink);cursor:pointer;font-family:inherit}
.qb-cell-btn:hover{border-color:var(--qb-line)}
.qb-cell-btn.muted-text{color:var(--text-muted)}

/* Line items table */
.qb-table-wrap{overflow-x:auto}
.qb-table{width:100%;border-collapse:collapse;font-size:13px;min-width:870px}
.qb-table th{background:var(--surface-2);color:var(--qb-dim);font-size:11px;font-weight:700;letter-spacing:.04em;
  text-transform:uppercase;text-align:left;padding:8px 7px;border-bottom:1px solid var(--qb-line);white-space:nowrap}
.qb-table td{padding:2px 3px;border-bottom:1px solid var(--qb-line);vertical-align:top}
.qb-table th.r,.qb-table td.r{text-align:right}
.qb-table-plain{min-width:0}
.qb-table-plain td{padding:8px 7px;vertical-align:middle}
.qb-amount{padding-top:9px!important;font-variant-numeric:tabular-nums;white-space:nowrap}
.qb-w-drag{width:26px}.qb-w-item{width:130px}.qb-w-qty{width:58px}.qb-w-price{width:96px}
.qb-w-disc{width:78px}.qb-w-acct{width:150px}.qb-w-tax{width:158px}.qb-w-amt{width:104px}.qb-w-del{width:30px}
.qb-drag{display:inline-block;cursor:grab;color:var(--text-muted);padding:7px 2px;user-select:none;line-height:1}
.qb-drag:active{cursor:grabbing}
.qb-dragging{opacity:.45}
.qb-link{border:none;background:none;color:var(--qb-blue);font-size:13px;font-weight:600;cursor:pointer;
  padding:9px 2px 0;display:inline-block;font-family:inherit}
.qb-link:hover{text-decoration:underline}

/* Totals + terms */
.qb-foot{display:flex;justify-content:space-between;gap:28px;flex-wrap:wrap;padding-top:18px;margin-top:6px;
  border-top:1px solid var(--qb-line)}
.qb-terms{flex:1 1 340px;max-width:520px}
.qb-totals{flex:0 0 280px;min-width:240px}
.qb-tot-row{display:flex;justify-content:space-between;gap:20px;padding:6px 0;font-size:13.5px}
.qb-tot-row span{color:var(--qb-dim)}
.qb-tot-row b{font-variant-numeric:tabular-nums}
.qb-tot-grand{border-top:2px solid var(--qb-ink);margin-top:6px;padding-top:10px;font-size:15px}
.qb-tot-grand span{color:var(--qb-ink);font-weight:700}
.qb-tot-grand b{font-size:19px}

/* Item picker groups + footnote */
.qb-pop-group{font-size:10.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--qb-dim);
  padding:9px 9px 4px;position:sticky;top:0;background:var(--surface)}
.qb-pop-foot{font-size:11px;color:var(--qb-dim);flex-shrink:0;padding:7px 9px 3px;border-top:1px solid var(--qb-line);margin-top:4px}

/* Save-state chips in the action bar - same dot+text idiom as .save-status
   elsewhere in the app, so "Unsaved changes" reads the same everywhere. */
.qb-dirty{display:inline-flex;align-items:center;gap:8px;font-size:12.5px;font-weight:600;color:var(--warning)}
.qb-dirty::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--warning);flex-shrink:0}
.qb-dirty.ok{color:var(--success)}
.qb-dirty.ok::before{background:var(--success)}
.qb-err{display:inline-flex;align-items:center;gap:8px;font-size:12.5px;font-weight:600;color:var(--danger)}
.qb-err::before{content:"";width:8px;height:8px;border-radius:50%;background:var(--danger);flex-shrink:0}

@media (max-width:1180px){
  .qb-head{grid-template-columns:1fr;gap:18px}
  .qb-tax-select{justify-content:flex-start}
}
@media (max-width:760px){
  .qb-head-grid{grid-template-columns:1fr 1fr}
  .qb{padding:10px}
  .qb-card{padding:14px}
}

/* Printing the app window is never the way to get a quote on paper - the real
   document is the server-built PDF behind the Print / PDF button. If someone
   hits Ctrl+P anyway, strip the app chrome so the page yields the quote rather
   than the sidebar and a row of tabs. */
@media print{
  .nav,.topbar,.tabs,.subtabs,.qb-bar,.save-bar,.toasts{display:none!important}
  body,.app,.main,.page-narrow{overflow:visible!important;height:auto!important;display:block!important}
  .qb{background:#fff;border:none;padding:0}
  .qb-card{border:none;padding:0;margin:0}
  .qb-inp{border-color:transparent!important;box-shadow:none!important}
  .qb-table{min-width:0}
  .qb-w-drag,.qb-w-del,.qb-w-acct,.qb-w-tax,.qb-pop{display:none!important}
}

/* ── Website Enquiries cards ──────────────────────────────────────────────
   The Website Enquiries page (Customer workspace) shows each lead from the
   public enquiry form as a rich card: contact header + status, a grid of the
   structured fields the form sent (parsed from the customer's notes - see
   parseEnquiry in 28-insurance.js), the Plans/Message text, and the lead
   actions. Purely presentational; every field degrades gracefully when absent. */
.enq-toolbar{align-items:center}
.enq-card{padding:18px 20px}
.enq-head{display:flex;align-items:flex-start;gap:14px}
.enq-name{font-weight:700;font-size:15px;cursor:pointer}
.enq-name:hover{color:var(--primary-2)}
.enq-contact{font-size:12.5px;margin-top:3px;word-break:break-word}
.enq-stamp{font-size:12px;white-space:nowrap;flex-shrink:0;padding-top:3px}
.badge.enq-status-new{background:var(--info-soft);color:var(--info)}
.badge.enq-status-contacted{background:var(--success-soft);color:var(--success)}
.badge.enq-status-archived{background:var(--surface-3);color:var(--text-dim)}
.enq-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px 20px;
  margin-top:16px;padding-top:16px;border-top:1px solid var(--border)}
.enq-field{min-width:0}
.enq-field-label{font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}
.enq-field-value{font-size:13.5px;color:var(--text);margin-top:3px;word-break:break-word}
.enq-block{margin-top:16px;padding-top:16px;border-top:1px solid var(--border)}
.enq-block-text{font-size:13.5px;color:var(--text);margin-top:5px;line-height:1.55;white-space:pre-wrap;word-break:break-word}
.enq-message{font-size:12.5px;margin-top:12px;white-space:pre-wrap;word-break:break-word}
.enq-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px;padding-top:16px;border-top:1px solid var(--border)}
@media (max-width:600px){
  .enq-stamp{font-size:11px}
  .enq-actions .btn{flex:1 1 auto;justify-content:center}
}
/* Call notes on an enquiry card - what staff discussed when they rang, newest
   first, so the next person to pick the lead up isn't ringing cold. */
.enq-note{margin-top:10px;padding:10px 12px;border-radius:9px;background:var(--surface-2);border:1px solid var(--border)}
.enq-note:first-of-type{margin-top:6px}
.enq-note-head{display:flex;align-items:flex-start;gap:10px}
.enq-note-meta{flex:1;min-width:0;font-size:11.5px;font-weight:700;color:var(--text-dim)}
/* Edit/delete sit quietly until the note is hovered, so a list of notes reads
   as text rather than as a row of buttons. Always visible on touch, where
   there is no hover to reveal them. */
.enq-note-acts{display:flex;gap:4px;flex-shrink:0;opacity:0;transition:.12s}
.enq-note:hover .enq-note-acts,.enq-note:focus-within .enq-note-acts{opacity:1}
@media (hover:none){.enq-note-acts{opacity:1}}
.icon-btn.sm{width:26px;height:26px;border-radius:6px;font-size:12px}
.icon-btn.sm.danger:hover{background:var(--danger);border-color:transparent;color:#fff}
.enq-note-meta .muted{font-weight:500}
.enq-note-text{font-size:13px;color:var(--text);margin-top:4px;line-height:1.55;white-space:pre-wrap;word-break:break-word}
