:root{
  /* The ESS Maintenance palette, verbatim, so the two apps read as one system.
     That app is dark by default and flips to light when the machine is, and this
     one now does the same — a tech's phone in the sun goes light on its own. */
  --bg:#0f1216; --panel:#171b21; --panel2:#1e242c; --line:#2a323c;
  --ink:#e6eaef; --muted:#8d99a8;
  /* Placeholders sit WELL below muted on purpose. --muted is real secondary text,
     which is why an example in an empty box read as a saved note. Joel, twice:
     the grey still read as typed, it has to be much fainter. This is a faint
     grey, close to the box itself (about 2.2:1): obviously a hint, and the label
     above every one of these carries the actual information. */
  --ph:#566170;
  --accent:#4f9cf9; --accent2:#2b6fc4; --accent-soft:#1b2a3a;
  /* Text that sits ON an accent fill. Blue is light enough that this has to be
     stated rather than assumed white. */
  --on-accent:#fff;
  --ok:#3fb950; --ok-soft:#16281a;
  --warn:#d29922; --warn-soft:#2b2414;
  /* Internal notes sit brighter than the other warn-tinted things: a note is
     read, not glanced at, and the old #2b2414 was almost the panel behind it.
     This is as bright as it goes without the note text dropping under the 4.5:1
     it needs to stay comfortable — it measures 5.6:1, against 2.6:1 separation
     from the panel, so the block reads as its own thing at a glance. */
  --note-bg:#6b5a2e;
  --danger:#f85149; --danger-soft:#2b1a1a;
  /* A reply WE sent, in a ticket thread. --accent-soft was the obvious choice
     and measures 1.18:1 against the card behind it, which is no tint at all on
     a dark screen — the block would have been distinguished by a 3px edge and
     nothing else. This measures 1.48:1 against the card and still carries the
     body text at 9.65:1. */
  --mine-bg:#223a54;
  --meta-mine:#a6b3c4;   /* 5.48:1 on --mine-bg */
  --meta-note:#ece3cd;   /* 5.26:1 on --note-bg */
  --bar:#171b21; --bar-line:#2a323c;
  --field:#0f1216;                 /* an input's own background */
  /* A secondary button used to be panel-on-panel: .btn.sec's background was
     --panel and every card and the save bar it sits on is --panel too, so "View
     PDF" and "Save" were a 1px outline and nothing else. These two are measured:
     the fill reads as a button, and the edge clears 3:1 against the panel behind
     it, which is what a control's boundary needs to be seen at all. */
  --sec-bg:#2b3440; --sec-line:#5a6a80;   /* edge 3.13:1 on panel, label 10.4:1 */
  /* A card's own edge. --line is right for rules and table borders INSIDE a
     card, but it was doing double duty as the card's outline too, where it
     measured 1.35:1 against the page and the sections ran together. 2.75:1 is
     enough to read as a boundary without every card looking boxed in. */
  --card-line:#4f5c6b;
  /* The header's exact outer height, including its bottom border. The tab strip
     sticks directly below it, and that offset used to be the hard-coded 51px
     that the bar happened to measure — so growing the logo left either a gap or
     an overlap when the page scrolled. One number, used by both. */
  /* The phone's own furniture. index.html asks for viewport-fit=cover, which
     is what lets the app run edge to edge behind the rounded corners - but it
     also puts the page UNDER the status bar and the home indicator, and
     nothing here ever made room for them. On Josh's iPhone the clock and the
     battery were sitting on top of the wordmark and the account button.

     Named rather than written as env() at each use, for two reasons: every
     bar that needs room reads the same number, and a test can set these to a
     pretend 47px and check that things actually moved - env() itself cannot
     be faked in a headless browser. */
  --safetop: env(safe-area-inset-top, 0px);
  --safebot: env(safe-area-inset-bottom, 0px);
  --safeleft: env(safe-area-inset-left, 0px);
  --saferight: env(safe-area-inset-right, 0px);
  /* The value for the instant before measureBar() reads the real one. It has
     to carry the inset too, or the tab strip starts the page tucked under the
     header on every load. */
  --barh:calc(60px + var(--safetop));
  /* The nav's height, for the open-ticket strip that sticks below it.
     Measured at runtime; this is only the value for the instant before. */
  --tabsh:44px;
  --chip-dim:#9caab9;    /* 5.31:1 on --sec-bg */
  --radius:10px;
  --shadow:0 1px 2px rgba(0,0,0,.3),0 4px 14px rgba(0,0,0,.25);
  --font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  /* Not cosmetic. Without this the browser draws a <select>'s popup list with
     the operating system's light background while the options inherit the
     near-white --ink set on the select, so every item in every dropdown is
     white text on white. Declaring the scheme hands the popup, the scrollbars
     and the date pickers to the browser's dark chrome, where they are legible. */
  color-scheme:dark;
}
@media (prefers-color-scheme: light){
  :root{
    --bg:#f4f6f8; --panel:#fff; --panel2:#f0f3f6; --line:#dce1e7;
    --ink:#1b2028; --muted:#66717f;
    /* Its own value: the same hex moves opposite ways in the two themes.
       A light, light grey (about 1.9:1 on --panel2): a hint, never a value. */
    --ph:#b4bcc7;
    --accent:#1f6feb; --accent2:#1a5cc4; --accent-soft:#e8f0fd;
    --ok:#1a7f37; --ok-soft:#e6f4ea;
    --warn:#9a6700; --warn-soft:#fdf5e6; --note-bg:#fbedbe;
    --danger:#cf222e; --danger-soft:#fdecea;
    --field:#fff;
    --sec-bg:#e9edf2; --sec-line:#838f9e;   /* edge 3.29:1 on white */
    --chip-dim:#5b6673;                    /* 4.97:1 on --sec-bg */
    --mine-bg:#d8e6fb;                     /* 1.26:1 on a white card, ink 12.95:1 */
    --meta-mine:#525c68;                   /* 5.38:1 on --mine-bg */
    --meta-note:#5e5847;                   /* 6.06:1 on --note-bg */
    --card-line:#97a3b0;                   /* 2.57:1 on a white card */
    color-scheme:light;
    --shadow:0 1px 2px rgba(20,28,40,.06),0 4px 14px rgba(20,28,40,.05);
  }
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
/* One knob for the whole interface: every font-size below is in rem, so changing
   this root value scales all of it. Setup → Company writes it per device. */
html{font-size:19.5px}
/* The phone default, before app.js is parsed. 19.5px on a 390px screen wraps
   the header onto two lines and shows four of eleven tabs; this is the size the
   rest of this file was drawn at. app.js holds the same two numbers at the same
   breakpoint (PHONE_MAX) and overrides this inline once it runs - keep the
   three in step. */
@media (max-width:700px){ html{font-size:16px} }
body{background:var(--bg);color:var(--ink);font-family:var(--font);font-size:1rem;line-height:1.45;-webkit-text-size-adjust:100%}
button{font:inherit;color:inherit}
/* Not `color:inherit` — a control inside any coloured panel would take that
   panel's text colour and disappear against its own white background. */
input,select,textarea{font:inherit;color:var(--ink)}
a{color:var(--accent)}

/* ---- app chrome */
/* The header follows the panel colour, exactly as ESS Maintenance does, so in
   light mode it is a white bar and in dark mode it is the same near-black. The
   wordmark has two colourways and swaps with it. */
.topbar{position:sticky;top:0;z-index:30;background:var(--panel);color:var(--ink);
  /* The status bar's room is padding, not margin, so the panel colour still
     runs up behind the clock instead of leaving a strip of page showing. */
  padding:calc(10px + var(--safetop)) calc(14px + var(--saferight)) 10px calc(14px + var(--safeleft));
  min-height:var(--barh);
  /* On a phone the row cannot hold the wordmark, the name and both buttons. Let
     it wrap onto a second line rather than squeezing the buttons, which is what
     turned "+ New ticket" into three stacked words. */
  display:flex;align-items:center;gap:12px;flex-wrap:wrap;border-bottom:1px solid var(--line)}
.topbar h1{font-size:0.875rem;margin:0;font-weight:500;letter-spacing:.2px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;color:var(--muted)}
/* The wordmark is the only branding left in the header now that "Pest" is gone,
   so it gets the room. Anything taller than this and the bar starts eating the
   screen on a phone. */
.topbar .logo{height:38px;width:auto;flex:0 0 auto;display:block}
.topbar .logo.light{display:block}
.topbar .logo.dark{display:none}
@media (prefers-color-scheme: light){
  .topbar .logo.light{display:none}
  .topbar .logo.dark{display:block}
}
/* Takes the slack the "Pest" wordmark used to hold, so the name sits left of the
   buttons and the buttons stay pinned right. */
.topbar .who{font-size:0.84rem;opacity:.85;white-space:nowrap;margin-left:auto}
.topbar button{white-space:nowrap;background:var(--panel2);border:1px solid var(--line);color:var(--ink);border-radius:6px;
  padding:5px 10px;font-size:0.8125rem;cursor:pointer}
.topbar button:hover{border-color:var(--accent)}
/* The one button in the header that starts something rather than ending it. */
.topbar button.primary{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600}
.topbar button.primary:hover{background:var(--accent2);border-color:var(--accent2)}
/* A phone's header is five fixed things in a row with nothing that can give, so
   whether it holds comes down to tens of pixels: at 12px gaps the row wants
   361px, and a 360px-wide phone put "Sign out" on a line of its own. Tightening
   the gaps and the side padding buys the 6px that keeps every screen from 360
   up on one row. Narrower than that it still wraps, which is fine - the tab
   strip below measures this bar at runtime rather than assuming a height. */
@media (max-width:430px){
  /* Keeps the side insets. Written as plain 11px once, this quietly undid the
     safe-area padding above on exactly the phones that need it. */
  .topbar{gap:9px;
    padding-left:calc(11px + var(--safeleft));
    padding-right:calc(11px + var(--saferight))}
}
.check{display:flex;align-items:center;gap:8px;cursor:pointer}
.check input{margin:0}
/* Inputs are width:100% everywhere, which in a flex row stretched a checkbox across the card and shoved its words to the far side. */
.check input[type=checkbox]{width:auto;flex:0 0 auto}
.check.cushionrow{margin-top:12px;justify-content:flex-start;text-transform:none;letter-spacing:0;font-size:0.95rem;font-weight:600}
.tabs{display:flex;gap:2px;background:var(--panel);border-bottom:1px solid var(--line);position:sticky;top:var(--barh);z-index:29;overflow-x:auto;-webkit-overflow-scrolling:touch}
.tabs button{flex:0 0 auto;background:none;border:0;border-bottom:3px solid transparent;padding:11px 15px;
  font-size:0.875rem;color:var(--muted);cursor:pointer;white-space:nowrap;border-radius:8px 8px 0 0}
/* Which screen you are on should be readable from across the shop, not inferred
   from a hairline. Tint, weight and a thick rule, all saying the same thing. */
.tabs button.on{color:var(--on-accent);background:var(--accent);border-bottom-color:var(--accent2);font-weight:700}
.tabs button:hover:not(.on){background:var(--accent-soft);color:var(--accent)}
/* There are eleven tabs and a phone holds four, so the strip has always been a
   sideways scroller - but nothing on screen said so. What a tech saw was the
   word "Dashboard" sliced down the middle at the left edge, which is what Aaron
   reported as the app being too big for the screen: the page fit, the strip was
   just scrolled and silent about it.
 *
 * Same two-layer trick the views rail uses down its bottom edge, turned on its
 * side. The covers are `local`, so they sit at the two ends of the CONTENT and
 * scroll away with it; the shadows are `scroll`, pinned to the element. At rest
 * the left cover hides the left shadow, so only the right edge is shaded; scroll
 * and the covers slide off to reveal the other one. No JavaScript needs to know
 * where the strip is. */
.tabs, .tickettabs {
  background-image:
    linear-gradient(to right, var(--panel) 60%, transparent),
    linear-gradient(to left,  var(--panel) 60%, transparent),
    linear-gradient(to right, rgba(0,0,0,.30), transparent),
    linear-gradient(to left,  rgba(0,0,0,.30), transparent);
  background-repeat: no-repeat;
  background-position: left center, right center, left center, right center;
  background-size: 26px 100%, 26px 100%, 16px 100%, 16px 100%;
  background-attachment: local, local, scroll, scroll;
}
/* Forms stay in a readable measure; data-heavy screens take the whole window. */
main{max-width:1100px;margin:0 auto;padding:16px 14px 80px}
main.wide{max-width:none;padding:16px 20px 80px}
/* A 1500px-wide text input is no easier to fill in than a 500px one. */
main.wide .formish .row > *{max-width:420px}

/* ---- primitives */
.card{background:var(--panel);border:1px solid var(--card-line);border-radius:var(--radius);padding:14px;margin-bottom:14px;box-shadow:var(--shadow)}
.card h2{margin:0 0 10px;font-size:0.9375rem;font-weight:650}
.card h3{margin:14px 0 8px;font-size:0.82rem;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);font-weight:700}
.row{display:flex;gap:10px;flex-wrap:wrap}
.row>*{flex:1 1 150px;min-width:0}
/* .row exists for form fields, which should share the width. A badge and a
   Call button are fixed things that happen to sit in one, and stretching them
   to 570px each is how a pill ends up looking like a banner. */
.row>.pill,.row>.callbtn,.row>.chip{flex:0 0 auto}
label{display:block;font-size:0.8rem;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);font-weight:700;margin:0 0 4px}
input,select,textarea{width:100%;padding:10px 11px;border:1px solid var(--line);border-radius:8px;
  background:var(--field);color:var(--ink);-webkit-text-fill-color:var(--ink);font-size:1rem}
input:focus,select:focus,textarea:focus{outline:2px solid var(--accent);outline-offset:-1px;border-color:var(--accent)}
/* Belt to color-scheme's braces: where the browser does honour it, this paints
   the popup itself rather than relying on it inheriting anything. */
select option,select optgroup{background:var(--panel2);color:var(--ink)}
textarea{min-height:76px;resize:vertical}
/* A placeholder has to look like a prompt, never like content.
 *
 * There was no rule here at all, so every box fell back to the browser's own
 * placeholder colour — which on this dark theme is close enough to real text
 * that Morgan read the example in the empty Notes box as a saved note and
 * reported it as appearing on every customer. It was on every customer: it is
 * a placeholder. The list above it said "Nothing yet." and lost the argument
 * to a line of grey text that looked typed.
 *
 * Muted and italic, stated once for the whole app rather than per box. */
/* An explanation that is not in the way.
 *
 * Every one of these paragraphs answered a question somebody had once, and
 * then sat on the screen for ever — so the answer to "what is crew at fault
 * for" was still there on the nine hundredth ticket, beside a control reading
 * N/A. Morgan: the ticket looks very busy next to the Zendesk one it replaced.
 *
 * The mark is small, sits on the label's baseline, and does not shout. It is a
 * button so the keyboard reaches it and a screen reader announces it. */
.hintq {
  display: inline-flex; align-items: center; justify-content: center;
  width: 15px; height: 15px; margin-left: 5px; padding: 0; vertical-align: baseline;
  border: 1px solid var(--line); border-radius: 50%;
  background: none; color: var(--ph); cursor: pointer;
  font-size: .62rem; font-weight: 700; line-height: 1;
}
.hintq:hover { border-color: var(--accent); color: var(--accent); }
.hintq.on { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
.hintq:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Indented under the label it belongs to, so an open one reads as an aside
   rather than as another field. */
.hintbox {
  margin: 4px 0 8px; padding: 7px 10px;
  border-left: 2px solid var(--line); border-radius: 0 6px 6px 0;
  background: var(--panel2); max-width: 70ch;
  /* Most of these live INSIDE a <label>, which is uppercase, letter-spaced and
     bold on this screen — a sentence of prose wearing that is unreadable. So
     the box states its own type rather than inheriting the label's. */
  display: block; text-transform: none; letter-spacing: normal;
  font-weight: 400; font-size: .78rem; line-height: 1.5; color: var(--muted);
}
/* An author `display` beats the browser's own [hidden] rule, so saying
   display:block above silently un-hid every one of these. The DOM read as
   hidden and the screen showed them anyway — which is why this was found by
   looking at the render rather than by asking the element. */
.hintbox[hidden] { display: none; }
.chip[hidden] { display: none; }

/* -webkit-text-fill-color matters: the fields above set it to --ink (to beat
   autofill's colouring), and it paints the PLACEHOLDER too, over whatever
   colour is set here. That is why every placeholder in the app read as full
   white text however dim --ph was made. It has to be restated on the
   placeholder itself. */
input::placeholder, textarea::placeholder {
  color: var(--ph); -webkit-text-fill-color: var(--ph); opacity: 1; font-style: italic;
}
.field{margin-bottom:11px}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;background:var(--accent);color:var(--on-accent);border:0;border-radius:8px;padding:11px 16px;font-size:0.9375rem;font-weight:600;cursor:pointer}
.btn:disabled{opacity:.5;cursor:not-allowed}
.btn.sec{background:var(--sec-bg);color:var(--ink);border:1px solid var(--sec-line)}
.btn.danger{background:var(--danger-soft);color:var(--danger);border:1px solid var(--danger)}
.btn.wide{width:100%}
.btn.sm{padding:7px 11px;font-size:0.8438rem}
.muted{color:var(--muted)}
.small{font-size:0.875rem}
.tiny{font-size:0.8rem}
.pill{display:inline-block;padding:2px 9px;border-radius:999px;font-size:0.8rem;font-weight:650;background:var(--accent-soft);color:var(--accent)}
.pill.warn{background:var(--warn-soft);color:var(--warn)}
.pill.danger{background:var(--danger-soft);color:var(--danger)}
.pill.grey{background:var(--panel2);color:var(--muted)}
.spacer{height:10px}
.right{text-align:right}
.hr{height:1px;background:var(--line);margin:12px 0}

/* ---- lists */
.list{border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;background:var(--panel)}
.item{display:flex;align-items:center;gap:12px;padding:12px 14px;border-bottom:1px solid var(--line);cursor:pointer;background:none;border-left:0;border-right:0;border-top:0;width:100%;text-align:left}
.item:last-child{border-bottom:0}
.item:hover{background:var(--panel2)}
.item .grow{flex:1;min-width:0}
/* A row whose main body is tappable while a Directions link sits beside it */
.rowmain{flex:1 1 0;min-width:0;max-width:100%;display:block;text-align:left;background:none;border:0;padding:0;cursor:pointer;font:inherit;color:inherit}
.item .godir{flex:0 0 auto;text-decoration:none;padding:7px 14px}
/* Title over detail. As inline spans these ran together — "4102 Sunnyside RdEdina". */
.item .t{font-weight:600;font-size:0.9375rem;display:block;min-width:0}
.item .s{font-size:0.875rem;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;min-width:0}
/* Inside a row button these are inline, and an inline box cannot ellipsis — the
   text just ran off the card and under the Go button on a phone. */
/* A plan's detail line carries the next date, which must not be ellipsised away
   on a phone — let these wrap instead. */
.planrow .s{white-space:normal}
.rowmain .t,.rowmain .s{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis}
.rowmain .t{white-space:normal}
/* When and what the tech is doing should not be the part that gets truncated. */
.rowmain .when{color:var(--ink)}
.empty{padding:26px 14px;text-align:center;color:var(--muted);font-size:0.875rem}

/* ---- tables */
table{width:100%;border-collapse:collapse;font-size:0.9rem}
th{text-align:left;font-size:0.78rem;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);padding:7px 8px;border-bottom:1px solid var(--line);white-space:nowrap}
td{padding:9px 8px;border-bottom:1px solid var(--line);vertical-align:top}
tr:last-child td{border-bottom:0}
.scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}

/* ---- station chips */
.station{border:1px solid var(--line);border-radius:var(--radius);padding:11px 12px;margin-bottom:9px;background:var(--panel)}
.station.done{border-color:var(--line);background:var(--accent-soft)}
.station .hd{display:flex;align-items:center;gap:9px;margin-bottom:7px}
.station .no{font-weight:700;font-size:0.9375rem}
.chips{display:flex;gap:6px;flex-wrap:wrap}
/* A row of chips is a set of choices, not a toolbar: each one stays its own
   width instead of stretching to fill the row the way .row's children do. */
.chiprow>.chip{flex:0 0 auto}
.chip{border:1px solid var(--sec-line);background:var(--sec-bg);border-radius:999px;padding:7px 13px;font-size:0.8438rem;cursor:pointer;white-space:nowrap}
.chip.on{background:var(--accent);border-color:var(--accent);color:var(--on-accent);font-weight:600}
.chip.on.act{background:var(--warn);border-color:var(--warn)}
.chip.on.bad{background:var(--danger);border-color:var(--danger)}
/* The × that takes a follower off the request. It sits ON a filled chip, so it
   takes the chip's own text colour rather than --ink, which is measured against
   a panel and not against accent blue. */
.chipx{background:none;border:0;padding:0 0 0 2px;margin:0;font-size:1rem;line-height:1;
  color:inherit;opacity:.75;cursor:pointer}
.chipx:hover{opacity:1}

/* ---- the tickets you have open, across the top
   Sticks under the nav, which sticks under the header. Both offsets are
   measured at runtime rather than hard-coded — the header grows when the
   buttons wrap onto a second line on a phone, and a fixed number there is what
   previously left the nav floating 42px below it. */
.tickettabs{position:sticky;top:calc(var(--barh) + var(--tabsh));z-index:28;
  display:flex;gap:6px;align-items:center;padding:7px 14px;
  background:var(--panel);border-bottom:1px solid var(--line);
  overflow-x:auto;-webkit-overflow-scrolling:touch}
.tchip{flex:0 0 auto;max-width:240px;display:flex;align-items:center;gap:7px;
  background:var(--sec-bg);border:1px solid var(--sec-line);border-radius:7px;
  padding:5px 8px;font-size:0.8125rem;cursor:pointer}
.tchip:hover{border-color:var(--accent)}
/* The one you are reading, said with fill and weight rather than a hairline. */
.tchip.on{background:var(--accent);border-color:var(--accent);color:var(--on-accent);font-weight:600}
.tchip .t{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Not --muted: that is measured against a panel, and on the chip's own fill it
   lands at 4.35:1, under what small text needs. */
.tchip .n{flex:0 0 auto;color:var(--chip-dim);font-variant-numeric:tabular-nums}
.tchip.on .n{color:var(--on-accent);opacity:.85}
.tchip .x{flex:0 0 auto;background:none;border:0;padding:0 1px;margin:0;color:inherit;
  opacity:.7;font-size:0.95rem;line-height:1;cursor:pointer}
.tchip .x:hover{opacity:1}
.tclear{flex:0 0 auto;background:none;border:0;color:var(--muted);font-size:0.78rem;
  cursor:pointer;padding:5px 4px;white-space:nowrap}
.tclear:hover{color:var(--ink);text-decoration:underline}

/* A tel: link wearing a button. Anchors do not inherit .btn's line-height the
   way a <button> does, so the two would sit at different heights side by side. */
.callbtn{display:inline-flex;align-items:center;gap:6px;text-decoration:none;
  line-height:1.15;white-space:nowrap}
.callbtn:hover{border-color:var(--accent)}

/* A lead record, told at a glance. The same screen serves leads and customers,
   and the only sign of which you were on was a field halfway down it. The edge
   carries it the way .dashcard.bad does, so it reads from across the room
   without the whole card turning into a warning. */
.leadcard{border-left:3px solid var(--warn)}

/* ---- pin */
.center{min-height:72vh;display:flex;align-items:center;justify-content:center;padding:20px}
.pinbox{max-width:340px;width:100%}
.pindots{display:flex;gap:9px;justify-content:center;margin:16px 0}
.pindots i{width:13px;height:13px;border-radius:50%;background:var(--line);display:block}
.pindots i.on{background:var(--accent)}
.keys{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}
.keys button{padding:17px 0;font-size:1.3125rem;border:1px solid var(--sec-line);background:var(--sec-bg);border-radius:10px;cursor:pointer}

/* ---- misc */
.banner{padding:11px 13px;border-radius:8px;font-size:0.875rem;margin-bottom:12px}
.banner.ok{background:var(--accent-soft);color:var(--accent)}
.banner.err{background:var(--danger-soft);color:var(--danger)}
.banner.warn{background:var(--warn-soft);color:var(--warn)}
.sticky-save{position:fixed;left:0;right:0;bottom:0;background:var(--panel);border-top:1px solid var(--line);
  box-shadow:0 -6px 18px rgba(0,0,0,.28);
  /* Same again, and it matters more here: the home indicator was lying across
     the Save button. */
  padding:10px calc(14px + var(--saferight)) calc(10px + var(--safebot)) calc(14px + var(--safeleft));
  display:flex;gap:10px;z-index:25}
.sticky-save .btn{flex:1}
.photos{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
.photos img{width:74px;height:74px;object-fit:cover;border-radius:8px;border:1px solid var(--line)}
.appl{border:1px solid var(--line);border-radius:8px;padding:10px;margin-bottom:8px;background:var(--panel2);font-size:0.875rem}
.appl b{font-size:0.9062rem}
/* The correction form, folded away until somebody asks for it. A record that
   shows every field of every product as an editable box invites a tech to
   change one by leaning on the phone in their pocket. */
.applEdit{border-top:1px solid var(--line);padding-top:8px}
.applEdit label{font-size:0.7rem}
@media (min-width:700px){ main{padding:20px 18px 80px} }
@media (min-width:1500px){ main.wide{padding-left:28px;padding-right:28px} }

/* Logo on the white sign-in / setup card */
/* The sign-in mark, in the colourway that shows against the panel behind it —
   the black wordmark on a near-black card is the logo not being there at all. */
.logo-mark{display:block;width:auto;max-width:210px;height:auto;margin:2px auto 14px}
.logo-mark.light{display:block}
.logo-mark.dark{display:none}
@media (prefers-color-scheme: light){
  .logo-mark.light{display:none}
  .logo-mark.dark{display:block}
}

/* ---- dispatch board */
.dispatch{font-size:0.875rem}
.dispatch th{white-space:nowrap;position:sticky;top:0;background:var(--panel);z-index:2}
/* The list's total, pinned in the Price header so it is still there after scrolling a long list. */
.dispatch .thtotal{font-size:.95rem;font-weight:700;letter-spacing:0;text-transform:none;color:var(--ink);margin-top:2px}
.dispatch td{vertical-align:middle}
/* The call-ahead mark. Joel: "If the job is a call ahead show it with a phone
   icon on the dispatch board." --warn rather than --danger: it is a thing to
   do before leaving, not a thing that has gone wrong, and the row already
   spends --danger on urgent. Nothing else in the Client column is this
   colour, which is what makes a column of them countable. */
.cahead{color:var(--warn);font-size:1.02em;line-height:1;cursor:help}

/* ---- Attach ----
 * Joel: "replace the attachment icon with a paperclip or something that
 * doesn't take up as much room. This looks very crowded."
 *
 * A native file input draws its own button and a "No file chosen" label and
 * neither can be styled away, so the input is moved off-screen -- not hidden
 * -- and a label is what you see. Off-screen rather than display:none because
 * display:none takes it out of the tab order and out of the accessibility
 * tree, and the label is useless without it.
 */
/* ---- The estimates filter ----
 * Joel: "why are the filters so close together". Two different questions --
 * when, and what outcome -- were two rows of identical pills eight pixels
 * apart. Grouping is done by proximity before it is done by anything else, so
 * the gap BETWEEN the groups has to be visibly bigger than the gap inside
 * them, and each group needs a label or the pills are just a heap.
 */
.estfilter{display:flex;flex-direction:column;gap:18px;margin-top:4px}
.estfilter .fgroup > label{display:block;margin-bottom:8px}
.estfilter .chips{gap:8px}
/* The estimate search. Given its own width rather than the card's: a box the
   width of the screen invites a sentence, and this one wants a word. The
   clear cross sits inside it, which is why the box is a positioned wrapper
   and the input carries room on the right for it. */
.estq{position:relative;display:block;width:100%;max-width:420px}
.estq > input{width:100%;padding-right:36px}
.estqx{position:absolute;top:50%;right:6px;transform:translateY(-50%);
  width:26px;height:26px;display:flex;align-items:center;justify-content:center;
  padding:0;border:0;border-radius:6px;background:none;color:var(--muted);
  font-size:.82rem;line-height:1;cursor:pointer}
.estqx:hover{color:var(--ink);background:var(--panel2)}

/* The "what it covers" box on an estimate line. Smaller than the name above
   it, because it is the supporting text and the name is the thing. */
.lndetail{margin-top:5px;font-size:.84rem;line-height:1.45;min-height:58px}

.clip{position:relative;display:inline-flex;flex:0 0 auto}
.clip > input[type="file"]{position:absolute;width:1px;height:1px;padding:0;margin:0;
  opacity:0;pointer-events:none}
.clip > label{display:inline-flex;align-items:center;justify-content:center;
  width:36px;height:32px;border:1px solid var(--line);border-radius:8px;
  background:var(--panel2);color:var(--muted);cursor:pointer;
  transition:color .12s,border-color .12s}
.clip > label:hover{color:var(--ink);border-color:var(--sec-line)}
/* The focus ring has to be drawn on the label, because the thing that actually
   takes focus is a 1px square nobody can see. */
.clip > input[type="file"]:focus-visible + label{outline:2px solid var(--accent);outline-offset:1px}
/* Something is queued. The chips below say what, but on a short composer they
   are the first thing to scroll out of sight. */
.clip.has > label{color:var(--accent);border-color:var(--accent);background:var(--accent-soft)}
.dispatch tr.picked td{background:var(--accent-soft)}
.dispatch tr.totals td{background:var(--panel2);border-top:2px solid var(--line);border-bottom:0}
.dispatch .ordbox{width:40px;padding:5px 6px;text-align:center;font-size:0.8125rem}
.dispatch .godir{padding:5px 10px;font-size:0.84rem}
.linky{background:none;border:0;padding:0;font:inherit;color:var(--accent);font-weight:600;cursor:pointer;text-align:left}
.linky:hover{text-decoration:underline}
.dispatch-cards{display:none}
.dispatch-cards .item{gap:10px}
.dispatch-cards .item.picked{background:var(--accent-soft)}
/* A twelve-column table is unreadable on a phone, so the same rows become cards. */
@media (max-width:760px){
  .dispatch-table{display:none}
  .dispatch-cards{display:block}
}
.pinnum{width:26px;height:26px;border-radius:50%;color:#fff;font-size:0.7812rem;font-weight:700;
  display:flex;align-items:center;justify-content:center;border:2px solid var(--panel);box-shadow:0 1px 4px rgba(0,0,0,.35)}
.pinnum.sel{box-shadow:0 0 0 3px rgba(22,104,201,.45),0 1px 4px rgba(0,0,0,.35)}
.pindot{width:14px;height:14px;border-radius:50%;border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,.45);box-sizing:border-box}
.pindot.sel{width:20px;height:20px;box-shadow:0 0 0 3px rgba(22,104,201,.45),0 1px 4px rgba(0,0,0,.35)}
.flash{animation:flash 1.4s ease-out}
@keyframes flash{0%{background:var(--warn-soft)}100%{background:transparent}}
.dispatch tr.flash td{animation:flash 1.4s ease-out}
.dispatch tr.closed td{opacity:.62}
.dispatch tr.closed .linky{color:var(--muted)}
.dispatch tr.closed .linky[data-jobcust]{color:var(--accent)}
.item.closed .t{color:var(--muted);font-weight:600}
.item.closed{opacity:.75}
.pill.ok{background:var(--accent-soft);color:var(--accent)}

/* The role grid: a wide left column of what the capability is, then one narrow
   tick column per role. */
.rolegrid td,.rolegrid th{vertical-align:top;padding:9px 8px}
.rolegrid tr:not(:last-child) td{border-bottom:1px solid var(--line)}
.rolegrid input[type=checkbox]{transform:scale(1.35);margin-top:3px}

/* The date picker is the control on this screen that gets used most, and on a
   white card with a hairline border it disappears. Give it its own tinted block
   and an accent edge so the eye lands on it. */
.daypick{flex:0 0 auto;background:var(--accent-soft);border:1px solid var(--line);
  border-radius:10px;padding:9px 11px 10px}
.daypick label{color:var(--accent);margin-bottom:5px}
.daypick-row{display:flex;gap:8px;align-items:center}
.daypick input[type=date]{width:auto;min-width:165px;border:1.5px solid var(--accent);
  background:var(--panel);font-weight:600;padding:9px 11px}
.daypick input[type=date]:focus{outline:2px solid var(--accent);outline-offset:1px}
.daypick .btn.sec{border-color:var(--accent);color:var(--accent);font-weight:700;white-space:nowrap}
/* The arrow between the two ends of the range does the work a "to" label would,
   without a second line of text in a control that is already two boxes wide. */
.daypick .tothe{color:var(--accent);font-weight:700;flex:0 0 auto;font-size:0.95rem}
/* Presets sit under the boxes: the common spans are one click, the odd ones are
   still typed. Same chip styling as the week strip so the panel reads as one. */
.daypick .rangepresets{gap:5px;margin-top:7px}
.daypick .rangepresets .chip{background:var(--panel);border-color:var(--line);
  padding:4px 9px;font-size:0.72rem;font-weight:700}
.daypick .rangepresets .chip.on{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
@media (max-width:760px){
  .daypick{width:100%}
  .daypick input[type=date]{flex:1;min-width:0}
}


/* Happening right now. The only green pill on the dispatch board, which is
   what makes it worth anything - .pill.ok is the same blue as the default, so
   a job being worked and a job already finished were the same colour on the
   one screen whose job is telling them apart. */
.pill.live{background:var(--ok-soft);color:var(--ok);white-space:nowrap}

/* Somebody is on this job right now. The figure under the pill is minutes on
   the clock, retimed in place rather than by redrawing the board. */
.onclk{margin-top:2px;line-height:1.1}
.ocmins{font-variant-numeric:tabular-nums}
.onclkline{display:flex;align-items:center;gap:6px;margin-top:3px}
.onclkline .pill{flex:0 0 auto}

/* A column of costs somebody fills down in one sitting. Narrow, right-aligned
   and numeric so the decimal points line up down the shelf. */
.costbox{width:88px;text-align:right;padding:5px 7px;font-variant-numeric:tabular-nums;display:inline-block}

/* The four numbers somebody opens this report to see, before any table. */
.costtiles{display:flex;gap:10px;flex-wrap:wrap}
.ctile{flex:1 1 150px;background:var(--panel2);border:1px solid var(--line);border-radius:10px;
  padding:10px 12px;display:flex;flex-direction:column;gap:2px}
.ctile b{font-size:1.3rem;line-height:1.15;font-variant-numeric:tabular-nums}
.ctile .k{font-size:0.72rem;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);font-weight:700}
.ctile.good b{color:var(--ok)}
.ctile.bad{border-color:var(--danger)}
.ctile.bad b{color:var(--danger)}
/* Money that went the wrong way reads as the wrong way, in the table too. */
.neg{color:var(--danger)}

/* A retired address. Dimmed rather than hidden: it keeps every visit and
   pesticide record filed against it, and the only way back is to see it. */
.item.retired{opacity:.6}
.item.retired .t{font-weight:600}

/* "+ What it covers".
 *
 * Joel: "I don't see 'what it covers' when making the estimate, only after it
 * sent." It was there the whole time, and it was a 12px blue text link under
 * the description box with the Total line right beneath it - the person who
 * asked for the feature scanned past it on every estimate he wrote. A control
 * nobody finds is a control that is not there.
 *
 * Still not a textarea on every blank line: an empty box per row makes a
 * three-line quote look like a form to fill in. A dashed outline reads as
 * "something goes here" without filling the space with an input.
 */
.addcovers{display:block;width:100%;margin-top:5px;padding:6px 10px;text-align:left;
  background:none;border:1px dashed var(--accent);border-radius:7px;color:var(--accent);
  font:inherit;font-size:0.8125rem;font-weight:600;cursor:pointer}
.addcovers:hover{background:var(--accent-soft)}
.addcovers .hint{font-weight:400;color:var(--muted);font-size:0.75rem}

/* The wording on a signed line: read, not edited. Indented under the
   description the way the customer saw it on the estimate. */
.lnlocked{margin-top:4px;font-size:0.8125rem;color:var(--muted);line-height:1.45}
.lnlocked ul{margin:3px 0 0;padding-left:18px}
.lnlocked li{margin:1px 0}

/* Picking which lines they took, on the office acceptance card. */
.afpick{border:1px solid var(--line);border-radius:9px;padding:10px 12px;background:var(--panel2)}
/* Each row is a <label>, which picks up the sheet's form-label styling -
   uppercase, letter-spaced, muted and bold. That is right for "WHAT THEY
   TOOK" above the box and wrong for the service names inside it, which are
   the customer's own words. Put back. */
.afline{display:flex;align-items:center;gap:10px;padding:5px 0;margin:0;
  font-size:0.9rem;text-transform:none;letter-spacing:0;font-weight:400;color:var(--ink)}
.afline .grow{flex:1 1 auto;min-width:0}
/* Only the tabular figures are doing visible work here - they line the
   decimal points up down the column. The flex and nowrap that were beside
   them changed nothing at any width worth testing, because .grow absorbs the
   squeeze, so they are gone rather than left reading as the thing holding
   the price off the name. */
.afline .muted{font-variant-numeric:tabular-nums}
/* And inputs are width:100% across this sheet, which on a flex row makes the
   tick box eat the line and shove the name into the price. */
.afline input[type=checkbox]{width:auto;flex:0 0 auto;margin:0}
/* A line they turned down. Still on the estimate, still legible, plainly not
   part of the job - the record has to show it was quoted. */
.estlines tr.notaken td{opacity:.55}
.estlines tr.notaken input{text-decoration:line-through}

/* A count on a chip that is otherwise a silent mode toggle. */
.chipn{display:inline-block;min-width:18px;padding:0 5px;margin-left:5px;border-radius:999px;
  background:var(--warn-soft);color:var(--warn);font-size:0.75rem;font-weight:700;
  font-variant-numeric:tabular-nums;text-align:center}
.chip.on .chipn{background:rgba(255,255,255,.22);color:inherit}
/* What an empty date means, under the empty date. */
.datehint{margin-top:4px;line-height:1.25}

.nowrap{white-space:nowrap}

/* Estimate line editor: inputs sit flush in the cells so the row reads as a row. */
.estlines td{padding:5px 6px}
.estlines input,.estlines select{padding:8px 9px}
.estlines input.right{text-align:right}

/* The week strip is the other control this screen lives on, so it gets the same
   treatment as the date picker rather than floating loose on the white card. */
.weekpick{flex:1 1 auto;min-width:0;background:var(--accent-soft);border:1px solid var(--line);
  border-radius:10px;padding:9px 11px 10px}
.weekpick label{color:var(--accent);margin-bottom:6px}
.weekpick .chips{gap:5px}
.weekpick .chip.day{background:var(--panel);border-color:var(--line);min-width:46px;padding:5px 9px;
  text-align:center;line-height:1.15;font-weight:600}
.weekpick .chip.day .dow{display:block;font-size:0.6rem;letter-spacing:.4px;color:var(--muted);font-weight:700}
.weekpick .chip.day.on{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
.weekpick .chip.day.on .dow{color:rgba(255,255,255,.72)}
/* Today is worth finding even when you are looking at another day. */
.weekpick .chip.day.istoday:not(.on){border-color:var(--accent);border-width:2px;color:var(--accent)}
.weekpick .chip.day.istoday:not(.on) .dow{color:var(--accent)}
.weekpick .chip.nav{background:var(--panel);border-color:var(--line);color:var(--accent);font-weight:700;padding:7px 11px}
/* A range can run past the seven days the strip shows. Without this the arrow
   looks inert and the range looks as though it stops on Saturday. */
.weekpick .chip.nav.contd{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}

.hint{color:var(--muted);font-size:0.8rem;margin:4px 0 0;line-height:1.4}

/* The book's filter bar and tab strip — one row on a desktop, stacked on a phone. */
.filterbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.filterbar input{flex:1 1 260px;min-width:0;margin:0}
.subtabs{display:flex;gap:6px;margin:12px 0 10px;flex-wrap:wrap}
.subtab{background:var(--panel);border:1px solid var(--line);border-bottom-width:2px;border-radius:9px 9px 0 0;
  padding:9px 15px;font:inherit;font-size:0.9rem;font-weight:600;color:var(--muted);cursor:pointer;
  display:flex;align-items:center;gap:7px}
.subtab.on{background:var(--accent-soft);border-color:var(--line);border-bottom-color:var(--accent);color:var(--accent)}
.subtab .pill{font-size:0.7rem;padding:1px 7px}

/* Tags and the selection bar. */
/* Checkboxes, and the room to hit one.
 *
 * These were 17px. WCAG 2.2 asks 24px of a target and a phone in a van wants
 * more than that — Joel asked for bigger twice, once for the box and once
 * because missing it opened the ticket. */
.tickcol{width:44px;text-align:center;padding-left:10px;padding-right:0}
.tickcol input{width:22px;height:22px;margin:0;cursor:pointer;accent-color:var(--accent)}

/* The tick column on the ticket list, where the whole ROW opens the ticket.
 *
 * So this cell is a hole in that, and the hole has to be the size of the cell
 * rather than the size of the glyph: the row handler treats a click anywhere
 * in here as ticking the box and never as opening the ticket. Selecting six
 * tickets was six chances to lose the list. */
.tkcell{width:44px;text-align:center;padding-left:8px;padding-right:0;cursor:default}
.tkcell input{width:22px;height:22px;margin:0;cursor:pointer;accent-color:var(--accent)}
/* Says it is not part of the row you would click through. */
.ticketlist tr:hover .tkcell{background:var(--panel2)}
.tagrow{display:flex;flex-wrap:wrap;gap:4px;margin-top:5px}
.tagchip{display:inline-flex;align-items:center;gap:5px;background:var(--tc,var(--muted));color:var(--on-accent);
  border-radius:999px;padding:2px 9px;font-size:0.68rem;font-weight:700;letter-spacing:.2px;white-space:nowrap}
.tagchip .x{background:none;border:0;color:rgba(255,255,255,.75);font-size:0.9rem;line-height:1;
  padding:0 0 0 1px;cursor:pointer}
.tagchip .x:hover{color:#fff}
.tagchip .n{background:rgba(255,255,255,.28);border-radius:999px;padding:0 5px;font-size:0.62rem}
/* Was a hard-coded 104px, which was the header and the nav added up by hand -
   so it was already wrong whenever the header wrapped, and the status-bar inset
   would have made it wrong everywhere. Both numbers are measured at runtime. */
.selbar{position:sticky;top:calc(var(--barh) + var(--tabsh));z-index:28;display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  background:var(--accent);color:var(--on-accent);border-radius:10px;padding:9px 12px;margin:0 0 10px;
  box-shadow:0 2px 10px rgba(0,0,0,.18)}
.selbar b{font-size:0.9rem}
/* The bar sets white text for its own labels; the input sits on white, so it
   has to say otherwise or you are typing in invisible ink. */
.selbar input{flex:0 1 190px;margin:0;padding:7px 10px;font-size:0.85rem;border-radius:8px;border:0;
  background:var(--field);color:var(--ink);-webkit-text-fill-color:var(--ink)}
.selbar input::placeholder{color:var(--ph);-webkit-text-fill-color:var(--ph);opacity:1}
.selbar input:focus{outline:2px solid #fff;outline-offset:1px}
.selbar .btn.sec{background:rgba(255,255,255,.15);border-color:rgba(255,255,255,.35);color:#fff}
.selbar .btn:not(.sec){background:var(--panel);color:var(--accent);border-color:var(--panel);font-weight:700}
/* The bar repaints every non-secondary button in its own accent, which turned
   the one destructive control on it into blue on blue. A delete has to look
   like a delete wherever it is standing. */
.selbar .btn.danger{background:var(--panel);color:var(--danger);border-color:var(--danger)}

/* Dispatch week calendar. Eight columns that scroll sideways rather than
   squeezing: a 90px-wide day column is unreadable, and a phone should swipe. */
.calhint{margin:0 0 8px}
.calgrid{display:grid;grid-template-columns:minmax(200px,1fr) repeat(7,minmax(190px,1fr));gap:8px;
  overflow-x:auto;-webkit-overflow-scrolling:touch;padding-bottom:6px;align-items:start}
.calcol{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);
  min-height:180px;display:flex;flex-direction:column}
.calcol.backlogcol{background:var(--panel2)}
.calcol.istoday{border-color:var(--accent);border-width:2px}
.calcol.dropping{background:var(--accent-soft);border-color:var(--accent);border-style:dashed;border-width:2px}
.calhead{display:flex;align-items:center;gap:7px;padding:9px 10px;border-bottom:1px solid var(--line);
  font-size:0.84rem;position:sticky;top:0;background:inherit;border-radius:var(--radius) var(--radius) 0 0}
.calhead .n{background:var(--panel2);border:1px solid var(--line);border-radius:999px;
  padding:0 7px;font-size:0.7rem;font-weight:700;color:var(--muted)}
.calcol.istoday .calhead b{color:var(--accent)}
.calbody{padding:8px;display:flex;flex-direction:column;gap:7px;flex:1}
.calempty{color:var(--muted);font-size:0.78rem;text-align:center;padding:12px 0}
.jobcard{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--muted);
  border-radius:8px;padding:8px 9px;cursor:grab;touch-action:none;user-select:none;
  -webkit-user-drag:none;-webkit-user-select:none}
.jobcard:hover{border-color:var(--accent)}
.jobcard .t{font-weight:700;font-size:0.82rem;line-height:1.25}
.jobcard .s{font-size:0.74rem;color:var(--muted);line-height:1.3}
.jobcard .meta{display:flex;flex-wrap:wrap;gap:4px;align-items:center;margin-top:5px}
.jobcard .meta .pill{font-size:0.64rem;padding:1px 6px}
.jobcard.p-high{border-left-color:var(--danger)}
.jobcard.p-low{border-left-color:var(--line)}
.jobcard.closed{opacity:.55;cursor:default}
.jobcard.dragging{opacity:.35}
.jobcard.ghost{position:fixed;z-index:999;pointer-events:none;box-shadow:0 8px 24px rgba(0,0,0,.35);
  transform:rotate(1.5deg);opacity:.95;cursor:grabbing}

/* Reordering. A grip rather than the whole row, so dragging can never be
   confused with editing the stop number sitting next to it. */
.ordcell{display:flex;align-items:center;gap:5px}
.grip{cursor:grab;color:var(--muted);font-size:0.9rem;line-height:1;touch-action:none;user-select:none;
  padding:2px 1px;letter-spacing:-2px}
.grip:hover{color:var(--accent)}
tr.dragging{opacity:.4}
tr.insert-before > td{box-shadow:inset 0 2px 0 var(--accent)}
tr.insert-before[data-after="1"] > td{box-shadow:inset 0 -2px 0 var(--accent)}
.jobcard .stop{display:inline-block;min-width:16px;margin-right:5px;color:var(--muted);font-weight:700;
  font-size:0.7rem}
.jobcard.insert-before{box-shadow:0 -3px 0 var(--accent)}
.calcol.dropping.append .calbody{box-shadow:inset 0 -3px 0 var(--accent)}

/* Saved views: one row of chips, the way the filters above them read. */
.viewbar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:0 0 10px}
.chip.view .star{color:var(--warn);font-size:0.7rem}
.chip.view.on .star{color:#fff}
.consentline{display:flex;align-items:center;gap:8px;margin:10px 0 0;text-transform:none;
  letter-spacing:0;font-weight:400;color:var(--ink);font-size:0.85rem}
.consentline input{width:18px;height:18px;flex:0 0 auto;accent-color:var(--accent)}

/* Job notes with an audience, and photos. */
.notebox{border-radius:8px;padding:9px 11px;font-size:0.85rem;line-height:1.45;
  border-left:3px solid var(--line);background:var(--panel2)}
/* The block takes the width of the card; the WORDS do not. A request screen is
   full width so the views rail can sit beside it, and a paragraph run out to
   1500px is one your eye loses its place in coming back to the next line.
   Around 90 characters is where prose stays comfortable. */
.notebox > div:last-child{max-width:90ch}
.notebox .lbl{display:block;font-size:0.64rem;text-transform:uppercase;letter-spacing:.6px;
  font-weight:700;color:var(--muted);margin-bottom:3px}
.notebox.sched{border-left-color:var(--accent)}
.notebox.sched .lbl{color:var(--accent)}
.notebox.internal{border-left-color:var(--warn);background:var(--note-bg)}
/* The label was --warn, which was legible on the old near-black note and is not
   on this one. It goes light instead — the amber block is already the signal. */
.notebox.internal .lbl{color:#fff3d0}
/* Whose words these are, in a thread that gets scrolled fast.
 *
 * Three kinds of block, three looks: what the customer sent us is the plain
 * one, what we sent them is tinted and carries the accent edge, and an internal
 * note stays amber. Never colour alone — each one also says so in words above
 * it, because a thread read by somebody colour-blind has to work too, and
 * because "is this what I wrote or what they wrote" is not a question anybody
 * should have to answer from a hue.
 */
.notebox.said-in{border-left-color:var(--muted)}
.notebox.said-out{border-left-color:var(--accent);background:var(--mine-bg)}
.notebox .who{font-weight:700}
.notebox.said-out .who{color:var(--accent)}
/* The "who wrote it, when" line is .muted, which is measured against the panel
   and not against a tinted block. On these two it lands at 4.03:1 and 2.32:1 —
   under the 4.5:1 that small text needs, and the amber one was already wrong
   before this change. Each tinted box states its own. */
.notebox.said-out .tiny.muted{color:var(--meta-mine)}
.notebox.internal .tiny.muted{color:var(--meta-note)}
.photogrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px;margin-top:10px}
.shot{margin:0;position:relative;background:var(--panel2);border:1px solid var(--line);border-radius:8px;
  overflow:hidden}
.shot img{display:block;width:100%;height:120px;object-fit:cover;background:var(--panel2)}
.shot figcaption{padding:6px 7px}
.shot figcaption input{padding:5px 7px;font-size:0.76rem;margin:0}
.shot .x{position:absolute;top:5px;right:5px;background:rgba(0,0,0,.6);color:#fff;border:0;
  border-radius:6px;padding:2px 7px;font-size:0.72rem;cursor:pointer}
.shot .x:hover{background:var(--danger)}
.shot .shotsend{display:flex;align-items:center;gap:5px;margin-top:4px;cursor:pointer}
.shot .shotsend input{width:auto;margin:0}

/* Backup: a key you copy once, and a command you paste. */
.codeblock{background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:10px 12px;
  font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.78rem;overflow-x:auto;margin:8px 0}
.keyline{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.8rem;
  word-break:break-all;background:var(--panel);border:1px solid var(--line);border-radius:6px;
  padding:8px 10px;margin-top:7px;user-select:all}

/* ---- Tickets: the saved views down the side, as ESS Desk had them ---- */
.tickets {
  display: flex; gap: 14px; align-items: flex-start;
  /* Set from the saved width on render, and live while dragging. */
  --railw: 246px;
}
.tickets .ticketmain { flex: 1 1 auto; min-width: 0; }

/* Open, or out of the way.
 *
 * Nine useful views and a list of names, occupying the width a long subject
 * line would rather have. Pinned is what it has always done: in the layout,
 * taking its width. Closed leaves a strip with a button, and the views slide
 * OVER the list rather than pushing it — so looking at the queues costs none
 * of the width you just took back, which is the whole reason to close it.
 *
 * The button is always in the markup and hidden while pinned, so the two
 * states are one stylesheet rule apart and nothing has to be re-rendered to
 * move between them. */
.railopen {
  display: none; flex: 0 0 auto; align-self: flex-start;
  position: sticky; top: 12px; z-index: 5;
  width: 34px; height: 34px; padding: 0;
  border: 1px solid var(--line); border-radius: 9px;
  background: var(--panel); color: var(--muted); cursor: pointer; font-size: 1rem;
}
.railopen:hover { border-color: var(--accent); color: var(--accent); }
.tickets.railclosed .railopen { display: inline-flex; align-items: center; justify-content: center; }
/* Out of the layout entirely, not merely narrow: a collapsed rail that still
   reserved its column would have given back nothing. */
.tickets.railclosed .viewrail,
.tickets.railclosed .railgrip { display: none; }
/* Peeking: back on screen, but floating over the list. */
.tickets.railclosed.railpeek { position: relative; }
.tickets.railclosed.railpeek .viewrail {
  display: block; position: absolute; left: 42px; top: 0; z-index: 30;
  width: var(--railw); box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
}

/* The pin lives in the rail's own header, because that is where you are
   looking when you decide you have finished with it. */
.railhead { display: flex; justify-content: flex-end; padding: 0 2px 4px; }
/* .viewrail button is a full-width row, and this is not one. Named through the
   rail so it actually wins rather than losing on specificity. */
.viewrail .railpin {
  display: inline-flex; align-items: center; justify-content: center;
  width: 24px; height: 24px; flex: 0 0 auto; padding: 0; line-height: 1;
  border: 1px solid transparent; border-radius: 6px;
  background: none; color: var(--muted); cursor: pointer; font-size: .7rem;
}
.viewrail .railpin:hover { border-color: var(--line); color: var(--ink); background: none; }
.viewrail .railpin.on { color: var(--accent); background: none; font-weight: 400; }
.railpin:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The subject is what people read down, so it takes the slack rather than
   letting five columns sit bunched on the left of a wide screen. */
.ticketlist { width: 100%; table-layout: auto; }
.ticketlist th:nth-child(1), .ticketlist td:nth-child(1) { width: 104px; }
.ticketlist th:nth-child(2) { width: auto; }
.ticketlist th:nth-child(3), .ticketlist td:nth-child(3) { width: 24%; }
.ticketlist th:nth-child(4), .ticketlist td:nth-child(4) { width: 14%; }
.ticketlist th:nth-child(5), .ticketlist td:nth-child(5) { width: 130px; }
.viewrail {
  flex: 0 0 var(--railw); background: var(--panel); border: 1px solid var(--line);
  border-radius: 12px; padding: 8px; align-self: flex-start;
  position: sticky; top: 12px; max-height: calc(100vh - 24px); overflow-y: auto;

  /* A rail taller than the laptop it is on, with nothing to say so.
   *
   * macOS hides scrollbars until you scroll, so a list that runs past the
   * bottom of a 13-inch screen simply appears to end — and the views below the
   * fold, "New tickets" among them, might as well not exist. Joel reported it
   * as the New view not showing up on a MacBook, which is exactly what it
   * looks like from the chair.
   *
   * Two cues, because either one alone can be missed. A scrollbar that is
   * always drawn rather than summoned by movement, and a shade along the
   * bottom edge that is there only while there is more underneath:
   * background-attachment local scrolls the gradient away with the content, so
   * it fades out as you reach the end and needs no JavaScript to know when. */
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
  background-image: linear-gradient(to top, var(--panel), transparent 26px),
                    linear-gradient(to top, rgba(0, 0, 0, .28), transparent 22px);
  background-repeat: no-repeat;
  background-position: bottom, bottom;
  background-size: 100% 26px, 100% 22px;
  background-attachment: local, scroll;
}
/* Firefox has scrollbar-width; WebKit and Chrome need telling, and on macOS
   this is what overrides the overlay behaviour and keeps the bar drawn. */
.viewrail::-webkit-scrollbar { width: 9px; }
.viewrail::-webkit-scrollbar-track { background: transparent; }
.viewrail::-webkit-scrollbar-thumb {
  background: var(--line); border-radius: 5px;
  border: 2px solid var(--panel);
}
.viewrail::-webkit-scrollbar-thumb:hover { background: var(--muted); }
/* The drag handle sits in the gap between the rail and the list rather than on
   either, so grabbing it never lands on a view button by mistake. */
.railgrip {
  flex: 0 0 7px; align-self: stretch; position: sticky; top: 12px;
  height: calc(100vh - 24px); margin: 0 -7px; cursor: col-resize;
  border-radius: 4px; background: transparent; transition: background .12s;
  touch-action: none;
}
.railgrip:hover, .railgrip:focus-visible, .railgrip.dragging { background: var(--accent); opacity: .55; }
.railgrip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
body.railresizing { cursor: col-resize; user-select: none; }
.viewrail .vh {
  font-size: .68rem; letter-spacing: .08em; text-transform: uppercase;
  color: var(--muted); padding: 12px 8px 5px;
}
.viewrail .vh:first-child { padding-top: 4px; }
.viewrail button {
  display: flex; width: 100%; align-items: center; gap: 8px; text-align: left;
  background: none; border: 0; color: var(--ink); cursor: pointer;
  padding: 6px 8px; border-radius: 8px; font: inherit; font-size: .875rem; line-height: 1.3;
}
.viewrail button span:first-child {
  flex: 1 1 auto; min-width: 0;
  /* Cut a long name off rather than wrapping it onto a second line: a rail of
     two-line rows is hard to scan, and the width is draggable when it matters. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.viewrail button:hover { background: var(--panel2); }
.viewrail button.on { background: var(--accent); color: #fff; font-weight: 600; }
.viewrail .vcount {
  flex: 0 0 auto; font-size: .75rem; color: var(--muted);
  background: var(--panel2); border-radius: 999px; padding: 1px 7px; min-width: 1.6em; text-align: center;
}
.viewrail button.on .vcount { background: rgba(255,255,255,.22); color: #fff; }

/* Tracing a shape is aiming, not grabbing. Leaflet's open hand hides the exact
   pixel you are about to place a point on, which is the whole job here — a
   crosshair has a centre you can see. The grabbing hand comes back while you are
   actually dragging, so panning still feels like panning. */
#ms_map.leaflet-grab,
#ms_map .leaflet-grab,
#ms_map .leaflet-interactive { cursor: crosshair; }
#ms_map.leaflet-dragging.leaflet-grab,
#ms_map.leaflet-dragging .leaflet-grab { cursor: grabbing; }

/* Offline bar. Pinned to the bottom, clear of the sticky header and the tab
   strip, and unmissable: which mode a tech is in changes what they should
   expect, so it is not a subtle grey hint. */
.offbar{position:fixed;left:0;right:0;bottom:0;z-index:40;background:var(--warn);color:#1b1405;
  font-weight:650;font-size:0.875rem;text-align:center;
  /* Clear of the home indicator, which otherwise crosses the one line on
     screen telling a tech their work has not sent yet. */
  padding:9px calc(12px + var(--saferight)) calc(9px + var(--safebot)) calc(12px + var(--safeleft));
  box-shadow:0 -4px 14px rgba(0,0,0,.3)}

/* A photo still on the phone. Dimmed and edged in amber so it reads as "taken,
   not sent" at a glance rather than looking like any other picture in the grid. */
.shot.waiting{border-color:var(--warn);opacity:.82}
.shot.waiting img{filter:saturate(.7)}
.shot.waiting figcaption b{color:var(--warn)}

.viewhead {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 11px 13px; border-bottom: 1px solid var(--line);
}
.viewhead b { font-size: 1.02rem; }

/* The builder's way in, pinned under the last view rather than floating in the
   rail, so the rail still reads top to bottom as a list of questions. */
.railfoot { padding: 9px 7px 3px; border-top: 1px solid var(--line); margin-top: 7px; }

/* The status pill carries the colour set in Setup, so it reads the same here as
   it does in the picker on the ticket itself. */
.spill {
  display: inline-block; color: #fff; font-size: .72rem; font-weight: 700;
  padding: 2px 8px; border-radius: 5px; white-space: nowrap;
}
.ticketlist th { user-select: none; }
.ticketlist td { vertical-align: top; }

@media (max-width: 860px) {
  /* On a phone the rail becomes a scrolling strip above the list: a 218px
     column beside a table is most of a phone screen spent on navigation. */
  /* flex-start is right beside the rail - it stops a short list stretching to
     the rail's full height. Stacked, the same word means something else: the
     cross axis is now the WIDTH, so the list sized itself to its widest row and
     ran off the side of the phone, taking the layout viewport out to 506px on a
     390px screen. Stacked things are full width. */
  .tickets { flex-direction: column; align-items: stretch; }
  .viewrail {
    flex: 1 1 auto; width: 100%; position: static; max-height: none;
    display: flex; gap: 6px; overflow-x: auto; padding: 8px;
  }
  .viewrail .vh { display: none; }
  .viewrail button { width: auto; white-space: nowrap; }
  .railgrip { display: none; }
}

/* ---- Dashboard: six answers, no charts ---- */
.dashgrid {
  display: grid; gap: 14px;
  /* min() or the floor wins on a narrow phone: a bare minmax(330px,...) is a
     hard 330px column, so on a 320px screen the grid - and with it the whole
     page - came out wider than the device and the dashboard scrolled sideways. */
  grid-template-columns: repeat(auto-fit, minmax(min(330px, 100%), 1fr));
  /* Stretch, so the bottom edge of a band comes out level.
     This only works because the two kinds of card were separated first. Mixed
     together, stretching gave a card holding one number four hundred pixels of
     nothing. Within a band everything is alike, so the slack is small and it
     sits inside a bordered card, which reads as room rather than a hole. */
  align-items: stretch;
}
/* Three across on a wide screen, because there are six cards: four and a gap
   with two stragglers underneath was most of why this looked untidy. Narrower
   than that and auto-fit takes over with two, then one. */
@media (min-width: 1080px) { .dashgrid { grid-template-columns: repeat(3, 1fr); } }
/* The second band sits under the first and holds a number each. */
.dashstrip { margin-top: 14px; }
.dashcard { margin: 0; }
.dashcard h3 { margin: 0 0 8px; font-size: .95rem; }
/* The left edge carries the state, so a card that needs attention reads from
   across the room without the whole thing turning into a warning. */
.dashcard.bad { border-left: 3px solid var(--warn); }
.dashbig { font-size: 1.9rem; font-weight: 700; line-height: 1.1; letter-spacing: -.01em; }
.dashbig.warnnum { color: var(--warn); }
.dashbig.oknum { color: var(--ok); }
.dashsub { font-size: .8rem; color: var(--muted); margin-top: 3px; }
.dashnil { font-size: .85rem; color: var(--muted); padding: 6px 0 2px; }
.dashlist {
  margin-top: 10px; display: flex; flex-direction: column; gap: 2px;
}
/* A pill is a fixed thing. Letting it wrap turned "4d late" into two lines and
   shoved the row out of line with every other row. */
.dashrow .pill, .dashrow .nowrap { flex: 0 0 auto; white-space: nowrap; }
.dashrow {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  background: none; border: 0; color: var(--ink); font: inherit;
  padding: 6px 7px; border-radius: 7px; cursor: pointer;
}
.dashrow.static { cursor: default; }
.dashrow:not(.static):hover { background: var(--panel2); }
/* flex:1 is the whole reason the pills line up. Without it .grow is only as
   wide as its text, so every pill sat wherever the customer's name happened to
   end and the right-hand edge came out ragged. */
.dashrow .grow { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.dashrow b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; font-size: .88rem; }
.dashrow .tiny { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.warntext { color: var(--warn); }

/* ---------------------------------------------------------------- the bell */
/* Its own stacking context so the panel hangs off it rather than off the bar,
   which is sticky and would clip it. */
.bellwrap{position:relative;flex:0 0 auto;display:flex;align-items:center}
.topbar .bell{position:relative;font-size:1rem;line-height:1;padding:5px 9px}
/* Unread is carried by the count, not by the icon: a bell that changes colour
   and a number that says six are two ways of saying the same thing, and on a
   phone in the sun the number is the one that reads.

   Its own red rather than --danger, which is #f85149 on a dark screen and
   carries white text at 3.35:1 — under the 4.5:1 small bold type needs. This
   measures 5.36:1 for the number and 3.23:1 against the header behind it, and
   the same value works in both schemes, so the badge does not change weight
   when the machine flips to light. */
.topbar .bell .bellcount{margin-left:5px;background:#cf222e;color:#fff;font-weight:700;
  font-size:0.6875rem;border-radius:9px;padding:1px 5px;vertical-align:1px}
.topbar .bell.lit{border-color:#cf222e}
.bellpanel{position:absolute;top:calc(100% + 6px);right:0;z-index:40;width:min(360px, calc(100vw - 28px));
  max-height:min(70vh, 480px);overflow-y:auto;background:var(--panel2);border:1px solid var(--line);
  border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.35)}
.bellhead{display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:10px 12px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--panel2);
  z-index:1}
.bellempty{padding:16px 14px;color:var(--muted);font-size:0.8125rem;line-height:1.5}
/* Every rule here has to outrank `.topbar button`, which this panel lives
   inside: that rule sets white-space:nowrap, and an alert is a sentence, so
   without the extra specificity the subject ran off the right edge on a phone
   — the half of the line that says which request it is about. */
.bellpanel .bellitem{display:block;width:100%;text-align:left;background:none;border:0;
  border-bottom:1px solid var(--line);border-radius:0;padding:10px 12px;cursor:pointer;
  color:var(--ink);white-space:normal;overflow-wrap:anywhere;font-size:1rem}
.bellpanel .bellitem:hover{background:var(--accent-soft);border-color:var(--line)}
.bellpanel .bellitem:last-child{border-bottom:0}
/* Unread gets an edge rather than a fill: a panel of ten filled rows is a wall,
   and the point is to see at a glance which of them are new. */
.bellpanel .bellitem.new{border-left:3px solid var(--accent);padding-left:9px}
.bellpanel .belltitle{font-size:0.8125rem;font-weight:600;margin-bottom:2px;line-height:1.35}
.bellpanel .bellbody{font-size:0.78rem;color:var(--muted);margin-bottom:3px;line-height:1.35;
  overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}

/* Files on a request.
   The table and the mail poller have both existed since the merge; nothing
   could read the bytes back, so a customer's photo of a chewed soffit sat in
   the database where nobody could see it. A chip rather than a row: most
   requests have none, a few have eight, and eight table rows for eight
   filenames is a card that pushes the thread off the screen. */
.filerow { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.filechipwrap { display: inline-flex; align-items: center; gap: 2px; }
.filechip {
  display: inline-flex; align-items: baseline; gap: 7px; max-width: 320px;
  padding: 4px 10px; border: 1px solid var(--line); border-radius: 6px;
  background: var(--panel2); color: var(--ink); text-decoration: none;
  font-size: .78rem; line-height: 1.3;
}
.filechip:hover { border-color: var(--accent); }
/* The name is the part worth reading, so it is the part that truncates — and
   the title attribute carries the whole thing for anything that does not fit. */
.filechip .fname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filechip .fsize { flex: 0 0 auto; color: var(--muted); font-size: .7rem; }

/* A photo, shown as a photo.
 *
 * Everything filed on a request was drawn as a filename and a byte count,
 * which for a scanned form is right and for a picture of a wasp nest under
 * somebody's deck is useless: the whole reason the customer sent it is that
 * you look at it.
 *
 * Sized in the stylesheet rather than left to the image, because a phone photo
 * is four thousand pixels wide and would otherwise fill the thread on its own.
 * The name stays underneath — a picture you cannot name is a picture you
 * cannot talk about on the phone to the customer who sent it. */
.filephoto {
  display: inline-flex; flex-direction: column; gap: 4px; max-width: 180px;
  padding: 4px; border: 1px solid var(--line); border-radius: 8px;
  background: var(--panel2); color: var(--ink); text-decoration: none;
  font-size: .72rem; line-height: 1.3;
}
.filephoto img {
  display: block; width: 100%; height: 120px; object-fit: cover;
  border-radius: 5px; background: var(--panel);
}
.filephoto .fname {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--muted); padding: 0 2px 2px;
}
.filephoto:hover { border-color: var(--accent); }
.filephoto:hover .fname { color: var(--ink); }

/* The hover preview on the ticket list.
   Fixed rather than absolute so it is positioned against the viewport, which
   is what the flip logic measures against - inside the scrolling table it
   would be placed against a box that has already scrolled. pointer-events off
   because a panel that sits under the cursor steals the mouseleave that is
   supposed to dismiss it, and then it never goes away. */
.tpreview {
  position: fixed; z-index: 90; max-width: 380px; width: max-content;
  background: var(--panel); border: 1px solid var(--line); border-radius: 9px;
  box-shadow: 0 10px 34px rgba(0,0,0,.34);
  padding: 11px 13px; pointer-events: none;
  opacity: 0; transition: opacity .12s ease;
}
.tpreview.on { opacity: 1; }
.tpreview .tprow { display: flex; align-items: center; gap: 7px; margin-bottom: 6px; }
.tpreview .tpsubject { font-weight: 650; line-height: 1.3; margin-bottom: 3px; }
.tpreview .tplast { margin-top: 9px; padding-top: 8px; border-top: 1px solid var(--line); }
/* Four lines is the most anybody reads standing up. The rest is why you open it. */
.tpreview .tptext {
  margin-top: 3px; font-size: .82rem; line-height: 1.42; color: var(--ink);
  display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
}

/* The "+ New" menu in the header.
   One button that raises any of the four things, because all four start the
   same way - somebody is on the phone and needs a record now - and a thing you
   have to navigate to first is a thing that ends up on paper. */
.newwrap { position: relative; display: inline-flex; }
.newmenu {
  position: absolute; top: calc(100% + 6px); right: 0; z-index: 80;
  display: none; flex-direction: column; min-width: 158px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 9px;
  box-shadow: 0 10px 30px rgba(0,0,0,.32); padding: 5px; gap: 2px;
}
.newmenu.on { display: flex; }
/* Scoped hard: .topbar button has its own nowrap/padding rules that would
   otherwise centre these and squash them onto one line. */
.topbar .newmenu button {
  display: block; width: 100%; text-align: left; white-space: nowrap;
  padding: 7px 11px; border: 0; border-radius: 6px;
  background: none; color: var(--ink); font-size: .86rem; cursor: pointer;
}
.topbar .newmenu button:hover { background: var(--panel2); }

/* The reply editor.
   Six buttons, deliberately: this is for saying "we will do three things" as
   three lines, not for building a newsletter. The server allows exactly this
   set and strips the rest, so a paste from Word arrives as words. */
.rtbar { display: flex; flex-wrap: wrap; gap: 4px; margin: 8px 0 0; }
.rtbar button {
  padding: 4px 9px; border: 1px solid var(--line); border-radius: 6px;
  background: var(--panel2); color: var(--ink); font-size: .78rem; cursor: pointer;
  line-height: 1.3;
}
.rtbar button:hover { border-color: var(--accent); }
.rtbar button.on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }
/* A contenteditable with no min-height collapses to a line and is hard to aim
   at; one with no placeholder looks broken before you start typing. */
.rtbox {
  min-height: 110px; max-height: 420px; overflow: auto;
  margin-top: 6px; padding: 9px 11px;
  border: 1px solid var(--line); border-radius: 8px;
  background: var(--panel2); color: var(--ink);
  font-size: .92rem; line-height: 1.5;
}
.rtbox:focus { outline: none; border-color: var(--accent); }
/* The rich-text box draws its own placeholder, so it needs the same token —
   a rule that covers three of the four kinds of box is how one drifts back. */
.rtbox:empty::before { content: attr(data-ph); color: var(--ph); font-style: italic; }
.rtbox ul, .rtbox ol, .msgbody ul, .msgbody ol { margin: 6px 0; padding-left: 22px; }
.rtbox mark, .msgbody mark { background: #fde68a; color: #1b2028; padding: 0 2px; border-radius: 2px; }
.rtbox a, .msgbody a { color: var(--accent); }
.msgbody { line-height: 1.5; }
.msgbody p { margin: 0 0 8px; }
.msgbody p:last-child { margin-bottom: 0; }

/* Somebody else has this request open.
   Amber rather than red: it is information, not a refusal — two people CAN
   answer one customer, and occasionally two people should. It only needs to be
   noticed before the second reply gets typed. */
.watcheye {
  display: inline-block;
  margin-left: .4rem;
  padding: .05rem .35rem;
  border-radius: .5rem;
  background: #3a2f16;
  color: #e0a85c;
  font-size: .78em;
  font-weight: 600;
  white-space: nowrap;
  vertical-align: middle;
}

/* Who wrote a message, and when — under it rather than over it, because that
   is where a reader arrives at the question. Separated by a hairline so it
   reads as a signature and not as another line of the message. */
.msgsig {
  margin-top: .5rem;
  padding-top: .35rem;
  border-top: 1px solid rgba(255, 255, 255, .08);
  opacity: .85;
}

/* Dropping a file onto the composer. The whole card lights up rather than a
   small dashed box: somebody dragging a photo is looking at the photo, not at
   the screen, and a small target is one they will miss. */
.dropzone{position:relative}
.dropzone.dropping{outline:2px dashed var(--accent);outline-offset:3px;border-radius:10px}
.dropzone.dropping::after{content:"Drop it here to attach it";position:absolute;inset:0;
  display:flex;align-items:center;justify-content:center;pointer-events:none;
  background:color-mix(in srgb, var(--accent) 10%, transparent);
  color:var(--accent);font-weight:600;border-radius:10px}

/* ------------------------------------------------------------------ calendar
   The month grid, and its phone-shaped twin.

   The grid is a real <table>: seven equal columns that stay aligned down six
   rows is exactly what a table does, and a grid of divs re-implements it badly.
   It is only ever drawn above the phone breakpoint — app.js picks the agenda
   below it — so there is no column-collapsing to do here.

   Squares are a fixed minimum height rather than content height, because a
   month whose rows jump between 40px and 180px is a month you cannot scan. */
/* minmax(0, 1fr), not 1fr. A bare 1fr has a min-content floor, so one long
   appointment title makes its column wide and squeezes the rest -- which is
   exactly what the <table> this replaced did. The 0 is what allows a column to
   be narrower than the words in it, so the seven stay equal and the chips
   ellipsis instead. */
.calgrid{display:grid;grid-template-columns:repeat(7, minmax(0, 1fr));gap:0}
.calhead{padding:6px 4px;font-size:0.78rem;color:var(--muted);font-weight:600;
  border-bottom:1px solid var(--line)}
.calcell{border:1px solid var(--line);padding:4px;min-height:104px;min-width:0;
  cursor:pointer;transition:background .12s}
.calcell:hover,.calcell:focus{background:var(--panel2);outline:none}
.calcell:focus{box-shadow:inset 0 0 0 2px var(--accent)}
/* The days either side of the month are shown rather than left blank: a week
   that runs across the turn of the month is still one week to whoever works it. */
.calout{opacity:.42}
.caltoday{box-shadow:inset 0 0 0 2px var(--accent)}
.caldate{font-size:0.8rem;font-weight:700;color:var(--muted);margin-bottom:3px}
.caltoday .caldate{color:var(--accent)}
.calchip{font-size:0.75rem;line-height:1.25;padding:2px 4px;margin-bottom:2px;border-radius:4px;
  background:var(--accent-soft);color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.calchip .caltime{color:var(--muted);font-variant-numeric:tabular-nums}
/* Something Josh put on the calendar himself. Flat and outlined rather than
   filled: it is on the screen for context and the app cannot change it, and it
   should not look like a thing to press. */
.calchip.calgoogle{background:transparent;border:1px solid var(--line);color:var(--muted)}
/* Booked in the app and NOT on the calendar Josh reads. It looks booked and he
   cannot see it, which is the one state on this screen worth a red edge. */
.calchip.calstuck{border-left:3px solid var(--danger)}
.calmore{font-size:0.72rem;color:var(--muted);padding:1px 4px}

.calagday{border-top:1px solid var(--line);padding:8px 0}
.calagday:first-child{border-top:none}
.calagdate{font-size:0.82rem;font-weight:700;color:var(--muted);margin-bottom:4px}
.calagday.caltoday .calagdate{color:var(--accent)}
.calagday.caltoday{box-shadow:none;border-left:3px solid var(--accent);padding-left:8px}

.calrow{display:flex;gap:10px;padding:7px 4px;border-radius:6px;align-items:flex-start}
.calrow.calmine{cursor:pointer}
.calrow.calmine:hover,.calrow.calmine:focus{background:var(--panel2);outline:none}
.calrow.calmine:focus{box-shadow:inset 0 0 0 2px var(--accent)}
/* Not clickable, and it has to look it — otherwise every tap on one of Josh's
   own Google events is a tap that does nothing and reads as a broken screen. */
.calrow.calgoogle{opacity:.7;cursor:default}
.calwhen{flex:0 0 76px;font-size:0.8rem;color:var(--muted);font-variant-numeric:tabular-nums;
  padding-top:1px}
.calwhat{flex:1 1 auto;min-width:0}
.caltitle{font-weight:600;line-height:1.3}

/* ------------------------------------------------- service history on a client
   A visit row carries more badges than an ordinary list item: what was charged,
   whether it was a go back, whether the customer's copy went out. On a phone
   three of those beside the text leave the text about ninety pixels wide, and
   the date comes out as "Oct 5, 202…" — which is the one thing on the row
   somebody is actually reading.

   So the row wraps: the words take the full width and the badges drop beneath
   them. Above the phone breakpoint there is room for both on one line, and the
   rule reverts. */
.svcrow{flex-wrap:wrap;row-gap:6px}
.svcrow .grow{flex:1 1 220px}
/* The chevron is decoration on a row that is entirely tappable; at narrow
   widths it is one more thing competing for the line. */
@media (max-width: 700px) {
  .svcrow{align-items:flex-start}
  .svcrow .grow{flex:1 1 100%}
  /* The title can breathe now that it owns the line — no reason to keep it to
     one. */
  .svcrow .t{white-space:normal}
  .svcrow .s{white-space:normal;overflow:visible;text-overflow:clip}
  .svcrow > .muted{display:none}
}

/* ---- The request screen ----
 *
 * Morgan: "its too busy". Three columns — what the request IS down the left,
 * what was SAID down the middle, who it is FOR down the right. Fixed outer
 * columns and a fractional middle, because the conversation is the part a
 * wide screen helps and a status dropdown is not improved by being 400px
 * across. minmax(0, 1fr), or one long unbroken address in a message blows the
 * column out and takes the layout with it.
 *
 * This block was six rounds of something more ambitious: a fixed shell the
 * height of the window, the conversation scrolling in its own pane, the
 * composer pinned to the bottom, folding itself away when idle, capped as a
 * share of the column. Every round of it cost something — a clipped Send
 * button, a conversation squeezed to 30px, a box that resized under the
 * cursor — and all of it was buying one thing: never having to scroll.
 *
 * Joel: "its fine if i have to scroll down the page. im not trying to get it
 * to fit my screen."
 *
 * So none of it was needed. The page scrolls like a page. What he actually
 * asked for three times is all that is left: the box is one size, it is big,
 * and every part of the composer is on screen at once.
 */
.reqgrid { display: grid; gap: 14px; align-items: start; grid-template-columns: 1fr; }
@media (min-width: 1180px) {
  .reqgrid { grid-template-columns: 290px minmax(0, 1fr) 300px; }
  /* The side columns follow the conversation rather than scrolling away from
     it: a request with forty messages is exactly when you still want the
     status dropdown and the customer's phone number on the screen. */
  .reqgrid > .reqside { position: sticky; top: 12px; }
  .reqgrid > .reqside > .card:last-child { margin-bottom: 0; }
}
/* Between a phone and a desk: two columns, and the customer panel goes under
   the properties rather than squeezing the conversation to nothing. */
@media (min-width: 820px) and (max-width: 1179.98px) {
  .reqgrid { grid-template-columns: 280px minmax(0, 1fr); }
  .reqgrid > .reqmid { grid-row: span 2; }
}

/* The properties stack is a column of fields, so they go one per line rather
   than two half-width ones that each hold a truncated name. Chip rows are the
   exception and were a real bug: a full-width basis put Monday to Friday on
   five separate lines, 284px of them, and made that column 1088px tall.
   Chips take the width of their own words. */
.reqside .row:not(.chiprow) > * { flex: 1 1 100%; }
.reqside .row.chiprow > * { flex: 0 0 auto; }
.reqside .card { padding: 12px; }

/* Their other requests, down the right. Zendesk calls it interaction history;
   what it answers is "have we heard from these people before", which is the
   question you ask before deciding how to word a reply. */
.reqhist { display: flex; flex-direction: column; gap: 2px; }
.reqhist button {
  display: block; width: 100%; text-align: left; background: none; border: 0;
  border-radius: 8px; padding: 7px 8px; cursor: pointer; color: var(--ink);
  font: inherit; line-height: 1.35;
}
.reqhist button:hover { background: var(--panel-2, rgba(127,127,127,.12)); }
.reqhist .t { display: block; font-size: .8125rem; }
.reqhist .s { display: block; font-size: .72rem; color: var(--muted); margin-top: 1px; }

/* ---- Locked top, locked bottom, scrolling middle ----
 *
 * Joel, with a Zendesk screenshot: "need to make the top section locked and
 * the bottom section locked with emails and internal notes scrollable like it
 * is in zendesk".
 *
 * This existed earlier today and was taken out again when he said "its fine
 * if i have to scroll down the page" — so it is back on purpose, not by
 * accident, and the note is here so nobody removes it a third time on the
 * strength of that sentence.
 *
 * The reason it failed the first time was arithmetic, not approach. On a
 * 731px window there are about 430px for the column, and the composer was
 * taking 400 of them, which left the conversation a 30px strip. So the thing
 * that had to change was the composer, not the layout: it is slimmer here,
 * and the middle gets a usable share.
 *
 * .reqscreen scopes all of it, because .ticketmain is also the ticket queue
 * and a fixed height there would stop the list scrolling.
 */
@media (min-width: 820px) {
  /* --rqgrow is how far past the window the column has been dragged. Zero
     unless somebody has pulled the handle past the point where the
     conversation would go under its floor, and then the page scrolls to
     reach the composer -- Joel, picking that over a handle that stops:
     "its fine if i have to scroll down the page". */
  .tickets.reqscreen,
  .tickets.reqscreen .ticketmain {
    height: calc(100vh - var(--reqsticky, 160px) - 16px + var(--rqgrow, 0px));
  }
  .tickets.reqscreen .ticketmain { display: flex; flex-direction: column; min-height: 0; }
  .tickets.reqscreen .reqgrid { flex: 1 1 auto; min-height: 0; height: auto; gap: 10px; }
  /* The header is read once and then sits there for as long as the request is
     open, so it gets no padding it does not need. */
  .tickets.reqscreen .ticketmain > .card:first-child { padding: 8px 12px; margin-bottom: 8px; }

  /* Everything that would otherwise make the PAGE scroll, which is what
     "locked" means. Each of these held it open once:
       the view rail, longer than the window on its own;
       its drag handle, align-self: stretch, sizing itself to the rail's
       CONTENT rather than the shell — 707px of invisible nothing;
       and the page's 80px bottom padding, which is room for a floating
       button on a page that scrolls and dead space on one that does not. */
  .tickets.reqscreen .viewrail { height: 100%; max-height: 100%; overflow-y: auto; position: static; }
  .tickets.reqscreen .railgrip { align-self: stretch; height: auto; max-height: 100%; }
  main:has(.tickets.reqscreen) { padding-bottom: 0; }
  /* Flush to the bottom of the window is fine when nothing scrolls. Once it
     does, the Send button wants a little air under it or it reads as cut
     off -- which is the complaint this whole screen started from. */
  main:has(.tickets.reqscreen.reqgrown) { padding-bottom: 16px; }
  .reqgrid > .reqside { height: 100%; min-height: 0; overflow-y: auto; }
  /* The middle column has to be told its height too, or the conversation
     inside it has nothing to be a share OF and simply grows, pushing the
     composer off the bottom of the screen. */
  .reqgrid > .reqmid { height: 100%; min-height: 0; }

  /* The middle: properties above, conversation taking the slack and
     scrolling, composer locked to the foot of the column.
     .reqthread is a class on the card rather than a :has() or an :nth-child,
     because the first attempt used `.card:not(#rreplycard)` and an id inside
     :not() counts as an id for specificity — it outranked the rule meant to
     win, and the whole pane silently did nothing. */
  .reqmid { display: flex; flex-direction: column; }
  .reqmid > .card { flex: 0 0 auto; }
  .reqmid > .reqthread { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
  .reqmid > #rreplycard { flex: 0 0 auto; margin-bottom: 0; }
}

/* ---- The handle between them ----
 *
 * Joel, with arrows drawn at the conversation and at the typing box: "can you
 * make these boxes draggable to make bigger?"
 *
 * ONE handle, not two. The two of them share a column of fixed height, so
 * there is only ever one number to set: every pixel the typing box takes is a
 * pixel off the conversation above it. Two handles would be two ways to set
 * the same number, and whichever was dragged second would appear to undo the
 * first.
 *
 * It is a real <button>, so it takes focus and answers the arrow keys. Dragging
 * a 3px line with a mouse is fine until the day you are doing it one-handed
 * with a phone in the other.
 *
 * Only where the locked layout is. Below 820px the column is not a fixed
 * height and there is nothing to divide, so the handle would be a control that
 * does nothing — worse than no control.
 */
.reqsplit { display: none; }
@media (min-width: 820px) {
  .reqmid .reqsplit {
    display: block; flex: 0 0 auto; width: 100%; height: 13px;
    margin: -3px 0 1px; padding: 0; border: 0; background: transparent;
    cursor: row-resize; position: relative; border-radius: 4px;
  }
  /* The typing box's own grip, on its bottom edge. Same control, one size
     down: it is sizing a box inside a card rather than a whole pane. */
  .reqmid .reqsplit.boxgrip { height: 11px; margin: 1px 0 0; }
  .reqmid .reqsplit.boxgrip::before { width: 40px; }
  .reqmid .reqsplit.boxgrip:hover::before,
  .reqmid .reqsplit.boxgrip:focus-visible::before,
  .reqmid .reqsplit.boxgrip.on::before { width: 60px; }

  /* Once the conversation has been dragged it holds the height it was given,
     instead of taking whatever the window leaves. Joel, arrow drawn at the
     conversation: "I need this box to get bigger" -- on a 731px window there
     is no "whatever is left" worth having, so it stops being a share and
     becomes a number. */
  .tickets.reqthreadset .reqmid > .reqthread { flex: 0 0 var(--rqthread); }
  /* The grip is drawn, the target is the whole 13px strip. A 3px hit area is
     a 3px hit area however thin the line looks. */
  .reqmid .reqsplit::before {
    content: ''; position: absolute; left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    width: 56px; height: 4px; border-radius: 3px;
    /* --line is the colour of a border nobody is meant to notice, and at rest
       this was invisible on the dark theme -- a handle you cannot see is not
       a handle. --sec-line is the one that says "this is a control". */
    background: var(--sec-line);
    transition: background .12s, width .12s, height .12s;
  }
  .reqmid .reqsplit:hover::before,
  .reqmid .reqsplit:focus-visible::before,
  .reqmid .reqsplit.on::before {
    background: var(--accent); width: 84px; height: 5px;
  }
  /* Focus shows on the grip, not as a box round the whole strip: the strip is
     the width of the column and an outline that size reads as the column
     being selected rather than the handle. The grip going accent-coloured and
     wider is the same information in the place the eye is already looking. */
  .reqmid .reqsplit:focus-visible { outline: none; }
  .reqmid .reqsplit:focus-visible::before { box-shadow: 0 0 0 2px var(--accent-soft); }
  /* While dragging, the pointer is often outside the handle and over text it
     would otherwise be selecting. */
  body.reqsplitting, body.reqsplitting * { user-select: none !important; }
  body.reqsplitting { cursor: row-resize !important; }
}

/* ---- The reply box ----
 *
 * Joel, three times: "lock this down so it doesnt scroll", "cant you just
 * lock this box down? its too small now", "That should show the whole box."
 *
 * One size. It does not fold, it does not shrink to fit what is left, it is
 * not capped and it is never clipped — every one of those hid the Send button
 * or moved the box under the cursor. The only thing that scrolls is the box
 * itself, once you have typed more than fits in it, which is what a box you
 * type in is expected to do.
 */
@media (min-width: 820px) {
  /* Joel: "the conversation window needs to be bigger like it is in zendesk".
     In Zendesk the thread is the big thing and the composer is a strip at the
     bottom; here it had been the other way round — 333px of composer against
     96px of conversation on his screen. So the composer is a strip too. 110
     is more than twice what it was at its worst, and everything past it comes
     straight out of the thread above. */
  .reqmid #rreplycard .rtbox,
  .reqmid #rreplycard #rnote {
    height: var(--rqbox, 104px); min-height: var(--rqbox, 104px); max-height: var(--rqbox, 104px);
  }
  /* Every other pixel in the card is frame, and frame is what the
     conversation is competing with. */
  .reqmid #rreplycard { padding: 8px 10px; }
  .reqmid #rreplycard .subtabs button { padding-top: 4px; padding-bottom: 4px; }
  .reqmid #rreplycard #rcompose > .row { margin-top: 3px !important; margin-bottom: 0 !important; }
  .reqmid #rreplycard .rtbox { margin-top: 3px; margin-bottom: 0; }
  /* The controls in a strip are a strip's size. Full-height dropdowns and a
     full-height Send were another 40px of the conversation between them. */
  .reqmid #rreplycard select,
  .reqmid #rreplycard .btn {
    padding-top: 5px; padding-bottom: 5px; font-size: .86rem;
  }
  .reqmid #rreplycard .subtabs { margin-bottom: 0; }
  .reqmid #rreplycard .subtabs button { font-size: .86rem; }

  /* The rest of the card is frame, and frame is what the conversation is
     competing with. The "Goes to ..." line says what the panel on the right
     already says; the small print under it is a nudge you read once. */
  .reqmid #rreplycard h2 { display: none; }
  .reqmid #rreplycard #rcompose > .tiny.muted { display: none; }
  .reqmid #rreplycard .row span.tiny.muted { display: none; }
  .reqmid #rreplycard .subtabs { margin-bottom: 2px; }

  /* Nothing in here wraps. The real middle column is narrower than the test
     fixture's, so on production the two tabs were taking two lines (92px) and
     the saved-reply picker plus the toolbar three (104px) — 100px of the
     composer spent on wrapping alone, which no screenshot explains. */
  .reqmid #rreplycard .subtabs { flex-wrap: nowrap; }
  .reqmid #rreplycard .subtabs button {
    flex: 1 1 0; min-width: 0; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis; padding: 6px 8px;
  }
  .reqmid #rreplycard .rqtools { flex-wrap: nowrap; gap: 6px; }
  .reqmid #rreplycard .rqtools > #rsaved { flex: 1 1 0; min-width: 0; }
  .reqmid #rreplycard .rqtools > .rtbar { flex: 0 0 auto; }
  .reqmid #rreplycard .rtbar { flex-wrap: nowrap; gap: 3px; }
  .reqmid #rreplycard .rtbar button { padding: 3px 6px; font-size: .78rem; }
  .reqmid #rreplycard #rcompose > .row:last-child { flex-wrap: nowrap; gap: 6px; }
  .reqmid #rreplycard #rcompose > .row:last-child > button { flex: 0 0 auto; }
  .reqmid #rreplycard #rcompose > .row:last-child > select { flex: 1 1 0; min-width: 0; }
}


/* ---------------------------------------------------------------- phone
 *
 * Joel: "The app looks very cramped on a phone." Measured on Dispatch at
 * 390x844 it was not cramped so much as buried: the board started 637px down
 * an 844px screen, so a tech opening the app saw one strip of job and the rest
 * was controls. Three things each wrapped onto a row they did not need:
 *
 *   the header  - the wordmark, the name and two buttons want more than 390px
 *   the week    - seven day chips at a 46px floor want 430px inside a 316px box
 *   the actions - six controls in a row that can only hold three
 *
 * Nothing is removed. The week strip shares the width it has instead of
 * demanding a fixed one, and the controls that change HOW the board is drawn
 * fold behind More, leaving the ones that change WHAT it shows.
 */
@media (max-width:560px){
  /* The two cards inside the card. A border inside a border inside a border
     cost about 40px of width and height for decoration. */
  .weekpick,.daypick{background:none;border:0;padding:0}
  .dispctl{padding:10px}

  /* The header on one row. It wrapped because the wordmark, the name and two
     buttons want about 410px and a phone has 390 - and the tab strip measures
     this bar at runtime, so every pixel it wrapped pushed the whole board
     down. A smaller wordmark and tighter text is the whole fix; nothing is
     taken out of the row. */
  .topbar .logo{height:30px}
  .topbar .who{font-size:0.78rem}
  .topbar button{padding:5px 9px;font-size:0.78rem}

  /* Seven days, one row, any width. The base rule gives each day chip a 46px
     floor, which at a 5px gap wants 430px inside a box a 390px phone can only
     give 316px of - so it wrapped. Sharing the width that exists instead of
     demanding a fixed amount cannot wrap by construction, and holds on a 320px
     phone as well as a 430px one.
     flex-wrap:nowrap was here too and did nothing: chips that shrink never
     reach the point of wrapping. Left in, it would read as the thing keeping
     the row together, and the next person would keep it and drop the line that
     actually does the work. */
  .weekpick .chips{gap:4px}
  .weekpick .chip.day{flex:1 1 0;min-width:0;padding:5px 2px}
  .weekpick .chip.nav{flex:0 0 auto;padding:5px 8px}

  .daypick{margin-top:10px}
  .daypick input[type=date]{flex:1 1 0;min-width:0}
}

/* Folded away on a phone, always open on a desktop.
 *
 * display:contents rather than a wrapper that lays anything out: the buttons
 * inside have to stay direct flex children of the chip row, or folding them
 * away would reflow the row that is left. */
.dispctl{padding:12px}
.dispmore{display:contents}
.dispmorebtn{display:none}
@media (max-width:560px){
  .dispmorebtn{display:inline-block}
  .dispmore{display:none}
  .dispmore.open{display:contents}
}

/* ---- the scheduling note, on the row
   Aliyah: "is there a way to make scheduling notes viewable from the
   work/dispatch tab? ... that way we dont need to click into the job to find
   those important notes."

   Deliberately NOT .notebox: that one is a block on a job screen with one
   note on it, and twenty of them down a board would be a wall. This is the
   same signal — the accent rule on the left, the small uppercase label —
   at a size that can appear on every row without taking the board over. */
.schedline{display:block;width:100%;text-align:left;cursor:pointer;
  border:0;border-left:3px solid var(--accent);border-radius:0 6px 6px 0;
  background:var(--panel2);padding:5px 9px;margin:4px 0 0;
  font-size:0.8rem;line-height:1.4;color:var(--ink)}
.schedline:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
.schedline .lbl{font-size:0.6rem;text-transform:uppercase;letter-spacing:.6px;
  font-weight:700;color:var(--accent);margin-right:7px}
/* Two lines, then stop. One long note must not push the next four stops off
   the screen -- pressing it opens the rest. */
.schedline .txt{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  overflow:hidden;white-space:pre-wrap;max-width:110ch;vertical-align:top}
.schedline.open .txt{display:block;overflow:visible}
/* In the table it is a row of its own under the job, so it needs none of the
   cell padding around it and must not look like a separate stop. */
tr.schedrow > td{padding:0 10px 7px 10px;border-top:0}
tr.schedrow.closed > td{opacity:.6}
/* A list row is a flex line; the note takes a line of its own below it.
   Wrapping is the whole of it -- .schedline is already width:100%, so once
   the row is allowed to wrap, a full-width child cannot sit beside anything.
   A flex-basis here as well read like the thing doing the work and was not;
   mutating it away changed nothing on the screen. */
.item.hasnote{flex-wrap:wrap}

/* --------------------------------------------- quotes on a request */
/*
 * The same list row, in a 280px rail.
 *
 * .item was drawn for a list the width of the screen, where a quote called
 * "Rodent exclusion and general perimeter pest control" sits on one line. In
 * the side panel of a request it wrapped to one word per line and the row
 * grew to ten of them -- legible, in the way a column of single words is
 * legible, and three times the height of the card it lives in.
 *
 * So the row gets two lines of its own: what it is called across the top,
 * clipped rather than wrapped, and the money and the status underneath. The
 * chevron goes: the whole row is a button and there is no room to spend on
 * saying so twice.
 */
#rquotecard .item{flex-wrap:wrap;gap:4px 8px;padding:10px 12px}
#rquotecard .item .grow{flex:1 0 100%}
#rquotecard .item .t{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#rquotecard .item > .muted{display:none}

/*
 * A pill you can press.
 *
 * The cancelled count on the day header both reports and reveals, and the
 * four pills beside it do nothing at all -- so it has to look like the one
 * that can be pressed rather than like them.
 */
button.pill.tap{border:0;cursor:pointer;font-family:inherit;font-size:0.8rem;
  font-weight:650;padding:2px 9px;line-height:1.5}
button.pill.tap:hover{filter:brightness(0.94)}
button.pill.tap.on{background:var(--accent);color:var(--on-accent)}

/* ------------------------------------------------- a photo, full size */
/*
 * Aaron: "On Apple when you click into a picture in a ticket it does not
 * have a way to get back to the ticket."
 *
 * Saved to a home screen there is no browser around the app -- no back
 * button, no tabs -- so a photo that opened in a new tab replaced the app in
 * the only window it had. It opens over the screen now instead, and the
 * Close button is the one thing guaranteed to be on top of it.
 */
button.zoomer{font-family:inherit;color:inherit;text-align:left;cursor:zoom-in}
.shot > button.zoomer{display:block;width:100%;padding:0;border:0;background:none}

/* The crew's photos on the dispatch board: a camera on the row, and a viewer over the page (under the full-screen zoom, z 9000). */
.clkmark{font-size:1rem;line-height:1;margin-right:4px;cursor:default}
/* The crew's comments: the newest as a line under the job, the thread in a box. */
.schedline.crewline{margin-top:4px;border-left-color:#c9791b}
.schedline.crewline .lbl{color:#c9791b}
.cmcard{max-width:560px}
.cmlist{max-height:50vh;overflow:auto;display:flex;flex-direction:column;gap:8px}
.cmitem{border:1px solid var(--line);border-radius:8px;padding:8px 10px;background:var(--panel)}
.cmmeta{font-size:.8rem;margin-bottom:2px}
.cmbody{white-space:pre-wrap;word-break:break-word;font-size:.92rem}
.cmadd{display:flex;flex-direction:column;gap:6px;margin-top:10px}
.cmtext{width:100%;box-sizing:border-box}
.phmark{display:inline-flex;align-items:center;gap:2px;padding:0 4px;margin-right:2px;border:0;background:none;color:inherit;font-size:1rem;line-height:1;cursor:pointer;border-radius:6px;vertical-align:baseline}
.phmark span{font-size:.7rem;font-weight:700;background:var(--accent-soft);border-radius:8px;padding:0 5px}
.phmark:hover{background:var(--accent-soft)}
.galbox{position:fixed;inset:0;z-index:8500;background:rgba(0,0,0,.72);display:flex;align-items:center;justify-content:center;padding:16px}
.galcard{background:var(--panel2);color:var(--ink);border:1px solid var(--line);border-radius:12px;width:min(980px,100%);max-height:100%;display:flex;flex-direction:column;gap:10px;padding:12px;box-shadow:0 10px 40px rgba(0,0,0,.5)}
.galhead{display:flex;align-items:center;gap:10px}
.galhead .grow{flex:1}
.galmain{display:flex;align-items:center;gap:8px;min-height:0}
.galpic{flex:1;display:flex;align-items:center;justify-content:center;min-height:200px;min-width:0}
.galpic button.zoomer{padding:0;border:0;background:none;display:block;cursor:zoom-in}
.galpic img{max-width:100%;max-height:62vh;object-fit:contain;border-radius:8px;display:block}
.galnav{flex:0 0 auto;width:40px;height:56px;border-radius:10px;border:1px solid var(--line);background:var(--panel);color:inherit;font-size:1.8rem;line-height:1;cursor:pointer}
.galnav:hover{background:var(--accent-soft)}
.galcap{text-align:center;opacity:.85;min-height:1.2em}
.galstrip{display:flex;gap:6px;overflow-x:auto;padding-bottom:2px}
.galthumb{flex:0 0 auto;width:64px;height:64px;padding:0;border:2px solid transparent;border-radius:8px;overflow:hidden;background:none;cursor:pointer;opacity:.7}
.galthumb.on{border-color:#4f9cf9;opacity:1}
.galthumb img{width:100%;height:100%;object-fit:cover;display:block}
@media (max-width:700px){ .galnav{width:30px} .galpic img{max-height:50vh} }
.shotbox{position:fixed;inset:0;z-index:9000;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:12px;background:rgba(0,0,0,.93);
  padding:calc(env(safe-area-inset-top,0px) + 64px) 12px
          calc(env(safe-area-inset-bottom,0px) + 18px)}
.shotbox img{max-width:100%;max-height:100%;object-fit:contain;border-radius:6px}
.shotbox .shotcap{color:#fff;font-size:0.875rem;line-height:1.4;text-align:center;
  max-width:760px;word-break:break-word}
/* Big, white, and in the corner every phone puts a close button in. 44px is
   the smallest thing a thumb hits reliably. */
.shotbox .shotx{position:absolute;
  top:calc(env(safe-area-inset-top,0px) + 12px);
  right:calc(env(safe-area-inset-right,0px) + 12px);
  min-height:44px;padding:10px 20px;border:0;border-radius:999px;
  background:#fff;color:#111;font-family:inherit;font-size:1rem;font-weight:650;
  cursor:pointer}
.shotbox .shotx:hover{background:#e8e8e8}
/* The page behind must not scroll under the photo. */
body.shotlock{overflow:hidden}

/*
 * The alert panel, on a phone.
 *
 * Aaron... no: Joel, on an iPhone — "This is cut off on mobile". The panel
 * hangs off the bell with right:0, which on a wide screen puts it neatly
 * under the bell and on a 390px phone puts 58 pixels of it off the left
 * edge: every alert lost the front of its line, which is the half that says
 * WHO — "...er Musolf replied", "...Westerholm gave you this request".
 *
 * Narrower than the panel is wide, the bell is simply not far enough from
 * the left edge to hang 360px off, and no amount of width clamping fixes
 * that while the anchor is the bell. So on a phone it hangs off the VIEWPORT
 * instead: pinned to the right edge of the screen, capped at the same width,
 * starting under the header — whose real height the app measures into --barh
 * because this header wraps onto two lines on a narrow screen.
 */
@media (max-width: 700px) {
  .bellpanel{
    position:fixed;
    top:calc(var(--barh) + 6px);
    left:auto;
    right:calc(8px + var(--saferight));
    width:min(360px, calc(100vw - 16px - var(--safeleft) - var(--saferight)));
    max-height:min(70vh, 480px);
  }
}

/* A tiny line that has to be noticed rather than skimmed past: a customer
   with no payment method set is a customer whose invoices quietly carry no
   admin fee, and nobody goes looking for a gap. */
.tiny.warn-text{color:var(--warn);font-weight:600}

/* The time a message arrived, on the line you can actually see.
   Pushed to the right so it sits out of the way of "The customer wrote"
   rather than running on from it as part of the same sentence. */
.notebox .msgwhen{float:right;font-variant-numeric:tabular-nums}

/* ---------------------------------------------------------------- snow
 *
 * A driver is in a truck at 3am with a glove on and the phone in a mount.
 * Everything here is sized for a thumb aimed badly: the row IS the button,
 * it is 76px of target, and there is nothing small next to it to hit by
 * mistake. Nothing on this screen needs two hands.
 */
.plowrow{display:flex;align-items:center;gap:12px;width:100%;text-align:left;
  min-height:76px;padding:12px 14px;border:1px solid var(--line);border-radius:12px;
  background:var(--card);color:inherit;font:inherit;cursor:pointer;margin-bottom:8px}
.plowrow:active{transform:scale(.995)}
.plowrow .n{flex:0 0 34px;font-size:1.1rem;font-weight:700;color:var(--muted);text-align:right}
.plowrow .who{flex:1 1 auto;min-width:0}
.plowrow .addr{font-size:1.1rem;font-weight:700;line-height:1.25;word-break:break-word}
.plowrow .sub{font-size:.85rem;color:var(--muted);margin-top:2px}
.plowrow .mark{flex:0 0 auto;font-weight:700;font-size:.95rem;padding:12px 16px;
  border-radius:10px;background:var(--accent);color:#fff;white-space:nowrap}
/* Done, and still tappable: he will come back round. Dimmed, never removed —
   a stop that vanishes when you plow it is a stop you cannot check. */
.plowrow.done{opacity:.55}
.plowrow.done .mark{background:transparent;color:var(--muted);border:1px solid var(--line)}
.plowrow .count{flex:0 0 auto;font-weight:700;color:var(--accent);white-space:nowrap}
/* The running tally, pinned so it survives scrolling a 300-stop route. */
.snowbar{position:sticky;top:0;z-index:5;background:var(--bg);padding:10px 0 8px;
  display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;border-bottom:1px solid var(--line)}
.snowbar .big{font-size:1.35rem;font-weight:700}
.routecard{display:flex;align-items:center;gap:14px;width:100%;text-align:left;
  padding:14px;border:1px solid var(--line);border-radius:12px;background:var(--card);
  color:inherit;font:inherit;cursor:pointer;margin-bottom:10px}
.routecard .bar{flex:1 1 auto;height:10px;border-radius:5px;background:var(--line);overflow:hidden}
.routecard .bar i{display:block;height:100%;background:var(--accent)}
@media (max-width:620px){
  .plowrow{min-height:84px}
  .plowrow .mark{padding:14px 14px}
  .plowrow .n{flex-basis:26px}
}

/* Space left on a snow route.
   Joel: "it needs to be set up to accommodate the people that have signed up
   knowing more will come." The room left is the number somebody cutting
   routes in October is actually reading, so it gets colour and the stop
   count does not. Over-capacity is red because a route past what it was
   built for is a truck that will not finish before the school run. */
.roomleft{color:var(--ok);font-weight:650}
.roomover{color:var(--danger);font-weight:650}

/* A job past its end date and still not done. Joel: the tile turns red. */
.item.overdue{background:var(--danger-soft);box-shadow:inset 4px 0 0 var(--danger)}

.custlink{cursor:pointer;text-decoration:underline dotted;text-underline-offset:2px}
.custlink:hover,.custlink:focus-visible{color:var(--accent);text-decoration-style:solid;outline:none}

.crewlegend{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:6px}
.crewkey{display:inline-flex;align-items:center;gap:5px;font-size:.8125rem}
.crewkey i{width:12px;height:12px;border-radius:50%;display:inline-block;border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.35);box-sizing:content-box}

.mapkey{display:inline-block;width:10px;height:10px;border-radius:50%;margin:0 3px 0 8px;vertical-align:baseline}
#rstops .item.picked{background:var(--accent-soft)}
#rstops .item.dragging{opacity:.4}
#rstops .item.dropbefore{box-shadow:inset 0 3px 0 var(--accent)}
#rstops .item.dropafter{box-shadow:inset 0 -3px 0 var(--accent)}

/* The standing billing note under a customer's name in the billing lists. */
.billnote{margin-top:3px;padding:2px 7px;border-left:3px solid var(--warn,#b7791f);background:rgba(183,121,31,.10);border-radius:4px;max-width:34ch;line-height:1.35}

/* ---- Client page: glanceable top, two columns below (Aliyah / Morgan) ---- */
.chead-top{display:flex;gap:14px;justify-content:space-between;align-items:flex-start;flex-wrap:wrap}
.chead-name{margin:0;font-size:1.35rem}
.chead-lines{margin-top:6px;line-height:1.5}
.chead-btns{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
.chead-btns a.btn{text-decoration:none;display:inline-flex;align-items:center}
.ckpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px;margin-bottom:14px}
.ckpis:empty{display:none}
.ktile{display:flex;flex-direction:column;gap:2px;text-align:left;background:var(--panel);border:1px solid var(--card-line);
  border-radius:var(--radius);padding:11px 13px;box-shadow:var(--shadow);font:inherit;color:inherit;cursor:pointer}
.ktile[tabindex="-1"]{cursor:default}
.ktile .klabel{font-size:0.72rem;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);font-weight:700}
.ktile .kval{font-size:1.2rem;font-weight:700;overflow-wrap:anywhere}
.ktile .ksub{font-size:0.82rem;color:var(--muted)}
.ktile.quiet .kval{color:var(--muted);font-weight:600}
.ktile.late .kval{color:var(--danger)}
.calert{display:flex;gap:10px;align-items:center;background:var(--warn-soft);color:var(--warn);border-radius:var(--radius);
  padding:9px 13px;margin-bottom:10px;font-weight:600}
.calert .grow{flex:1}
.cgrid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(300px,1fr);gap:14px;align-items:start}
.cmain,.cside{min-width:0}
.cfold{margin-top:4px}
.kv{display:flex;flex-direction:column;margin-top:8px}
.kvrow{display:flex;justify-content:space-between;gap:12px;padding:5px 0;border-bottom:1px solid var(--card-line)}
.kvrow:last-child{border-bottom:0}
.kvrow .k,.kvlong .k{color:var(--muted);font-size:0.85rem}
.kvrow .v{font-weight:650;text-align:right;overflow-wrap:anywhere}
.kvlong{padding:6px 0;white-space:pre-wrap}
.kvlong .k{display:block}
.cform{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:2px 10px;margin-top:8px}
.cform .field.wide{grid-column:1/-1}
@media (max-width:1100px){
  .cgrid{grid-template-columns:minmax(0,1fr)}
  .cside{order:-1}
}
@media (max-width:600px){
  .ckpis{grid-template-columns:1fr 1fr}
}
.item[hidden]{display:none}

/* The search box in the top bar - on every screen. */
.topbar .gsearch{flex:1 1 180px;max-width:380px;min-width:140px;position:relative}
.topbar .gsearch input{width:100%;margin:0;padding:8px 14px;border-radius:999px;font-size:.9rem}
.gres{position:absolute;left:0;right:0;top:calc(100% + 6px);z-index:60;min-width:260px;background:var(--panel);
  border:1px solid var(--line);border-radius:10px;box-shadow:0 10px 28px rgba(0,0,0,.35);overflow:hidden}
.gitem{display:block;width:100%;text-align:left;background:transparent;border:0;border-bottom:1px solid var(--line);
  padding:9px 12px;color:var(--ink);font:inherit;font-size:.92rem;cursor:pointer;border-radius:0}
.gitem span{display:block;font-size:.8rem;opacity:.7;margin-top:1px}
.gitem:hover,.gitem:focus{background:var(--accent-soft);outline:none}
.gitem.gall{border-bottom:0;font-weight:650;color:var(--accent)}
.gnone{padding:10px 12px;opacity:.7;font-size:.9rem}
@media (max-width:700px){ .topbar .gsearch{flex:1 1 100%;order:20;max-width:none} }

/* The middle of the customer header: properties, payment and contacts, where it used to be blank. */
.chead-mid{font-size:.92rem;flex:1 1 360px;display:flex;gap:22px;flex-wrap:wrap;align-self:stretch;padding:0 6px}
.chmid-col{flex:1 1 150px;min-width:140px;line-height:1.45}
.chlabel{font-size:.72rem;text-transform:uppercase;letter-spacing:.05em;opacity:.7;margin-bottom:2px}
.chline{margin:1px 0}
.chmid-col .linky{padding:0;text-align:left}
.warn-text{color:var(--warn)}
/* The balance on the customer header: the number people look for first, so it is the biggest thing there. */
.chbal{flex:0 0 auto;align-self:stretch;text-align:right;padding:0 10px;display:flex;flex-direction:column;justify-content:center}
.chbal-amt{font-size:2.1rem;font-weight:700;line-height:1.1;letter-spacing:-.01em}
.chbal-sub{font-size:.88rem;line-height:1.35}
@media (max-width:700px){ .chbal{text-align:left;padding:0;flex:1 1 100%} }

/* Dispatch: a dropdown you can tick several things in. */
.msel{position:relative;display:inline-block}
.msel-btn{width:auto;padding:6px 28px 6px 10px;font-size:.8438rem;text-align:left;background:var(--panel,#0f141b) ;color:inherit;border:1px solid var(--line,#2a3340);border-radius:8px;cursor:pointer;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;position:relative}
.msel-btn::after{content:'\25be';position:absolute;right:10px;top:50%;transform:translateY(-50%);opacity:.7}
.msel-btn.on{border-color:#4f9cf9}
.msel-panel{position:absolute;z-index:60;top:calc(100% + 4px);left:0;min-width:240px;max-height:min(60vh,460px);overflow:auto;background:var(--panel2);color:var(--ink);border:1px solid var(--line);border-radius:10px;padding:6px;box-shadow:0 10px 30px rgba(0,0,0,.45)}
.msel-panel[hidden]{display:none}
.msel-top{display:flex;align-items:center;flex-wrap:wrap;justify-content:space-between;gap:4px 10px;padding:2px 4px 6px}
.msel-top .tiny{flex:1 1 100%}
.msel-q{display:block;width:calc(100% - 8px);margin:0 4px 6px;padding:7px 10px;font-size:.875rem;box-sizing:border-box}
.msel-none{padding:8px}
.msel-grp{font-weight:700;padding:8px 8px 2px;font-size:.8rem;opacity:.85}
.msel-row{text-transform:none;letter-spacing:0;font-size:.875rem;color:inherit;display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;cursor:pointer;margin:0;font-weight:400}
.msel-row:hover{background:var(--accent-soft)}
.msel-row input[type=checkbox]{width:auto;flex:0 0 auto;margin:0}

/* Team Assignment: the SA-style crew board (Unassigned pool + a box per crew). */
.ta-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 1000; display: flex; align-items: flex-start; justify-content: center; overflow: auto; padding: 24px 12px; }
.ta-card { width: 100%; max-width: 720px; margin: 0; }
.ta-scope { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-bottom: 10px; font-size: .9rem; }
.ta-pool, .ta-crew { display: flex; align-items: flex-start; gap: 10px; padding: 7px 10px; border: 1px solid var(--line, #30363d); border-radius: 8px; margin-bottom: 6px; }
.ta-pool { flex-direction: column; background: rgba(255,255,255,.03); gap: 6px; }
.ta-head { display: flex; align-items: center; gap: 8px; width: 100%; }
.ta-head input { margin-left: auto; width: 160px; padding: 4px 8px; font-size: .85rem; }
.ta-crew { min-height: 40px; }
.ta-crew .ta-title { flex: 0 0 130px; font-size: .9rem; padding-top: 3px; }
.ta-title { font-weight: 600; }
.ta-dot { flex: 0 0 8px; height: 8px; border-radius: 50%; margin-top: 9px; }
.ta-chips { display: flex; flex-wrap: wrap; gap: 4px; flex: 1; align-items: center; min-height: 26px; }
.ta-chip { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: .8rem; cursor: grab; border: 1px solid var(--line, #3d444d); background: rgba(255,255,255,.06); user-select: none; }
.ta-chip.lead { border-color: #6f8fd8; }
.ta-chip.sel { outline: 2px solid #f2c94c; }
.ta-pool.over, .ta-crew.over { outline: 2px dashed var(--accent, #4f9cf9); }
.ta-card .row > button, .ta-card .row > .btn { flex: 0 0 auto !important; width: auto; }

/* Job site risk check: a full-screen sheet over the visit, so the crew leader fills it standing at the truck. */
.rk-ov{position:fixed;inset:0;z-index:1000;background:var(--bg);overflow:auto;-webkit-overflow-scrolling:touch}
.rk-sheet{max-width:640px;margin:0 auto;padding:0 14px}
.rk-head{position:sticky;top:0;background:var(--bg);display:flex;justify-content:space-between;align-items:center;gap:10px;padding:12px 0;border-bottom:1px solid var(--line);z-index:1}
.rk-body h3{margin:20px 0 8px;font-size:0.82rem;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);font-weight:700}
.rk-body textarea,.rk-body input{width:100%}
.rk-seg{display:flex;gap:8px;flex-wrap:wrap}
.rk-seg .chip{flex:1 1 0;text-align:center;padding:10px 8px;white-space:normal}
.rk-go,.rk-nogo{flex:1;padding:16px;font-size:1.05rem;font-weight:700;background:var(--sec-bg);color:var(--ink);border:1px solid var(--sec-line)}
.rk-go.on{background:var(--ok);border-color:var(--ok);color:#fff}
.rk-nogo.on{background:var(--danger);border-color:var(--danger);color:#fff}
.rk-sig{width:100%;height:150px;background:#fff;border-radius:8px;touch-action:none;border:1px solid var(--sec-line)}

/* Dispatch day list grouped by crew: a heading row above each crew's stops. */
.dispatch tr.grphead td{background:var(--panel2);border-top:2px solid var(--line);padding:9px 10px;font-size:0.9rem}
.crewdot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:7px;vertical-align:baseline}
.ok-text{color:var(--ok)}

.topbar .mapsel{width:auto;flex:0 0 auto;padding:5px 6px;font-size:0.8rem;margin:0 4px}
