/* ===================================================================
   DIVISION 1 ROOFING & EXTERIORS — additions
   Channels & DMs, photo avatars, admin management sheets.
   Loaded after styles.css. Same brand tokens, nothing overridden
   except where a rule is deliberately replacing an older one.
   =================================================================== */

/* ---------- channel strip: unread counts, DM markers, new-DM button ---------- */
.chip{position:relative;display:inline-flex;align-items:center;gap:6px}
.chip-badge{
  min-width:17px;height:17px;padding:0 4px;border-radius:999px;
  background:var(--purple);color:#fff;font-size:10px;font-weight:800;
  font-style:normal;display:grid;place-items:center;line-height:1;flex:none;
}
.chip.active .chip-badge{background:#04191b;color:var(--teal)}
.chip-dm{
  width:6px;height:6px;border-radius:50%;background:var(--purple);
  flex:none;font-style:normal;
}
.chip.active .chip-dm{background:#04191b}
.chip.new{border-style:dashed;color:var(--txt-3);font-weight:700}
/* Read vs unread, the way Slack draws it: an unread conversation is bold
   with its count; a read one sits quiet. Long-press opens the menu, so the
   browser's own text-selection callout has to stay out of the way. */
#chanStrip .chip{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
#chanStrip .chip .chip-name{font-weight:450;color:var(--txt-2)}
#chanStrip .chip.unread .chip-name{font-weight:800;color:var(--txt)}

  /* A # in front of every channel, in its own fixed column, so that every
     name on the list starts at the same place whether the row is a channel
     or a person. Without it the channels sit flush left and the DMs are
     pushed in by their avatar, and the left edge of the list is ragged. */
  #chanStrip .chip .chip-hash{
    width:24px;flex:none;text-align:center;font-style:normal;
    color:var(--txt-3);opacity:.5;font-weight:600;
  }
  #chanStrip .chip.unread .chip-hash{opacity:.85}

  /* Channels and people, told apart quietly. Not Slack's full collapsing
     section machinery -- just enough to say where one ends and the other
     begins, because the order inside each is still the order the app chose
     and these must not look like something you can sort or fold. */
  #chanStrip .chan-sec{
    font-size:10.5px;font-weight:700;letter-spacing:.9px;text-transform:uppercase;
    color:var(--txt-3);opacity:.62;padding:14px 10px 4px;
  }
  #chanStrip .chan-sec:first-child{padding-top:4px}
#chanStrip .chip.active .chip-name{color:inherit}
#chanStrip .chip:not(.unread) .chip-dm{background:var(--txt-3);opacity:.6}
#chanStrip .chip.active .chip-dm{opacity:1}
/* The other person's photo on a DM row, sized to the row. */
#chanStrip .chip .avatar.chip-ava{width:24px;height:24px;font-size:9px;flex:none;border-radius:50%}
#chanStrip .chip .avatar.chip-ava img{width:100%;height:100%;object-fit:cover;display:block}
#chanStrip .chip:not(.unread) .avatar.chip-ava{opacity:.75}

/* ---------- photo avatars ---------- */
.avatar{overflow:hidden;background-size:cover;background-position:center}
.avatar img,.brand-mark img{width:100%;height:100%;object-fit:cover;display:block}
.brand-mark{overflow:hidden}
.brand-mark.has-img{background:none}

/* ---------- sheets that hold a list ---------- */
.scroll-sheet{max-height:46vh;overflow-y:auto;-webkit-overflow-scrolling:touch;
  padding:2px;margin:0 -2px 14px}
.sheet-head{align-items:center}
.item.tap{cursor:pointer}
/* A face or a name in the transcript is a door to the person, so it has
   to look pressable without turning the transcript into a field of
   links. A pointer and a touch target, nothing louder. */
.msg [data-who]{cursor:pointer;-webkit-tap-highlight-color:transparent}
.msg-head b[data-who]:active,.msg span[data-who]:active{opacity:.6}
#personContact .item .who small{word-break:break-word}
#personContact .item .btn{margin-left:6px}
#personContact .item .who{margin-right:auto}
.item.tap:active{transform:scale(.995)}
.item .chev{color:var(--txt-3);font-size:18px;flex:none;line-height:1}
.btn.link.danger{color:var(--bad);font-weight:600}
.btn.on{background:var(--teal);border-color:var(--teal);color:#04191b}

/* "Invited" is a state, not a warning: somebody was added and has not got
   round to signing in. Quiet enough to scan past on a full team list, and
   there on the one day it matters -- the morning after everyone was
   invited and half of them cannot get in. */
.pill.wait{background:var(--field);color:var(--txt-3);border:1px solid var(--line)}

/* ---------- admin: channel rows and logo picker ---------- */
.logo-row{display:flex;align-items:center;gap:14px;margin-bottom:14px}
.logo-copy b{display:block;font-size:13.5px;font-weight:600;margin-bottom:2px}
.pill.dm{background:#6435d61f;color:var(--purple-ink)}
.pill.auto{background:#0f9d631f;color:var(--ok)}
.pill.pending{background:#b8860b1f;color:#8a6508}
.pill.disabled{background:#7679761f;color:var(--gray)}

/* ---------- empty chat ---------- */
.no-room{margin:auto;text-align:center;max-width:280px;color:var(--txt-3);
  font-size:13.5px;line-height:1.6}
.no-room b{display:block;color:var(--txt);font-size:15px;margin-bottom:6px}

/* ---------- message body ---------- */
.msg-body{position:relative}

/* ---------- sheets with a form in them ---------- */
.sheet .sec:first-of-type{margin-top:16px}

/* ---------- reordering channels ---------- */
.item .who.tap{cursor:pointer}

/* ---------- reactions ---------- */
.rx{display:flex;flex-wrap:wrap;gap:4px;margin-top:5px}
.msg.mine .rx{justify-content:flex-end}
.rx-pill{
  display:inline-flex;align-items:center;gap:4px;padding:2px 8px 2px 6px;
  border:1px solid var(--line);background:var(--card);border-radius:999px;
  font:inherit;font-size:12px;line-height:1.5;cursor:pointer;transition:.12s;
}
.rx-pill i{font-style:normal;font-size:10.5px;font-weight:700;color:var(--txt-2)}
.rx-pill.mine{background:#00b3bb1f;border-color:var(--teal)}
.rx-pill.mine i{color:var(--teal-ink)}
.rx-pill:active{transform:scale(.94)}

/* ---------- who put that there ----------
A count tells you the message landed; a name tells you who with. There
is nothing to hover on a phone, so the pill opens the list on a hold,
and the message card offers the same thing by name for anyone who
never discovers the hold. */
.rx-pill{cursor:pointer;-webkit-touch-callout:none;user-select:none}
.who-rx{display:flex;flex-direction:column;max-height:52vh;overflow-y:auto;margin-bottom:12px}
.who-rx .grp{display:flex;gap:12px;padding:10px 2px;border-bottom:1px solid var(--line)}
.who-rx .grp:last-child{border-bottom:0}
.who-rx .em{
flex:none;width:36px;height:36px;border-radius:11px;background:var(--field);
display:grid;place-items:center;font-size:19px;line-height:1;
}
.who-rx .names{flex:1;min-width:0;align-self:center;font-size:13.5px;line-height:1.4;color:var(--txt)}
.who-rx .names small{display:block;margin-top:1px;font-size:11px;color:var(--txt-3)}

/* The full set, grouped. Not the whole emoji keyboard -- a list you can
thumb through in one go, in the order a sales floor reaches for them. */
.rx-groups{margin:6px 0 14px;max-height:52vh;overflow-y:auto;-webkit-overflow-scrolling:touch}
.rx-group-name{
margin:14px 2px 7px;font-size:10.5px;font-weight:800;letter-spacing:.09em;
text-transform:uppercase;color:var(--txt-3);
}
.rx-group-name:first-child{margin-top:2px}
.rx-palette{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:6px}
.rx-big{
border:0;background:var(--field);border-radius:11px;padding:7px 0;
font-size:21px;line-height:1.15;cursor:pointer;transition:.12s;
}
.rx-big.mine{background:#00b3bb26;box-shadow:inset 0 0 0 1.5px var(--teal)}
.rx-big:active{transform:scale(.9)}

/* ---------- mentions ---------- */
.mention{color:var(--purple-ink);font-weight:600;background:#8958fe14;
  border-radius:4px;padding:0 2px}
.mention.all{color:var(--teal-ink);background:#00b3bb1f}
.msg.mine .mention{color:#d8ffff;background:#ffffff2e}
.msg.mine .mention.all{color:#fff;background:#ffffff38}
.msg.calls-me .bubble{border-left:3px solid var(--purple)}

.mention-bar{flex:none;display:flex;gap:6px;overflow-x:auto;padding:8px 12px;
  background:var(--bg-2);border-top:1px solid var(--line);scrollbar-width:none}
.mention-bar::-webkit-scrollbar{display:none}
.mention-opt{display:flex;align-items:center;gap:8px;flex:none;padding:6px 12px 6px 8px;
  border:1px solid var(--line);background:var(--card);border-radius:999px;
  font:inherit;cursor:pointer;text-align:left}
.mention-opt:active{border-color:var(--teal)}
.mention-txt{display:flex;flex-direction:column;line-height:1.25}
.mention-txt b{font-size:13px;font-weight:600;white-space:nowrap}
.mention-txt small{font-size:10.5px;color:var(--txt-3);white-space:nowrap}
.mention-all-mark{width:30px;height:30px;border-radius:50%;flex:none;display:grid;
  place-items:center;font-weight:800;font-size:14px;color:#04191b;
  background:linear-gradient(135deg,var(--teal),var(--purple))}

/* ---------- adding a teammate ---------- */
.inline-link{font-weight:600;color:var(--teal-ink);word-break:break-all}
#nuResult b{font-size:14.5px}
#nuCreds{max-height:none;font-size:12px;line-height:1.7;color:var(--txt);
  white-space:pre;word-break:normal;overflow-x:auto}
#mePwBox{margin-top:10px;padding-top:12px;border-top:1px solid var(--line)}

/* ---------- ServiceTitan panel ---------- */
#stOut{max-height:340px;font-size:11px;line-height:1.55;white-space:pre;
  word-break:normal;overflow:auto;margin-top:10px}
#stStatus{color:var(--txt-2);font-weight:600}

/* ================= v3 additions ================= */

/* ---------- avatars: a real circle ----------
   .avatar is a <button>, and a button carries default padding. The box is
   34x34 either way, but the padding shrinks the CONTENT box, and the <img>
   was sized to that -- so the photo rendered as a squat rectangle sitting
   inside a round button. Zero the padding and pin the image to the border
   box so it fills the circle whatever shape the photo is. */
.avatar{
  padding:0;border:0;position:relative;aspect-ratio:1;
  border-radius:50%;overflow:hidden;flex:none;
}
.avatar img{
  position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;border-radius:50%;display:block;
}

/* ---------- long press on a message ---------- */
.msg .bubble{
  -webkit-touch-callout:none;   /* no iOS copy/lookup bubble on hold */
  user-select:none;             /* Copy lives in the menu instead */
  transition:transform .12s ease, opacity .12s ease;
}
.msg .bubble.pressing{transform:scale(.975);opacity:.82}

/* ---------- press and hold: one card, emoji on top ----------
The old menu was four full-width slabs stacked in a sheet, and putting
a thumbs-up on a message cost two taps because the emoji lived on a
second screen. Now the emoji are the top shelf of the same card -- one
tap, and the shelf scrolls sideways through the rest -- with the
actions under them as a tight list rather than four more slabs. */
#msgSheet .sheet{padding:0;overflow:hidden}
.msg-card-head{
display:flex;align-items:baseline;gap:8px;padding:14px 16px 0;
font-size:12px;color:var(--txt-3);
}
.msg-card-head b{flex:none;font-size:12px;font-weight:700;color:var(--txt-2)}
.msg-card-head span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rx-shelf{
display:flex;align-items:center;gap:7px;padding:11px 12px;
overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;
border-bottom:1px solid var(--line);
}
.rx-shelf::-webkit-scrollbar{display:none}
.rx-quick{
flex:none;width:42px;height:42px;border:0;border-radius:13px;
background:var(--field);font-size:22px;line-height:1;cursor:pointer;
display:grid;place-items:center;transition:.12s;
}
.rx-quick.mine{background:#00b3bb26;box-shadow:inset 0 0 0 1.5px var(--teal)}
.rx-quick:active{transform:scale(.9)}
.rx-more{
flex:none;width:42px;height:42px;border-radius:13px;
border:1px dashed var(--line);background:none;color:var(--txt-3);
font:600 19px inherit;line-height:1;cursor:pointer;
display:grid;place-items:center;
}
.rx-more:active{transform:scale(.9)}
.msg-menu{display:flex;flex-direction:column;gap:0;margin:0;padding:5px 0 6px}
.row-btn{
display:flex;align-items:center;gap:13px;width:100%;
padding:12px 16px;border:0;border-radius:0;background:none;
color:var(--txt);font:inherit;font-weight:600;font-size:14.5px;
text-align:left;cursor:pointer;
}
.row-btn svg{width:18px;height:18px;flex:none;color:var(--txt-3)}
.row-btn:active{background:var(--field)}
.row-btn.danger{color:var(--bad)}
.row-btn.danger svg{color:var(--bad)}
#msgSheetClose{
margin:0;border-top:1px solid var(--line);border-radius:0;
padding:14px;padding-bottom:calc(14px + var(--safe-b));
}

/* ---------- replies ---------- */
.reply-quote{
  display:block;margin:0 0 6px;padding:5px 9px;
  border-left:3px solid var(--teal);border-radius:0 8px 8px 0;
  background:color-mix(in srgb,var(--teal) 9%,transparent);
  font-size:12.5px;line-height:1.35;
}
.reply-quote b{display:block;color:var(--teal);font-size:11.5px;font-weight:800}
.reply-quote span{color:var(--txt-2);display:block;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.reply-quote.gone{color:var(--txt-3);font-style:italic;border-left-color:var(--line);background:none}
/* Inside your own bubble the text is white, so a quote painted in page ink
   reads black on teal and cannot be made out at all. That is what this rule
   had been doing: --purple is #000000 and --purple-ink is near-black, so the
   quote was being painted in black on a black wash over a teal bubble.

   Slack does not keep a second palette for this. A quote borrows the colour
   of the text around it and sits on a wash of that same colour, so it stays
   legible whatever it is sitting on. Same idea here: white inside your own
   bubble, page ink everywhere else. */
.msg.mine .reply-quote{
  border-left-color:rgba(255,255,255,.6);
  background:rgba(255,255,255,.15);
}
.msg.mine .reply-quote b{color:#fff}
.msg.mine .reply-quote span{color:rgba(255,255,255,.88)}
.msg.mine .reply-quote.gone{
  color:rgba(255,255,255,.7);
  border-left-color:rgba(255,255,255,.35);
  background:rgba(255,255,255,.08);
}

.reply-bar{
  display:flex;align-items:center;gap:10px;
  padding:8px 14px;border-top:1px solid var(--line);background:var(--bg-2);
}
.reply-bar-txt{min-width:0;flex:1;font-size:12.5px;line-height:1.3}
.reply-bar-txt b{display:block;color:var(--teal);font-size:11.5px;font-weight:800}
.reply-bar-txt span{
  display:block;color:var(--txt-2);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.reply-x{
  border:0;background:none;color:var(--txt-3);font-size:22px;line-height:1;
  padding:0 4px;cursor:pointer;flex:none;
}

/* ---------- owner ---------- */
.pill.owner{background:var(--gold);color:#2a1c00;font-weight:800}

/* ---------- drag a channel into place ----------
   The rows animate to their new spot, except the one under the finger:
   that one has to track the pointer exactly, so its transition is off
   while it is held. */
.chan-row{position:relative;background:var(--card-2)}

/* Scope chips on the left, search on the right. The chips take the slack
   so the button stays the same size whether there are one or four of
   them -- a target that moves as permissions change is a target people
   stop reaching for. */
.sched-row{display:flex;align-items:center;gap:8px}
.sched-row .seg{flex:1;min-width:0}
.sched-row .chan-search{flex:none}
/* The scope chips carried an 18px bottom margin from the base sheet, and
   because the row centers each item's MARGIN box that margin dropped the
   search button 9px below the chips. Move the spacing to the row so the
   chips and the search button share one center line. */
.sched-row{margin-bottom:18px}
.sched-row .seg{margin-bottom:0}
.grip{
  flex:none;width:18px;color:var(--txt-3);font-size:13px;line-height:1;
  letter-spacing:-3px;text-align:center;user-select:none;
}
.chan-row.dragging-list{transition:transform .16s ease}
.chan-row.drag-me{
  transition:none;z-index:5;
  box-shadow:0 8px 22px rgba(0,0,0,.16);
  border-color:var(--teal);
  /* stop the page scrolling under a drag that has already started */
  touch-action:none;
}
.chan-row.drag-me .grip{color:var(--teal)}
.chan-row.dragging-list .who.tap,
.chan-row.dragging-list .chev{pointer-events:none}

/* ---------- branches ---------- */
.br-row{align-items:center}
.br-rate{
  flex:none;min-width:56px;text-align:right;
  font-weight:800;font-size:15px;color:var(--teal-ink);
  font-variant-numeric:tabular-nums;
}
/* Unassigned is a gap in the ServiceTitan setup, not a branch. It reads as
   a caveat rather than a peer so nobody mistakes it for a fourth market. */
.br-unassigned{background:var(--field);border-style:dashed}
.br-unassigned .who b{color:var(--txt-2)}
.br-unassigned .br-rate{color:var(--txt-3)}
#brAsOf{margin:2px 0 10px}
#brGap{margin-top:10px;line-height:1.5}
#brSheetReps .item small{color:var(--txt-3)}

/* ---------- schedule ---------- */
/* Six tabs instead of five once the Admin one shows. A hair less type and
   padding is all it takes to keep "Scorecard" on one line at 375px. */
.tabs .tab{font-size:11px}
.tabs .tab svg{width:20px;height:20px}

/* The tab dot carries a count on this tab, so it needs to be a pill
   rather than a 7px circle. */
.tab .dot.count{
  width:auto;min-width:15px;height:15px;padding:0 4px;border-radius:999px;
  top:4px;right:calc(50% - 15px);color:#fff;font-size:9.5px;font-weight:800;
  font-style:normal;display:grid;place-items:center;line-height:1;
}

.sched-day{display:flex;align-items:baseline;justify-content:space-between}
.sched-day small{text-transform:none;letter-spacing:0;font-size:11px;font-weight:600}

.item.appt{gap:12px}
.appt-time{
  flex:none;width:74px;font-size:12px;font-weight:700;color:var(--txt-2);
  line-height:1.25;
}
.item.appt .pill{flex:none}

/* Status straight from ServiceTitan, so the words are theirs. Color only
   has to separate "still coming" from "no longer happening". */
.pill.st-sched{background:#6435d61f;color:var(--purple-ink)}
.pill.st-live{background:#0f9d631f;color:var(--ok)}
.pill.st-done{background:#7679761f;color:var(--gray)}
.pill.st-cancel{background:#d645451f;color:var(--bad)}

.ap-line{padding:9px 2px;border-bottom:1px solid var(--line)}
.ap-line:last-child{border-bottom:0}
.ap-line small{display:block;font-size:11px;color:var(--txt-3);margin-bottom:3px}
.ap-line b{font-size:14px;font-weight:600;line-height:1.4}

/* Call, text, directions -- side by side because they are peers, and
   because somebody reading this in a truck should not have to scroll to
   reach the one they want. Flex rather than three fixed columns so that
   an appointment with no phone on it shows one wide Directions button
   instead of one button and two empty holes. */
.ap-acts{display:flex;gap:8px;margin-top:12px}
.ap-acts .btn{
  /* min-width:0 because flex:1 cannot equalise while min-width defaults
     to auto: Directions and Messages kept their content width and the
     short labels took whatever was left, so the row read as four
     different buttons. 13.5px is what lets the two long ones sit inside
     78px without spilling. */
  flex:1;min-width:0;margin-top:0;padding:12px 6px;
  font-size:13.5px;text-align:center;text-decoration:none;
}
.ap-acts .btn.hidden{display:none}
.ap-acts:not(:has(.btn:not(.hidden))){display:none}

/* A line in the appointment sheet that opens the CRM. Coloured like a
   link and carrying an arrow, because a job number that happens to be
   tappable and a job number that is not look identical otherwise. */
/* Date and time inputs on iOS are native controls: they size themselves,
   ignore width:100% and centre their value, so on a real phone both fields
   ran past the right edge of the sheet. Turning the native appearance off
   puts them back under our own sizing. */
.fld input[type="date"],
.fld input[type="time"]{
  -webkit-appearance:none;appearance:none;
  min-width:0;max-width:100%;text-align:left;
}

/* .sheet-head is sticky, so it paints OVER what follows rather than
   pushing it down. Margin collapses through and changes nothing --
   padding does not. Without this the first label sits behind the header. */
#bookSheet .row2{padding-top:24px}
/* v305 raised the other sheets from 12px to 24px and missed this one,
   and the reopen warning was added in front of .row2 afterwards -- so
   when a completed job was booked onto, the FIRST line of "This job is
   marked complete" sat behind the header and read as half a sentence.
   Whichever element is first has to carry the padding, not just .row2. */
#bookSheet #bookReopen:not(.hidden){padding-top:24px}
#bookSheet #bookReopen:not(.hidden) + .row2{padding-top:12px}

.ap-line .ap-link{color:var(--teal);text-decoration:none;font-weight:600}
.ap-line .ap-link:active{opacity:.7}
.ap-line .ap-ext{font-size:12px;opacity:.7;margin-left:2px}

/* The maps chooser: one wide button per app, stacked, thumb-height.
   The last app used carries a small tag so the habitual choice is
   findable without reading. */
.maps-list{display:flex;flex-direction:column;gap:8px;margin-top:8px}
.maps-list .btn{
  margin-top:0;padding:14px 12px;text-align:center;text-decoration:none;
  position:relative;
}
.maps-list .btn.hidden{display:none}
.maps-list .btn.last::after{
  content:"Last used";position:absolute;right:12px;top:50%;
  transform:translateY(-50%);font-size:11px;color:var(--txt-3);font-weight:400;
}

/* ---------- schedule: day chips and the per-branch day ---------- */
#schedDays{margin-bottom:14px}
#schedDays .chip{position:relative}

.appt-src{color:var(--txt-3);opacity:.85}
.item.appt .who small{display:block;line-height:1.45}

/* The appointment note. Italic so it reads as somebody's words rather than
   another field, and clamped to one line -- these run to a paragraph and
   the row is already four lines deep on a phone. The whole note is on the
   appointment sheet. */
/* Two lines, not one. Clamped to a single line the note came out as
   "Customer is C\u2026" on a phone, which tells nobody anything -- the time
   column and the status pill leave the middle column narrow. Needs to beat
   `.item.appt .who small{display:block}`, hence the longer selector. */
.item.appt .who small.appt-note,
.appt-note{
  color: var(--txt-3);
  font-style: italic;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  max-width: 100%;
}

/* A queue row on the Projects tab stacks the same way an appointment row
   does -- name, where it is stuck, then the detail. Without the block the
   two small lines run together into one paragraph, which is what made the
   insurance queue unreadable on a phone. The pill keeps its width so a
   long stage name cannot squeeze it. */
.item.qrow .who small{display:block;line-height:1.45}
.item.qrow .pill{flex:none}

/* The money on a Projects-by-status row, and on the project rows inside
   an expanded one. Fixed width so a long status name wraps inside its own
   column instead of shoving the figure off the edge, and tabular figures
   so a column of dollar amounts lines up on the decimal rather than
   wandering. The open rows used to wrap the figure and the chevron in one
   span, so "$23,993" and the arrow sat welded together while every row
   above them had a gap -- two lists on one screen disagreeing again. */
.item.qrow .amt,
.br-day-open .item .amt{
  flex: none;
  font-size: 13.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* A row in one of these lists that is not tappable still has to line its
   money up with the rows that are: By branch opens, By trade and Insurance
   and retail do not, and without this the three blocks on the Production
   tab ended their numbers at three different right edges on one screen.
   A hidden chevron is exact where a guessed padding is not -- it is the
   same glyph at the same size, so the column cannot drift if the arrow
   ever changes. */
.item.qrow .chev.ghost{visibility:hidden}

/* An expanded branch. Inset and hairlined so the appointments read as
   belonging to the branch above them rather than as peers of it. */
.br-day-open{
  margin:-4px 0 10px 12px;padding-left:12px;
  border-left:2px solid var(--line);
  display:flex;flex-direction:column;gap:8px;
}
.br-day-open .item{border-radius:11px}

/* The count on a branch card. Reuses the branch-tab rate slot so the two
   screens line up visually. */
.br-row .br-rate{min-width:26px;text-align:right}

/* ---------- the KPI row nobody had styled ---------- */
/* .kpi-grid existed, .kpis never did — so the Branches company row and the
   appointment sheet were stacking four full-width cards instead of laying
   them out. Same two-up as the scorecard. */
.kpis{display:grid;grid-template-columns:1fr 1fr;gap:10px}
/* An odd number of tiles on a two-column grid leaves the last one sitting
beside an empty cell, which reads as a tile that failed to load rather
than as a layout. The straggler takes the full width instead -- the You
block now has five, because Appointments joined Sales, Closing rate,
Claims and Avg job, and five will never divide by two. */
@media (max-width:899px){
.kpi-grid > .kpi:last-child:nth-child(odd),
/* .kpis never got this rule, so the branch sheet's fifth tile (Open
   pipeline) sat in the left column with a hole beside it -- reading as a
   tile that failed to load. Excluded: kpis-3 and kpis-4 lay out three and
   four across on purpose, and a full-width straggler would break them. */
.kpis:not(.kpis-3):not(.kpis-4) > .kpi:last-child:nth-child(odd){grid-column:1 / -1}
}
/* A section with exactly two numbers in it keeps two columns on every
   screen, rather than being widened to four and leaving two empty
   cells -- which reads as missing data, not as a clean layout. */
.kpis.kpis-2{grid-template-columns:1fr 1fr}
/* The Marketing funnel, six tiles, three across even on a phone: Leads,
   Booked jobs, Booking rate on the top row; Opportunities, Demo rate,
   Close rate on the second. They are short numbers, and stacking one
   step of a funnel onto its own row breaks the left-to-right reading.
   Three columns wrapping to two rows also keeps each rate beside the
   two counts it came from, which four or six across would not. */
.kpis.kpis-3{grid-template-columns:repeat(3,minmax(0,1fr))}
/* Four across, and small enough that four actually fit. The lead-flow
   block runs to eleven tiles now that Signed and Cost per sale joined
   it, and eleven at three across is four rows deep -- enough to push
   the campaign list off the bottom of every phone before you have read
   a single campaign name. Everything here is the standard tile with the
   air taken out of it: nothing is hidden, the numbers are just smaller.
   Full size comes back on desktop, where there is room for it. */
.kpis.kpis-4{grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}
.kpis-4 .kpi{padding:9px 8px;border-radius:12px}
.kpis-4 .kpi .lbl{font-size:8.5px;letter-spacing:.02em;margin-bottom:4px;line-height:1.25}
.kpis-4 .kpi .val{font-size:16px;letter-spacing:-.4px}
.kpis-4 .kpi .sub{font-size:9px;margin-top:2px;line-height:1.3}

/* =====================================================================
   DESKTOP
   The same app, laid out for a screen you are not holding. Nothing here
   changes behavior or markup: the phone layout is the base and this is
   one media query on top, so there is one app to maintain, not two.

   900px is the breakpoint because that is roughly where a bottom tab bar
   stops making sense — your thumb is nowhere near the bottom of a laptop
   screen, and the eye goes to the left edge instead.
   ===================================================================== */
/* ---- composer: stop paying for the home indicator twice ----
   The base sheet gives .composer padding-bottom: calc(10px + var(--safe-b))
   AND .tabs padding-bottom: var(--safe-b). On a phone the tab bar sits
   BELOW the composer, so it already clears the home indicator -- and the
   composer's own copy of the inset became a 34px band of dead background
   between the send button and the tabs on every iPhone. The element that
   touches the bottom of the screen is the one that should pay. */
.composer{padding-bottom:10px}

@media (min-width: 900px) {

  /* Nav down the left, topbar and content to the right of it.
     The base stylesheet pins .app to a centered 560px "phone frame" from
     760px up. That is the right call when the layout is a phone layout;
     it is exactly wrong once there is a real desktop one, so it is undone
     here rather than edited there — styles.css stays the phone base. */
  .app{
    max-width:none;margin:0;border-left:0;border-right:0;
    display:grid;
    grid-template-columns:236px minmax(0,1fr);
    grid-template-rows:auto minmax(0,1fr);
    grid-template-areas:"nav top" "nav main";
    padding-top:0;
  }
  .topbar{grid-area:top;padding:15px 26px}
  .topbar strong{font-size:16px}

  /* The tab bar becomes a sidebar. Same buttons, same JS, read top to
     bottom instead of left to right. */
  .tabs{
    grid-area:nav;flex-direction:column;align-items:stretch;gap:2px;
    border-top:0;border-right:1px solid var(--line);
    padding:14px 10px;padding-bottom:14px;overflow-y:auto;
  }
  .tabs .tab{
    flex:none;flex-direction:row;align-items:center;justify-content:flex-start;
    gap:12px;padding:10px 12px;border-radius:11px;font-size:13.5px;
  }
  .tabs .tab svg{width:19px;height:19px;flex:none}
  .tabs .tab.active{background:var(--card-2)}
  /* The unread markers were positioned against a centered icon; on a row
     they belong at the far end. */
  .tabs .tab .dot,
  .tabs .tab .dot.count{position:static;margin-left:auto;top:auto;right:auto}

  /* Every view shares the one content cell. Only one is ever visible. */
  .view{grid-area:main;min-height:0}

  /* Reading measure: a KPI table stretched across 27 inches is unreadable. */
  .scroll{padding:24px 28px 34px;max-width:1240px;margin-inline:auto;width:100%}
  /* Four fixed columns put the fifth tile on a row of its own with three
empty cells beside it. auto-fit collapses the tracks nothing lands in,
so four tiles make four columns and five make five, and neither ends
up with a hole in it. The floor is 140px rather than 160 so that a
900px window -- sidebar plus content -- still gets four across
instead of dropping to three. */
.kpi-grid{grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}
.kpis{grid-template-columns:repeat(4,minmax(0,1fr))}
  .kpis.kpis-2{grid-template-columns:1fr 1fr}
  .kpis.kpis-3{grid-template-columns:repeat(3,minmax(0,1fr))}
  /* The squeeze is a phone measure, not a house style. On a screen with
     room, the marketing tiles and the campaign figures are the same size
     as every other tile in the app. */
  .kpis.kpis-4{grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
  .kpis-4 .kpi{padding:14px 15px;border-radius:15px}
  .kpis-4 .kpi .lbl{font-size:11px;letter-spacing:.8px;margin-bottom:7px}
  .kpis-4 .kpi .val{font-size:23px;letter-spacing:-.7px}
  .kpis-4 .kpi .sub{font-size:11.5px;margin-top:3px}
  .seg{max-width:520px}

  /* ---- chat: channels beside the conversation ---- */
  #view-chat{
    display:grid;
    grid-template-columns:264px minmax(0,1fr);
    grid-template-rows:minmax(0,1fr) auto auto auto;
    grid-template-areas:
      "chans msgs"
      "chans mention"
      "chans reply"
      "chans composer";
  }
  /* The row wrapping the strip is what sits in the grid now. It becomes
     the column, the strip scrolls inside it, and the search button pins
     to the bottom where a thumb-free desktop hand expects a utility. */
  /* An id, so this beats the phone block further down the sheet. That
block is later in the file and sets the row direction, and until this
selector outweighed it the desktop sidebar was laying its channels out
sideways with the search button stranded in the middle. */
#view-chat .chan-row{
grid-area:chans;flex-direction:column;align-items:stretch;gap:0;
    min-height:0;border-right:1px solid var(--line);border-bottom:0;
  }
  #chanStrip{
    flex:1;min-height:0;flex-direction:column;align-items:stretch;gap:3px;
    overflow-x:hidden;overflow-y:auto;
    border-bottom:0;border-right:0;padding:14px 12px;
  }
  /* On a laptop the sidebar is tall and search belongs at its foot, as a
     utility rather than a field competing with the channel list. The
     phone rules above give it a field's border and fill, so those are
     handed back here. */
  #view-chat .chan-row::after{display:none}
.chan-row .chan-search{
display:flex;align-items:center;
width:auto;height:auto;margin:0;padding:11px 12px;gap:8px;
    justify-content:flex-start;
    background:transparent;color:var(--txt-2);
    border:0;border-top:1px solid var(--line);border-radius:0;
    font:600 13px inherit;
  }
  .chan-row .chan-search:hover{background:var(--card-2);color:var(--txt)}
  .chan-row .chan-search::after{content:"Search"}
  .chan-row .chan-strip{padding-bottom:14px}
  /* A pill is a horizontal-strip shape. In a vertical list it wants to be
     a row that fills the width. */
  #chanStrip .chip{
    width:100%;justify-content:flex-start;text-align:left;white-space:normal;
    border-radius:10px;border-color:transparent;background:transparent;
    padding:9px 12px;
  }
  #chanStrip .chip:hover{background:var(--card-2)}
  #chanStrip .chip.active{background:var(--card-2);border-color:var(--line);color:var(--txt)}
  #chanStrip .chip.active .chip-badge{background:var(--purple);color:#fff}
  #chanStrip .chip-badge{margin-left:auto}

  #msgs{grid-area:msgs;padding:18px 26px 8px}
  #mentionBar{grid-area:mention}
  #replyBar{grid-area:reply}
  /* Here the nav is a rail down the left, so nothing sits below the
     composer and it clears the home indicator itself -- an iPad in
     landscape gets this layout and still has one. This is the rule
     that decides it: an id beats the .composer rules either sheet
     sets, so putting the inset on a class here would never fire. */
  #composer{grid-area:composer;padding:14px 26px;
    padding-bottom:calc(14px + var(--safe-b))}
  #composer textarea{max-height:180px}
  /* .comp-field #msgInput is a class plus an id, so the line above alone
     would not win it back on a laptop. */
  .comp-field #msgInput{max-height:180px}

  /* ---- sheets become dialogs ---- */
  /* Sliding up from the bottom edge is a phone gesture. On a desktop the
     same markup reads better as a centered panel. */
  .sheet-wrap{align-items:center;justify-content:center;padding:28px}
  .sheet{
    width:min(560px,100%);max-height:82vh;overflow-y:auto;
    border-radius:18px;border-top:0;padding:24px;padding-bottom:24px;
    box-shadow:0 24px 60px #0f213a3d;animation:pop .16s ease;
  }
  .scroll-sheet{max-height:42vh}

  /* ---- schedule reads wider ---- */
  .appt-time{width:96px;font-size:12.5px}
  .br-day-open{margin-left:16px;padding-left:16px}
}

@keyframes pop{from{opacity:0;transform:scale(.98)}}

/* ---------- native shell ---------- */
/* The lock screen the iOS app shows before Face ID. Deliberately opaque:
   the point is that the day's customer list is not readable over someone's
   shoulder while the prompt is up. */
#nativeLock{
  position:fixed;inset:0;z-index:200;background:var(--bg);
  display:grid;place-items:center;padding:28px;
}
#nativeLock[hidden]{display:none}
#nativeLock .lock-card{text-align:center;max-width:300px;width:100%}
#nativeLock .lock-card b{display:block;margin:16px 0 18px;font-size:16px}

/* An offline banner rather than a screen that silently stops working —
   a crew in a basement should be told, not left guessing. */
html.is-offline .topbar::after{
  content:"Offline — showing the last update";
  position:absolute;left:0;right:0;top:100%;
  background:#b8860b;color:#fff;font-size:11.5px;font-weight:700;
  text-align:center;padding:4px 8px;z-index:5;
}
.topbar{position:relative}

/* The web app's "add to home screen" and browser-push prompts have no
   meaning inside the app, which already is on the home screen. */
html.is-native .pwa-only{display:none !important}

/* ---------- photos in the chat ---------- */
.attach{
  flex:none;width:38px;height:38px;border-radius:50%;border:1px solid var(--line);
  background:var(--card);color:var(--txt-2);display:grid;place-items:center;
  cursor:pointer;transition:.15s;
}
.attach:hover{background:var(--card-2);color:var(--txt)}
.attach:disabled{opacity:.5;cursor:default}

/* The strip above the composer while a photo is waiting to be sent. It is
   uploaded the moment it is picked, so sending stays instant and a photo
   with no caption is a perfectly good message. */
.pending-photo{
  flex:none;display:flex;align-items:center;gap:11px;padding:9px 12px;
  border-top:1px solid var(--line);background:var(--card-2);
}
.pending-photo img{width:42px;height:42px;border-radius:9px;object-fit:cover;flex:none}
.pending-photo-txt{flex:1;min-width:0;font-size:12.5px;line-height:1.3}
.pending-photo-txt b{display:block;font-size:11.5px;font-weight:800;color:var(--teal)}
.pending-photo-txt span{color:var(--txt-3)}

/* A definite width, not a percentage. The bubble is inline-block, so it
   sizes to its content -- and a width:100% child inside a shrink-to-fit
   parent is circular: the browser resolves it against the image's own
   intrinsic size, which is 0 until the file arrives and something else
   once it has. That is what made the photo grow and shrink. */
.msg-photo{
  display:block;width:260px;max-width:100%;height:auto;border-radius:12px;
  background:var(--card-2);cursor:zoom-in;
}
/* A photo message IS the photo, not a frame around one.
   The bubble behind a job photo was pure weight: a picture already reads
   as a message without a slab under it, and on your own messages the
   teal gradient wrapped every photo in a coloured border with the
   caption printed white inside it. Job photos are most of what gets
   posted here, so that frame set the tone of the whole thread.
   Now: rounded photo, caption underneath as ordinary text.

   Width comes from the PHOTO rather than from shrink-to-fit, or a
   caption longer than the image stretches the row and strands the
   picture at one end of it. */
.bubble.has-photo{
  width:260px;max-width:100%;padding:0;overflow:visible;
  background:none;border:0;box-shadow:none;border-radius:0;
}
/* .msg.mine .bubble is three classes and would otherwise keep the
   gradient, the white text and the tail radius. */
.msg.mine .bubble.has-photo{
  background:none;border-color:transparent;color:var(--txt);
  border-radius:0;text-align:right;
}
.bubble.has-photo .msg-photo + *{
  display:block;padding:5px 2px 0;color:var(--txt);font-size:13.5px;
}
.bubble.has-photo .reply-quote{margin:0 0 6px}
/* An @mention still has to announce itself, and its bar has nothing to
   sit against once the frame is gone. */
.msg.calls-me .bubble.has-photo{
  border-left:3px solid var(--purple);padding-left:8px;
}

.photo-view{
  position:fixed;inset:0;z-index:120;background:#0f213aee;
  display:grid;place-items:center;padding:20px;
}
.photo-view img{max-width:100%;max-height:100%;border-radius:10px}
.photo-view-x{
  position:absolute;top:14px;right:16px;width:40px;height:40px;border-radius:50%;
  border:0;background:#ffffff26;color:#fff;font-size:26px;line-height:1;cursor:pointer;
}

@media (min-width:900px){
  .msg-photo{width:340px}
  /* the bubble is sized from the photo, so it moves with it */
  .bubble.has-photo{width:340px}
  .pending-photo{padding:10px 26px}
}

/* ---------- admin as a menu ---------- */
.admin-back{
  border:0;background:none;color:var(--teal-ink);font:inherit;font-size:13.5px;
  font-weight:700;padding:2px 2px 12px;cursor:pointer;
}
.admin-back:hover{text-decoration:underline}

/* ---------- appearance: color pickers ---------- */
.swatches{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0 14px}
.swatch{
  width:52px;height:34px;border-radius:9px;border:1px solid var(--line);
  padding:0;overflow:hidden;display:flex;cursor:pointer;background:none;
}
.swatch i{flex:1;display:block}
.swatch:hover{border-color:var(--txt-3)}

.color-row{display:flex;gap:10px}
.color-pick{
  flex:1;display:flex;align-items:center;justify-content:space-between;gap:10px;
  border:1px solid var(--line);border-radius:11px;padding:9px 11px;background:var(--field);
  font-size:12.5px;font-weight:600;color:var(--txt-2);cursor:pointer;
}
/* A native color input is a gray box with a swatch in it on most
   platforms; stripped back to just the swatch it reads as a color. */
.color-pick input[type=color]{
  width:38px;height:28px;padding:0;border:1px solid var(--line);border-radius:7px;
  background:none;cursor:pointer;
}
.color-pick input[type=color]::-webkit-color-swatch-wrapper{padding:2px}
.color-pick input[type=color]::-webkit-color-swatch{border:0;border-radius:5px}

/* ---------- capability checkboxes ---------- */
.caps{display:flex;flex-direction:column;gap:2px;margin:4px 0 14px}
.cap{
  display:flex;gap:11px;align-items:flex-start;padding:9px 10px;border-radius:10px;
  cursor:pointer;
}
.cap:hover{background:var(--card-2)}
.cap input{margin:2px 0 0;width:17px;height:17px;flex:none;accent-color:var(--teal)}
.cap span{min-width:0}
.cap b{display:block;font-size:13.5px;font-weight:600}
.cap small{display:block;font-size:11.5px;color:var(--txt-3);line-height:1.4}
/* Greyed rather than hidden: knowing a permission exists, and that you
   cannot pass it on, is more useful than it silently not being there. */
.cap-off{opacity:.5;cursor:not-allowed}
.cap-off:hover{background:none}

.lvl-edit{
  margin:-4px 0 10px 12px;padding:8px 12px 4px;
  border-left:2px solid var(--line);
}

/* ---------- Management: branch setup, and the two-column field row ----------
   .btn.tiny is the row-level action: small enough that two fit beside a
   name on a phone without wrapping, big enough to still be a 32px tap
   target. A switched-off branch stays legible rather than going ghost --
   it is a row you still have to be able to read to switch back on. */
.br-edit{align-items:center;gap:8px}
.br-edit .who{flex:1;min-width:0}
.br-edit.off .who b{opacity:.55}
.br-edit.off .who small{color:var(--txt-3)}
.btn.tiny{
  flex:0 0 auto;padding:7px 11px;font-size:12px;font-weight:600;
  border-radius:9px;min-height:32px;line-height:1;
  background:var(--field);color:var(--txt);border:1px solid var(--line);cursor:pointer;
}
.btn.tiny:hover{filter:brightness(.95)}
.btn.tiny:active{transform:translateY(1px)}

.two-up{display:grid;grid-template-columns:1fr 1fr;gap:10px}
@media (max-width:400px){.two-up{grid-template-columns:1fr}}

/* The leaderboard scope sits above the metric chips and needs the same
   breathing room the period segment has on the Scorecard. */
#boardScope{margin-bottom:10px}

/* ---------- attachments, voice notes and search ---------- */

/* A file is a link, not a card: one tap opens it, and the whole row is
   the target because a thumb on a ladder is not precise. */
.msg-file{
  display:flex;align-items:center;gap:10px;text-decoration:none;
  padding:9px 11px;margin:2px 0 4px;border-radius:11px;
  background:rgba(0,0,0,.05);color:inherit;max-width:280px;
}
.msg.mine .msg-file{background:rgba(255,255,255,.16)}
.msg-file:hover{filter:brightness(.97)}
.ff-ico{
  flex:none;display:grid;place-items:center;width:38px;height:38px;
  border-radius:9px;background:var(--teal);color:#04191b;
  font-size:9.5px;font-weight:800;letter-spacing:.02em;
}
.ff-txt{min-width:0;display:flex;flex-direction:column;gap:1px}
.ff-txt b{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ff-txt small{font-size:11px;opacity:.7}

/* The native audio element is ugly but it is also the only control that
   works identically in Safari, Chrome and a WKWebView, and it comes with
   a scrubber people already know. Sized rather than restyled. */
.msg-voice{display:flex;align-items:center;gap:8px;margin:2px 0 4px}
.msg-voice audio{height:36px;max-width:230px}
.vn-len{font-size:11px;opacity:.7;font-variant-numeric:tabular-nums}

.pending-ico{
  flex:none;display:grid;place-items:center;width:38px;height:38px;
  border-radius:9px;background:var(--teal);color:#04191b;
  font-size:10px;font-weight:800;
}
.pending-photo-txt b{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}

/* Recording. The dot is the whole affordance -- a red pulse says
   "live" faster than any label. */
.rec-bar{
  display:flex;align-items:center;gap:9px;padding:9px 12px;
  border-top:1px solid var(--line);font-size:13px;
}
.rec-bar b{font-weight:600}
.rec-bar #recLen{font-variant-numeric:tabular-nums;opacity:.75}
.rec-hint{margin-left:auto;font-size:11.5px;opacity:.6}
.rec-dot{
  width:9px;height:9px;border-radius:50%;background:var(--bad);
  animation:recpulse 1.1s ease-in-out infinite;flex:none;
}
@keyframes recpulse{0%,100%{opacity:1}50%{opacity:.25}}
.attach.mic.recording{background:var(--bad);color:#fff}

/* ---- one composer, not four shapes ----
   Attach, mic, field and send used to be four separate objects in a row:
   two bordered circles, a pill and a filled circle. Attach and mic now
   live inside the field, which leaves two shapes -- the thing you type
   in, and the one button you cannot undo. */
.comp-field{
  flex:1;min-width:0;display:flex;align-items:flex-end;gap:4px;
  padding:3px 4px 3px 5px;
  border:1px solid var(--line);border-radius:20px;background:var(--field);
  transition:.15s;
}
.comp-field:focus-within{border-color:var(--teal)}
/* Inside the field these are glyphs, not buttons with their own edges. */
.comp-field .attach{
  width:32px;height:32px;border:0;background:none;
}
.comp-field .attach svg{width:19px;height:19px}
.comp-field .attach:hover{background:var(--card-2)}
.comp-field .attach.mic.recording{background:var(--bad)}
/* #msgInput is an id, so .composer textarea alone would not win. */
.comp-field #msgInput{
  flex:1;min-width:0;border:0;background:none;border-radius:0;
  padding:8px 2px;max-height:120px;
}
.comp-field #msgInput:focus{border:0}
@media (prefers-reduced-motion:reduce){.rec-dot{animation:none}}

/* ---- channels, then search ----
   Search used to be a 34px unbordered square pinned to the right of the
   channel strip, with the chips scrolling underneath and getting sliced
   in half by it. It did not read as a button; it read as a smudge.

   Now the row stacks: chips on top (they are what you switch between all
   day, so they keep the top of the screen), a real field beneath. Both
   sit on one white block with a single edge at the bottom, so the whole
   thing reads as chrome rather than as the first message in the thread.
   Still ONE element, so the desktop sidebar below can keep pinning it to
   the bottom of the column without a second copy in the markup. */
/* ---- channels, then search ----
Search is a BUTTON beside the chips again, which is where it started
and where it belongs: channels are what you switch between all day and
they keep the top of the screen, and search costs no row of its own.

What was wrong before was never the position. It was a 34px unbordered
square with the strip scrolling UNDER it, so a chip got sliced in half
by an edge that had no edge of its own and the control read as a
smudge. Now it has a border, it sits outside the scrolling strip
rather than on top of it, and the strip fades out before it. */
.chan-row{
position:relative;
display:flex;flex-direction:row;align-items:center;gap:0;
background:var(--bg-2);border-bottom:1px solid var(--line);
}
.chan-row .chan-strip{flex:1;min-width:0;border-bottom:0}
/* The fade is the whole trick: a chip that runs out of room dissolves
instead of being cut off square against the button. */
.chan-row::after{
content:"";position:absolute;top:1px;bottom:1px;right:50px;width:24px;
pointer-events:none;
background:linear-gradient(90deg,#fff0,var(--bg-2));
}
.chan-search{
flex:none;width:36px;height:36px;margin:0 10px 0 4px;padding:0;
display:grid;place-items:center;
border:1px solid var(--line);border-radius:11px;
background:var(--card);color:var(--txt-2);
font:inherit;cursor:pointer;transition:.15s;
}
.chan-search svg{flex:none}
.chan-search:hover{border-color:var(--teal);color:var(--teal-ink)}
.chan-search:active{transform:scale(.94)}

/* A follow-up row is a schedule row wearing different content: the date
   where the time goes, how long it has been sitting where the status
   pill goes. Keeping the shape is the point -- it belongs to the list
   above it, it is not a second widget. */
.item.fu .appt-time{font-size:12px;font-weight:800;line-height:1.3}
.pill.fu-age{background:var(--field);color:var(--txt-2);font-weight:700}
.fu-quote{display:block;margin-top:2px;font-size:12px;font-weight:700;color:var(--teal-ink)}
.fu-quote.fu-declined{color:var(--txt-3);font-weight:600}
.sec.fu-sec{color:var(--purple-ink)}

/* Booked and Signed $ both have rows behind them, so both read as tappable.
   Leads are daily counts with nothing to open. Signed $ carries its own class
   because the script wires every .drill in that table to the booked sheet. */
.kpi.drill{cursor:pointer}
.kpi.drill .val::after{content:" \203A";color:var(--teal-ink);font-weight:400}
.camp-figs .drill b,
.camp-figs .drill-sold b{color:var(--teal-ink);text-decoration:underline dotted;text-underline-offset:3px;cursor:pointer}
.jobs-list{max-height:60vh;overflow-y:auto;-webkit-overflow-scrolling:touch;margin:6px 0}
.jobrow .btn{padding:6px 10px;font-size:12px;margin-left:6px;white-space:nowrap}
#jobsSheetNote{padding:6px 2px 0}

/* ---------- a tile you can open ----------
   Four of the six Scorecard tiles hold a list of jobs behind the number.
   The affordance is deliberately quiet: a chevron in the corner and a
   pressed state, not a button. Six outlined boxes in a six-tile grid
   would read as a form, and the two tiles that do NOT open would then
   look broken rather than simply different. */
.kpi-tap{cursor:pointer;position:relative;-webkit-tap-highlight-color:transparent}
.kpi-tap::after{content:"\203a";position:absolute;top:8px;right:11px;
  font-size:16px;line-height:1;color:var(--txt-3);opacity:.5}
.kpi-tap:active{opacity:.78;transform:translateY(1px)}
@media (hover:hover){.kpi-tap:hover{border-color:var(--teal-ink)}}

/* The money sits right, away from the customer name, so a column of
   amounts can be scanned without reading any of the rows. */
/* Two facts about a job, one per line. <small> is inline by default, so
   without this the job number runs straight into the rep's name -- which
   is how "Job J-4 · Completed" and "Landyn Marcinick" ended up sharing a
   line on the first build. Applies to the Marketing sheet too, which had
   the same collision on shorter strings. */
.jobrow .who small{display:block}
/* Same collision on the Finance sheets, which use .item rather than
   .jobrow: "no sold date on it" ran straight into "Columbus · Scheduled".
   Inline <small> is the default and it is never what these rows want. */
#arSheetList .who small{display:block}

.drill-amt{margin-left:auto;padding-left:10px;font-weight:700;
  font-variant-numeric:tabular-nums;white-space:nowrap}
/* On the Demo rate sheet the right-hand column is a verdict, not money.
   The gap is what matters, so "no quote" takes the eye and "quoted"
   recedes -- the opposite of ranking by amount. */
.drill-amt.no-quote{color:var(--bad);font-size:12px}
.drill-amt.quoted{color:var(--txt-3);font-weight:600;font-size:12px}

/* Work in progress is money that has not landed anywhere yet -- not
   revenue, not a receivable. It gets the same quiet chevron the Scorecard
   tiles use rather than a colour of its own, because the point is that it
   opens, not that it shouts. */
.kpi.wip-tile{cursor:pointer;position:relative;-webkit-tap-highlight-color:transparent}
.kpi.wip-tile::after{content:"\203a";position:absolute;top:8px;right:11px;
  font-size:16px;line-height:1;color:var(--txt-3);opacity:.5}
.kpi.wip-tile:active{opacity:.78;transform:translateY(1px)}
@media (hover:hover){.kpi.wip-tile:hover{border-color:var(--teal-ink)}}


.search-view{
  position:fixed;inset:0;z-index:60;background:var(--bg);
  display:flex;flex-direction:column;
}
.search-head{
  display:flex;align-items:center;gap:8px;padding:12px 14px;
  border-bottom:1px solid var(--line);
}
.search-head input{
  flex:1;min-width:0;font:inherit;font-size:15px;padding:11px 13px;
  border:1px solid var(--line);border-radius:11px;
  background:var(--field);color:var(--txt);
}
/* Under the search head, not inside it: three controls on one line on a
   390px phone leaves the text box too narrow to read what you typed. */
.ap-dates{
  display:flex;gap:8px;padding:10px 14px;
  border-bottom:1px solid var(--line);
}
.ap-dates label{
  flex:1;min-width:0;display:flex;align-items:center;gap:6px;
  font-size:11px;font-weight:700;letter-spacing:.04em;
  text-transform:uppercase;color:var(--txt-2);
}
.ap-dates input{
  flex:1;min-width:0;font:inherit;font-size:14px;padding:8px 10px;
  border:1px solid var(--line);border-radius:10px;
  background:var(--field);color:var(--txt);
}
.search-results{flex:1;overflow-y:auto;padding:8px 12px 24px;-webkit-overflow-scrolling:touch}
.sr .who{min-width:0}
.sr-where{
  margin-left:8px;font-size:11px;font-weight:600;opacity:.6;
  text-transform:none;letter-spacing:0;
}
.sr-snip{display:block;margin-top:2px;font-size:13px;color:var(--txt-2);line-height:1.45}
/* .item .who b is the name line and is a block, which made the highlight
   in a snippet stretch the full row. Inside a snippet it is a mark, not a
   line -- and it has to match that selector's specificity to say so, which
   a bare .sr-snip b does not. */
.item .sr-snip b{
  display:inline;color:var(--txt);background:rgba(255,214,0,.28);
  border-radius:3px;padding:0 2px;font-weight:700;
}
.sr-when{display:block;margin-top:3px;font-size:11px;opacity:.55}

/* The message you jumped to, and the way back to the bottom. */
.bubble.hit{
  outline:2px solid var(--teal);outline-offset:2px;
  animation:hitfade 2.6s ease-out forwards;
}
@keyframes hitfade{0%,70%{outline-color:var(--teal)}100%{outline-color:transparent}}
.jump-back{
  display:block;margin:14px auto 4px;padding:9px 16px;border:0;cursor:pointer;
  border-radius:999px;background:var(--teal);color:#04191b;
  font:600 13px inherit;
}
@media (prefers-reduced-motion:reduce){.bubble.hit{animation:none}}

/* ---------- zone mapping on the branches page ---------- */
.zone-pick{
  margin:-4px 0 10px;padding:10px 12px 12px;
  border:1px solid var(--line);border-top:0;
  border-radius:0 0 12px 12px;background:var(--card-2);
}
/* A zone another branch already owns stays visible and readable rather
   than vanishing: "Cleveland has it" is the answer to the question you
   opened this panel to ask. */
.zone-pick .cap.locked{opacity:.55}
.zone-pick .cap.locked input{cursor:not-allowed}
.warn-txt{color:var(--bad);font-weight:600}
#brAddZone{
  font:inherit;font-size:14px;padding:11px 12px;width:100%;
  border:1px solid var(--line);border-radius:11px;
  background:var(--field);color:var(--txt);
}

/* ---------- five-window period strips ----------
   A segmented control with five labels does not fit a phone. Scrolling
   sideways keeps every option full size and hittable, which matters more
   than seeing all five at once -- and the active one is scrolled into
   view on paint so you never land on a strip that looks empty. */
.seg.scroll-seg{
  overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;
  scrollbar-width:none;justify-content:flex-start;
}
.seg.scroll-seg::-webkit-scrollbar{display:none}

/* flex:1 1 0 makes the five share the row equally, so the strip ends
   where the last option ends instead of leaving 150px of dead space
   past "All time" while "Today" sits against the left edge -- measured,
   not guessed. min-width:max-content is what keeps that honest: a label
   is never squashed, so on a phone where five equal columns will not fit
   they overflow their natural width and the strip scrolls instead. */
.seg.scroll-seg .seg-btn{
  flex:1 1 0;
  min-width:max-content;
  white-space:nowrap;
  padding-left:14px;padding-right:14px;
}

/* ---------- lead flow ---------- */
#mkSources .item{align-items:center;gap:10px}
#mkSources .who{flex:1;min-width:0}
#mkSources .metric{font-weight:700;font-variant-numeric:tabular-nums}

/* ---- campaign rows ----
   Name, ServiceTitan's own category under it, then the mix that makes up
   the lead count and where it ended up. The rate sits under the lead
   count on the right so the eye can run down one column and compare. */
/* Its own class rather than a name from styles.css, so this section does
   not inherit a heading style that gets retuned for something else. */
.mg-sub{
  display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  font-size:11px;font-weight:800;letter-spacing:.09em;text-transform:uppercase;
  color:var(--txt-3);margin:16px 0 8px;
}
/* Window totals on the right of the heading. Not uppercase and not
   letter-spaced: it is a figure, not a label. */
.mg-sub-r{
  font-size:11.5px;font-weight:700;letter-spacing:0;text-transform:none;
  color:var(--txt-2);font-variant-numeric:tabular-nums;
}
/* Campaign ranking: the best row is first and says so; the select under
   the heading re-ranks by one figure. */
.mk-sort{display:flex;justify-content:flex-end;margin:-2px 0 8px}
.mk-sort label{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:700;color:var(--txt-2)}
.mk-sort select{
  font:inherit;font-weight:700;color:var(--txt);background:var(--field);
  border:1px solid var(--line);border-radius:8px;padding:4px 8px;
}
#mkSources .camp .who b .pill.camp-top{
  display:inline-block;margin-left:8px;vertical-align:middle;
  background:var(--teal-ink);color:#fff;font-size:9.5px;line-height:1;padding:4px 7px;
}
#mkSources .camp.top{border-color:var(--teal);box-shadow:inset 0 0 0 1px var(--teal)}
/* One campaign, the top of the funnel in miniature. The tiles above
give the window's Leads, Booked jobs and Booking rate; these are the
same three numbers for this campaign alone -- labelled, because
"12 · 4 · 33%" is a puzzle rather than a reading. Sold only appears
when there is money against the row: a column of $0 reads as a
finding when it is really an absence.

The share-of-leads bar that used to sit here is gone. It was drawn
against the biggest campaign and never said so, so it read as
decoration -- and the lead count it was ranking is right there on
the same row. */
#mkSources .item.camp{align-items:stretch;padding-top:12px;padding-bottom:12px}
#mkSources .camp .who{flex:1;min-width:0}
#mkSources .camp .who b{display:block;line-height:1.3}
#mkSources .camp-cat{
display:block;font-size:11px;font-weight:600;color:var(--txt-3);
margin-top:1px;letter-spacing:.01em;
}
/* Sized so the four-figure row -- the one that also carries Sold --
still fits "Booking rate" without clipping it at 390px. Measured,
not guessed. */
.camp-figs{
/* 92px, not 66. The row carries NINE figures now that cost per booked
job joined it, and at 66px a 390px phone fits four across -- which
lays nine out as 4 + 4 + 1, with the last figure stranded alone on a
row of its own. 92px forces three columns at that width, so nine
figures read as a tidy 3 x 3 block, and the wider track also stops
"Booking rate" wrapping. On a desktop auto-fit still collapses to
nine across. */
/* Four fixed columns rather than auto-fit at a floor width: eleven
   figures across three columns was four rows per campaign card, and
   with seventy-eight campaigns on the board that is a very long tab.
   Four across makes it three rows, and the type below is sized so the
   longest label -- Booking rate -- still fits a 320px phone without
   clipping. Desktop goes back to auto-fit and lays them all out. */
display:grid;grid-template-columns:repeat(4,minmax(0,1fr));
gap:6px;margin:9px 0 6px;
}
.camp-figs span{display:flex;flex-direction:column;gap:2px;min-width:0}
.camp-figs i{
font-style:normal;font-size:8.5px;font-weight:700;letter-spacing:.02em;
text-transform:uppercase;color:var(--txt-3);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.camp-figs b{
font-size:14px;font-weight:700;line-height:1.1;
font-variant-numeric:tabular-nums;
}
.camp-figs .rate b{color:var(--purple-ink)}
.camp-figs .money b{color:var(--teal-ink)}
/* A dash is an absence, not a value, so it never takes an accent. */
.camp-figs .none b{color:var(--txt-3);font-weight:600}

/* Desktop puts the figures back to full size and lets them lay out
   across the width. This lives here rather than up in the DESKTOP block
   because the base .camp-figs rules above come later in the file, and a
   media query does not beat source order on equal specificity -- put it
   up there and it silently loses. */
@media (min-width:900px){
  .camp-figs{grid-template-columns:repeat(auto-fit,minmax(92px,1fr));gap:7px}
  .camp-figs i{font-size:9px;letter-spacing:.04em}
  .camp-figs b{font-size:17px}
}

/* The smallest phones still in the field -- an SE at 320px -- lose the
   last few pixels to the fourth column, and "$15,860" was clipping
   rather than shrinking. One step down for those only. */
@media (max-width:349px){
  .kpis-4 .kpi{padding:8px 6px}
  .kpis-4 .kpi .lbl{font-size:8px;letter-spacing:0}
  .kpis-4 .kpi .val{font-size:14px}
  .camp-figs i{font-size:8px;letter-spacing:0}
  .camp-figs b{font-size:13px}
}

/* The Custom chip carries its own dates once chosen, so it gets more
   room than the fixed windows. */
#brCustomBtn,#scCustomBtn{min-width:max-content}
#rangeSheet .two-up{margin-bottom:12px}
#rangeFrom,#rangeTo{
  font:inherit;font-size:15px;padding:11px 12px;width:100%;
  border:1px solid var(--line);border-radius:11px;
  background:var(--field);color:var(--txt);
}

/* ---- tab bar, six across ----
   Admin moved into the account sheet, so the bar carries six on a phone
   instead of seven and the labels can breathe again: bigger type and
   bigger icons than before Marketing was added, not smaller.

   flex-basis stays auto. Six EQUAL columns give 61px of text room at
   390px and "Management" wants 62.3px even at 9px type, so equal widths
   clip it on the narrowest phones no matter how small the font gets.
   Sizing each tab to its own label and sharing the slack afterwards puts
   all six labels at about 323px of a 390px bar -- everything fits, at a
   readable size. Slightly uneven, which is the cheaper compromise: an
   uneven bar still reads, a clipped word does not.

   min-width:0 stays as the backstop. A flex item defaults to min-width
   auto, so without it a label that will not fit widens the whole row
   past the screen rather than shrinking. */
.tabs .tab span{min-width:0}
.tabs .tab span{max-width:100%;white-space:nowrap;overflow:hidden}

/* Six EQUAL columns. Sizing each tab to its own label shared the slack
out unevenly, which reads as sloppy spacing: wide gaps under the short
words and none under the long ones. "Management" was the one label
that would not fit an equal column on a 390px phone, so it is
"Manage" in the markup now and all six share the row exactly. */
/* Even GAPS, not even columns. Six equal columns are tidy in the markup
and wrong on the screen: every label is centred in its own column, so a
short word like Chat leaves half a column of air on each side while
Scorecard nearly fills its own. Measured on the live bar, the space
between labels came out 40px, 26px, 30px, 35px -- and the widest of
them sat right where the eye goes first. Sizing each tab to its own
word and splitting the leftover room evenly between them puts the same
distance between every pair. */
@media (max-width:899px){
/* space-EVENLY, not space-between. Between puts the outer two tabs hard
against the screen edge while every inner gap is twice that, and with six
tabs instead of five there is less slack to disguise it: measured at 390px
it read 9 / 21 / 21 / 21 / 21 / 21 / 9. Evenly shares the room across all
seven spaces including the two ends, so the row is symmetrical -- 18 / 17 /
17 / 18 / 17 / 17 / 18 at 390px, and still even at 320. The side padding
and the gap both go, because either one would be added on top of the space
the browser is already distributing and reintroduce the imbalance. */
.tabs{justify-content:space-evenly;gap:0;padding-left:0;padding-right:0}
/* Retired. Everything above this line was written when the bar had SIX
     tabs and the job was sharing one screen width evenly between them.
     There are nine now, and sharing 390 points nine ways gives each label
     43 -- less than the word "Customers" needs, which is why the labels on
     a phone had started running into each other.

     styles.css already says what to do instead, and has for a while:
     "Nine tabs will not fit across a phone. Let the strip scroll rather
     than squeezing every label until none of them can be read." It sets
     min-width:72px and hides the scrollbar, and setView() already scrolls
     the tab you switched to into view. All of it was being undone here by
     min-width:0.

     So the tabs keep their 72 points and the row overflows, which is the
     point: five and a half fit on a 390-point phone, so the sixth is
     always cut in half at the edge. That half tab is the only thing that
     tells anybody the row moves, and a row that fits exactly would not
     have it. */
  .tabs .tab{flex:0 0 auto;min-width:72px;padding-left:0;padding-right:0}
}
@media (max-width:430px){
.tabs .tab{font-size:10.5px;padding-top:9px;padding-bottom:7px}
.tabs .tab svg{width:22px;height:22px}
}

/* =====================================================================
   INSTALL CALENDAR
   A month grid for the Schedule tab's Installs scope. Sales work gets
   seven day-chips because an estimate is set for this week; production
   work is booked six weeks out, and the question you open it to answer
   is which WEEKS are full — which a list cannot show and a grid can.
   ===================================================================== */
.cal{background:var(--card);border:1px solid var(--line);border-radius:14px;
  padding:10px 10px 12px;margin:0 0 14px}
.cal-head{display:flex;align-items:center;gap:8px;padding:2px 2px 10px}
.cal-title{flex:1 1 auto;min-width:0;text-align:center;line-height:1.25}
.cal-title b{display:block;font-size:15px;color:var(--txt)}
.cal-title small{display:block;font-size:11px;color:var(--txt-3)}
/* 34px square: the smallest target that is still comfortable with a
   thumb, and the arrows are the only controls on this row. */
.cal-nav{flex:0 0 auto;width:34px;height:34px;border-radius:10px;
  border:1px solid var(--line);background:var(--card);color:var(--txt-2);
  font-size:19px;line-height:1;cursor:pointer}
.cal-nav:disabled{opacity:.32;cursor:default}
.cal-dow{display:grid;grid-template-columns:repeat(7,1fr);gap:4px;
  margin-bottom:4px}
.cal-dow span{text-align:center;font-size:10px;font-weight:700;
  letter-spacing:.04em;color:var(--txt-3)}
.cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:4px}
/* Square cells at every width, so the month keeps its shape on a phone
   instead of collapsing into seven tall ribbons. */
.cal-cell{position:relative;aspect-ratio:1;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:1px;
  border:1px solid transparent;border-radius:9px;background:transparent;
  font:inherit;color:var(--txt-3);cursor:default;padding:0}
.cal-cell.blank{border:0}
.cal-cell i{font-style:normal;font-size:12px;line-height:1}
.cal-cell em{font-style:normal;font-size:10px;font-weight:800;line-height:1;
  color:var(--teal)}
/* A day with work is the only thing worth looking at, so it is the only
   thing that carries weight. Empty days stay quiet rather than being
   drawn as empty boxes. */
.cal-cell.has{background:var(--card-2);color:var(--txt);font-weight:700;
  cursor:pointer}
.cal-cell.today{border-color:var(--teal)}
.cal-cell.today i{color:var(--teal)}
.cal-cell.on{background:var(--teal);border-color:var(--teal);color:#fff}
.cal-cell.on i,.cal-cell.on em{color:#fff}

@media (min-width:900px){
  /* Seven square cells across 1240px would be 170px each — a wall
     calendar, not a control. The grid only needs to be readable. */
  .cal{max-width:460px}
}

/* A day heading INSIDE an expanded branch card. .sec is the section
   heading for a whole screen; nested one level in it shouts. */
/* The search results borrow it. Jobs / Projects / Messages is the
   same job: a quiet divider between groups inside one list, not a
   heading for the screen. */
.sched-sub, .sr-sec{
  display:flex;align-items:baseline;justify-content:space-between;gap:8px;
  margin:10px 2px 4px;font-size:11px;font-weight:700;letter-spacing:.03em;
  text-transform:uppercase;color:var(--txt-3)
}
.sched-sub small, .sr-sec small{font-size:10.5px;font-weight:600;letter-spacing:0;
  text-transform:none;opacity:.8}
.sched-sub:first-child, .sr-sec:first-child{margin-top:2px}

/* ---------- auto-posted wins ----------
   A sold estimate writes itself into #wins. It is a real message, so it
   scrolls and pushes like any other, but it must not read as a person
   typing -- hence a card of its own instead of a bubble. */
.win{
  display:flex;align-items:center;gap:10px;
  margin:12px 6px;padding:11px 14px;border-radius:14px;
  background:var(--card-2);border:1px solid var(--gold);
  box-shadow:0 1px 0 rgba(0,0,0,.04);
  font-size:14px;line-height:1.35
}
.win-burst{font-size:19px;line-height:1;flex:0 0 auto}
.win-line{flex:1 1 auto;min-width:0;color:var(--txt)}
.win-line b{font-weight:700}
.win-time{flex:0 0 auto;font-size:11.5px;color:var(--txt-3);white-space:nowrap}

/* ---------- pinned strip ----------
   Sits between the channel row and the messages. Quiet by default: it is a
   shelf, not an announcement, and it must not compete with the newest
   message for attention. Capped at three items by the painter. */
.pin-strip{display:flex;flex-direction:column;gap:4px;padding:6px 10px;
  background:var(--card);border-bottom:1px solid var(--line)}
.pin-item{display:flex;align-items:center;gap:7px;width:100%;text-align:left;
  background:none;border:0;padding:4px 6px;border-radius:8px;cursor:pointer;
  color:var(--txt-2);font-size:12.5px;font-family:inherit}
.pin-item:hover{background:var(--bg)}
.pin-item svg{flex:none;color:var(--txt-3)}
.pin-item span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* On desktop the chat column is a NAMED-AREAS grid, so a new child with no
   area of its own auto-places into an implicit row at the bottom -- which is
   exactly where the pinned strip first appeared, under the composer. It needs
   its own row above the messages. Below 900px this is not a grid at all, so
   these properties are inert there and DOM order already puts it in place. */
@media (min-width: 900px){
  #view-chat{
    grid-template-rows: auto minmax(0,1fr) auto auto auto;
    grid-template-areas:
      "chans pins"
      "chans msgs"
      "chans mention"
      "chans reply"
      "chans composer";
  }
  #view-chat > #pinStrip{ grid-area: pins; }
}

/* ==========================================================================
   CHANNEL LIST ON THE PHONE
   Logan: open the app and see the channels, then tap into one -- instead of
   being dropped into General with the rest hidden off the edge of a strip.
   A horizontal strip is a fixed-width shelf: at 7 channels it is fine, at 20
   everything past the edge gets no traffic because nothing says it is there.

   ENTIRELY INSIDE max-width:899px. Above that the sidebar already shows every
   channel permanently, so none of this exists and #view-chat.room is inert.
   (The cascade trap from the v77 work: a bare .chan-row rule out here would
   beat the desktop block further up the file.)
   ========================================================================== */
@media (max-width: 899px){
  .chan-back, .chan-cur{display:none}

  /* ---------- list pane: the channels ARE the screen ---------- */
  #view-chat:not(.room) .chan-strip{
    flex-direction:column;align-items:stretch;gap:2px;
    overflow-y:auto;overflow-x:hidden;padding:6px 8px 10px;
  }
  #view-chat:not(.room) .chan-row{
    flex:1 1 auto;min-height:0;overflow:hidden;
    flex-direction:column;align-items:stretch;gap:0;padding:0;
  }
  #view-chat:not(.room) .chan-strip{flex:1 1 auto}
  #view-chat:not(.room) .chan-search{
    order:2;flex:none;width:auto;height:40px;margin:6px 10px 10px;
    display:flex;align-items:center;justify-content:flex-start;gap:9px;
    padding:0 12px;border:1px solid var(--line);border-radius:11px;
    background:var(--bg);color:var(--txt-3);font-size:14px;
  }
  #view-chat:not(.room) .chan-search::after{content:"Search messages"}
  #view-chat:not(.room) .chan-row::after{display:none}   /* the fade belongs to the strip */
  #view-chat:not(.room) .chan-strip .chip{
    display:flex;align-items:center;gap:9px;width:100%;
    border-radius:11px;padding:13px 12px;font-size:15px;font-weight:600;
    background:none;border:0;
  }
  #view-chat:not(.room) .chan-strip .chip.active{background:var(--bg)}
  #view-chat:not(.room) .chan-strip .chip .chip-badge{margin-left:auto}
  #view-chat:not(.room) .chan-strip .chip .chip-when{
    /* Off. It only ever appeared on rows that had a message recently, so the
       column sat blank on half the list, and a column that is empty half the
       time reads as broken rather than quiet. The order already says what is
       recent. Everything else about the rule is kept for the day it returns. */
    display:none;
    margin-left:auto;font-style:normal;font-size:11.5px;color:var(--txt-3);font-weight:500
  }
  /* a badge and a time both want the right edge; the badge wins and the time
     sits before it rather than fighting for the same spot */
  #view-chat:not(.room) .chan-strip .chip .chip-when + .chip-badge{margin-left:auto}
  #view-chat:not(.room) .chan-strip .chip.new{justify-content:center;color:var(--teal);border-bottom:0}

  /* everything that belongs to a conversation is simply not on this screen */
  #view-chat:not(.room) .pin-strip,
  #view-chat:not(.room) .msgs,
  #view-chat:not(.room) .mention-bar,
  #view-chat:not(.room) .reply-bar,
  #view-chat:not(.room) .rec-bar,
  #view-chat:not(.room) #pendingPhoto,
  #view-chat:not(.room) .composer{display:none !important}

  /* ---------- room pane: back arrow, channel name, search ---------- */
  #view-chat.room .chan-strip{display:none}
  #view-chat.room .chan-back{
    display:flex;align-items:center;justify-content:center;flex:none;
    width:34px;height:34px;background:none;border:0;padding:0;
    color:var(--txt);cursor:pointer
  }
  #view-chat.room .chan-cur{
    display:block;flex:1 1 auto;min-width:0;font-weight:700;font-size:15.5px;
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap
  }
  #view-chat.room .chan-row{flex-direction:row;align-items:center;gap:4px;padding:4px 8px}
  #view-chat.room .chan-row::after{display:none}
}

/* The desktop sidebar keeps the look it already had: no timestamps, and none
   of the phone navigation, which has no job beside a permanent sidebar. */
@media (min-width: 900px){
  .chip-when{display:none}
  #view-chat .chan-back, #view-chat .chan-cur{display:none}
}

/* ---------------------------------------------------------------------
   Skeletons (round 5). A grid or list marked data-skel that has nothing
   in it yet shows two soft grey blocks with a slow shimmer instead of a
   blank gap, so a screen reads as "loading" rather than "broken" in the
   half second before the numbers land. Once app.js paints anything into
   it -- real rows or an .empty line -- the blocks go. Pure CSS.
   --------------------------------------------------------------------- */
@keyframes d1shimmer{from{background-position:200% 0}to{background-position:-200% 0}}
[data-skel]:empty{display:grid;gap:10px;min-height:20px}
[data-skel]:empty::before,[data-skel]:empty::after{
  content:"";display:block;border-radius:14px;min-height:72px;
  background:linear-gradient(90deg,var(--field,#f0f3f7) 25%,var(--card,#fff) 50%,var(--field,#f0f3f7) 75%);
  background-size:200% 100%;animation:d1shimmer 1.6s linear infinite;opacity:.9}
.kpi-grid[data-skel]:empty{grid-template-columns:repeat(2,1fr)}
.kpis[data-skel]:empty{grid-template-columns:repeat(2,1fr)}
.list[data-skel]:empty::before,.list[data-skel]:empty::after{min-height:54px;border-radius:12px}
.board[data-skel]:empty::before,.board[data-skel]:empty::after{min-height:54px}
.podium[data-skel]:empty{grid-template-columns:repeat(2,1fr)}
@media (prefers-reduced-motion:reduce){[data-skel]:empty::before,[data-skel]:empty::after{animation:none}}

/* ---- loading placeholders ----
   Shown while the channel list is still in flight. The point is that a
   slow start should look like it is working, not like it is empty. */
.chip.skel {
  pointer-events: none;
  cursor: default;
}
.chip.skel .skel-bar {
  display: block;
  height: 15px;
  border-radius: 8px;
  background: currentColor;
  opacity: .09;
  animation: d1-skel 1.4s ease-in-out infinite;
}
@keyframes d1-skel {
  0%, 100% { opacity: .07; }
  50%      { opacity: .16; }
}
@media (prefers-reduced-motion: reduce) {
  .chip.skel .skel-bar { animation: none; }
}

/* A job/project thread opened from another tab floats over it (openThreadHere
   in app.js). The phone back arrow gives way to a labelled Back that returns
   to that tab; it shows on desktop too, where the arrow never exists. */
.float-back{display:none}
body.thread-float #view-chat .float-back{
  display:inline-flex;align-items:center;flex:none;align-self:flex-start;
  border:0;background:transparent;color:var(--teal-ink, #0f766e);
  font:inherit;font-weight:700;font-size:15px;padding:6px 8px;
  cursor:pointer;white-space:nowrap;-webkit-tap-highlight-color:transparent
}
body.thread-float #view-chat .chan-back{display:none !important}
body.thread-float #view-chat.room .chan-row{align-items:center}

/* The production queue stacks three lines per row -- who it is, which
   column it is stuck in, then the detail. <small> is inline, so without
   this the last two run together and a row reads "Review Project10069":
   the task name and the job number with nothing between them. */
.item .who small.pbline{display:block}

/* Gross profit rows (Production tab). Name and the number people compare on
   one line, the working underneath in small grey type, then a thin bar at
   the margin. Tabular figures so the dollar amounts line up row to row. */
.item.gp-row{align-items:center;padding:12px 14px}
.gp-main{flex:1;min-width:0}
.gp-top{display:flex;align-items:baseline;gap:10px}
.gp-name{flex:1;min-width:0;font-size:14.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gp-val{font-size:16px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.gp-pending{font-size:11.5px;font-weight:600;color:var(--txt-3);background:var(--card-2);
  border-radius:999px;padding:3px 9px;white-space:nowrap}
.gp-sub{margin-top:3px;font-size:12px;color:var(--txt-3);font-variant-numeric:tabular-nums;line-height:1.4}
.gp-sub b{color:var(--txt-2);font-weight:600}
.gp-bar{margin-top:8px;height:4px;border-radius:4px;background:var(--card-2);overflow:hidden}
.gp-bar i{display:block;height:100%;border-radius:4px;background:var(--teal)}

/* ---------- the chat activity feed ----------
   Logan, Sept 24: "this looks like shit ... let's make this look more like
   the AccuLynx activity area."

   The uneven widths were not a style choice, they were a bug. A feed row
   is .item -- a card class -- and .item here is a <button>. Inside .list
   that is fine, because .list is a flex column and its children stretch;
   but .feed-list had NO CSS AT ALL, so every button shrank to fit its own
   text and no two rows lined up.

   It is a feed, not a deck of cards, so it is one panel with hairline
   dividers instead of eight floating rectangles. The message is what you
   are scanning for, so it gets a line of its own; the conversation name,
   which used to be the bold headline and read "Scoreboard" five times down
   the screen, is now a quiet label next to the author. The avatar gives
   the eye a column to run down, and the timestamp is nowrap because
   "6:17 PM" was breaking across two lines. */
.feed-list{display:flex;flex-direction:column;gap:8px}
.feed-panel{display:flex;flex-direction:column;background:var(--card);
  border:1px solid var(--line);border-radius:13px;overflow:hidden;
  box-shadow:0 1px 2px #0f213a0f}
.feed-row{display:flex;align-items:flex-start;gap:11px;width:100%;
  padding:11px 13px;background:none;border:0;border-top:1px solid var(--line);
  font:inherit;color:var(--txt);text-align:left;cursor:pointer}
.feed-row:first-child{border-top:0}
.feed-row:active{background:var(--field)}
.feed-row .avatar{margin-top:1px;cursor:inherit}
.feed-main{flex:1;min-width:0}
.feed-top{display:flex;align-items:baseline;gap:6px}
/* The name never squeezes the time out, and never takes more than about
   half the row however long somebody's name is. */
.feed-top b{font-size:14px;font-weight:600;flex:none;max-width:48%;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.feed-where{font-size:11.5px;color:var(--txt-3);min-width:0;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.feed-when{font-size:11.5px;color:var(--txt-3);white-space:nowrap;
  flex:none;margin-left:auto;padding-left:8px}
.feed-say{font-size:13px;color:var(--txt-2);line-height:1.4;margin-top:2px;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
  overflow:hidden}

/* The backlog job list sits under the two tiles inside an open branch, so
   it needs a little air or it butts straight into them. */
.bk-jobs{margin-top:10px}

/* ---------------- blocked time ----------------
   Blocked time is not a job, and the row says so before you read a word of
   it: no customer, a muted face, a slab down the left edge. The whole risk
   of this feature is somebody using it to schedule work -- there is a
   two-day install on the dispatch board as a non-job event right now -- so
   it is deliberately the plainest row on the screen.

   .item.appt carries the display:block rule for its own <small> tags and a
   blocked row is not one, so it needs its own. */
#schedList .item.block{
  background:var(--card);
  border-left:3px solid var(--line);
}
#schedList .item.block .who small{display:block;line-height:1.45}
#schedList .item.block .who b{font-weight:600}
#schedList .item.block .appt-time{color:var(--txt-3)}
#schedList .pill.blocked{background:#eef0f3;color:#5b6472}

/* .sheet-head is sticky, and margin collapses straight through a sticky
   box while padding does not -- the same thing that clipped the booking
   sheet in v297. Pad the first field rather than margin it.

   v305: 12px was not enough. The collapse leaves the next box 9.8px
   ABOVE the header's bottom edge, so padding-top:12px bought only 2.2px
   of real clearance and the label clipped the moment the sheet scrolled
   a hair. Measured on the live page: 24px lands 14.2px clear. */
#blockSheet .sheet-head + .fld{padding-top:24px}

/* .fld is display:block and stacks its label above its control, which is
   right for every field here except the checkbox, where the box belongs
   beside its words. .fld > span is block too, so both need saying. */
#blockSheet .fld.chk{
  display:flex;flex-direction:row;align-items:flex-start;
  gap:10px;cursor:pointer;margin-bottom:16px;
}
#blockSheet .fld.chk > input{
  width:18px;height:18px;margin:1px 0 0;flex:0 0 auto;border-radius:5px;
}
#blockSheet .fld.chk > span{
  display:block;margin:0;font-size:13px;font-weight:400;line-height:1.45;
  letter-spacing:0;color:var(--txt-2);
}

/* .btn.danger on its own had no rule -- only .btn.link.danger, which is red
   text on nothing. "Remove this" was rendering as a plain default button,
   which is the wrong shape for the one destructive thing on the sheet. */
#blockOneSheet .btn.danger{
  background:transparent;color:var(--bad);
  border:1px solid var(--bad);font-weight:600;
}
#blockOneSheet .btn.danger:active{background:rgba(192,57,43,.08)}

/* .sheet-head is sticky, and margin collapses straight through a sticky
   box while padding does not -- the same thing that clipped the booking
   sheet in v297 and the blocked-time sheet in v298. Measured on the
   chooser at 400px: the Book a job button sat 10px BEHIND the header,
   and margin-top:22px moved it exactly nowhere. A button cannot absorb
   the padding itself because its background fills the box, so the
   chooser pads a wrapper and the job sheet pads its first field.

   v305: raised 12px -> 24px on all three sheets. See the note above
   #blockSheet -- the first box starts 9.8px above the header bottom, so
   12px was never a gap, it was a 2.2px hairline. */
#pickSheet .sheet-head + .pick-body{padding-top:24px}
#jobSheet  .sheet-head + .fld{padding-top:24px}

/* The two captions under the chooser buttons belong to the button above
   them, not the one below, so they sit tight to it. */
#pickSheet .pick-body p.muted{margin:6px 2px 14px}
#pickSheet .pick-body p.muted:last-of-type{margin-bottom:8px}

/* Who is running the visit, directly under the customer. Same size as the two
   lines below it so the row keeps its rhythm, but a step darker and heavier,
   because on this screen the person is the thing being looked for. Installs
   do not get this line -- the crew is already on line two there. */
.item .who small.appt-tech{color:var(--txt-2);font-weight:600}
/* Leaderboard: pin the period picker so no control ever renders half-sliced.

   WHAT WAS ACTUALLY WRONG
   Nothing overlaps. .topbar is position:relative everywhere -- there is no sticky
   header and no z-index fight. What you saw is the scroll container's own top edge
   cutting the branch row (.seg.small) through the middle of the chips as the list
   scrolled past it. A segmented control sliced exactly in half reads as a rendering
   bug even though it is ordinary scrolling.

   THE FIX
   Pin the period strip at the very top of the scroll area. The branch row then
   travels up BEHIND an opaque pinned bar instead of being cut at a bare edge, and
   the period filter -- the control people actually change -- stays reachable without
   scrolling back up.

   The 24px of breathing room moves from the container to the branch row, so the
   pinned strip can sit flush at top:0. Measured with it applied: the strip pins at
   exactly the container's top edge (72px = 72px) and the branch row passes behind
   it at -44px, fully covered.

   box-shadow + clip-path paint the strip's background edge to edge, so it covers
   the container's side padding too. That padding is 28px on desktop and narrower on
   a phone; this works at both without hard-coding either.

   .metric-strip is itself overflow-x:auto (the chips scroll sideways on a phone).
   That does not interfere -- being a scroll container affects its CHILDREN's
   stickiness, not its own.
*/

#view-board .scroll { padding-top: 0 }

#view-board .seg.small { margin-top: 24px }

#view-board .metric-strip {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--bg);
  box-shadow: 0 0 0 100vmax var(--bg);
  clip-path: inset(0 -100vmax);
}

/* Stage chips under the Finance tiles.

   Each chip opens the stage bucket from schema-v315, which is the SAME customer-card
   sheet the money tiles open. A stage is another door into the same people, not a
   second way of drawing them -- which is why there is no card markup here at all,
   only the chips.

   The row scrolls sideways rather than wrapping. There are up to fourteen stages and
   only the ones holding money are drawn (money_stage_summary), but on a phone even
   six would wrap to three lines and push the list below the fold.
*/

.stage-chips {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding: 4px 0 2px;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.stage-chips::-webkit-scrollbar { display: none }

.stage-chips-lab {
  flex: 0 0 auto;
  align-self: center;
  padding-right: 4px;
  font-size: 12px;
  font-weight: 700;
  color: var(--txt-3);
  white-space: nowrap;
}

.stage-chip {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--card);
  font: inherit;
  text-align: left;
  cursor: pointer;
  white-space: nowrap;
}

.stage-chip:active { transform: scale(0.98) }

.stage-chip b {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.2px;
}

.stage-chip small {
  font-size: 11px;
  font-weight: 600;
  color: var(--txt-2);
}

/* The supplements line under a rep on the leaderboard (schema-v319, v320).

   Deliberately quieter than the sales figure beside it: supplements count toward the
   company total, and always did, but the ranking is on new sales. A rep who wrote
   $16k of insurance supplements should be able to see it there; he should not appear
   to have out-sold someone who brought in new work.
*/

.supp-line {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: -0.1px;
  color: var(--txt-3);
}


/* The two lines under the campaign table: what could not be attributed, and
   where each column's data actually starts. Quiet on purpose -- they are a
   caveat on the numbers above, not a number of their own. */
.mk-sold-note{margin:10px 2px 0;font-size:12px;line-height:1.5;color:var(--txt-3)}
.mk-sold-note a{color:var(--teal-ink);text-decoration:underline dotted;
  text-underline-offset:3px;cursor:pointer}

/* The margin line on a sold job card. Muted when there is no cost yet: that
   blank is deliberate and must not read as though it were a figure. */
.who small.gpLine{display:block;color:var(--txt-2);font-weight:600}
.who small.gpLineMuted{color:var(--txt-3);font-weight:400;font-style:italic}
/* GP at sold: real information, not a placeholder, so it keeps the
   weight of the actual margin line and only loses a little colour.
   The words carry the distinction - see marginFor() in app.js. */
.who small.gpLineSold{color:var(--txt-2);font-weight:600;opacity:.85}


/* A duplicate invoice is a billing error, not a status line -- it reads
   in the warning colour so it is not skimmed past with the project
   stage. Deliberately NOT netted out of the figure beside it: see
   money_duplicate_invoices(). */
.who small.dupFlag{color:var(--warn,#8a6508);font-weight:600}


/* ---------- Job messages sheet ----------
   Same row as every other drill-in list: the name on top, a grey line
   under it, one line of preview and a right-hand column. It was built
   with its own markup -- the job number inline with the name, an
   unclamped preview that ran to three lines, an italic date, no chevron
   -- so the sheet looked like a different app from the thread list it
   opens over. */
#jobMsgList .who small{display:block}
/* One line, like every other sub-line in the app. A message is longer
   than a status, so this is the only place that needs saying. */
#jobMsgList .who small.msg-prev{
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
#jobMsgList .drill-when{
  margin-left:auto;padding-left:10px;display:flex;align-items:center;gap:6px;
  color:var(--txt-3);font-size:12px;white-space:nowrap;flex:none}

/* A sold margin under 10% is a prompt, not a statistic. Same colour the
   duplicate-invoice flag uses, for the same reason: it wants opening. */
.who small.gpLineLow{color:var(--warn,#8a6508);font-weight:600}

/* v462: the Change job type sheet (v461) had the same sticky-header clip
   as the booking, blocked-time and job sheets above: its first label sat
   behind .sheet-head. Same fix, same 24px. */
#typeSheet .sheet-head + .fld{padding-top:24px}

/* v467: the Reschedule sheet, same sticky-header clearance as the rest. */
#moveSheet .sheet-head + .fld{padding-top:24px}

/* v469: By category rows on the Marketing tab -- the campaign list under a row, shown on tap */
#mkCats .cat-camps{display:block;margin-top:8px;line-height:1.4}

/* v471: the category / campaign sheet - Leads, Booked, Sold tabs */
#jobsSheet .grp-tabs{margin:4px 0 12px}
#jobsSheet .grp-calls{margin:0 0 10px;line-height:1.4}

/* v472: Reps page */
.reps-entry{margin-top:14px}
#repsSheet #repsPeriod{margin:4px 0 12px}
#repsSheet .rep-row .camp-cat{margin-bottom:4px}

/* v474: the job lists in #jobsSheet (What closed, Booked, the category sheet).
   On a phone the date column plus the amount and the Job button left the
   customer about a third of the row, so a job number broke over three lines.
   The date now sits on its own line above the name. */
#jobsSheet .jobrow{flex-wrap:wrap;align-items:center;row-gap:2px}
#jobsSheet .jobrow .appt-time{flex:0 0 100%;width:auto;color:var(--txt-3);font-weight:600}
#jobsSheet .jobrow .who{flex:1 1 0;min-width:0}

/* v477: What closed / Unsold rows. Logan, Oct 6: "format off". The amount
   and Job button sat in a column beside a name that had grown four lines
   (rep, collected bar, GP at sold), squeezing it and floating the amount
   halfway down. Now the date and the amount share the top line, the
   customer gets the width below, and the Job button sits up by the name. */
#jobsSheet .jobrow.drillrow .appt-time{order:1;flex:1 1 50%}
#jobsSheet .jobrow.drillrow .drill-amt{order:2;flex:0 0 auto}
#jobsSheet .jobrow.drillrow .who{order:3;flex:1 1 60%}
#jobsSheet .jobrow.drillrow > .btn{order:4;align-self:flex-start}

/* v478: job tags and Closeout disposition on the What closed / Unsold rows. */
#jobsSheet .job-out{margin-top:4px}
#jobsSheet .job-tags{display:flex;flex-wrap:wrap;gap:4px}
#jobsSheet .job-tag{font-size:11px;line-height:16px;padding:1px 7px;border-radius:999px;
  background:rgba(120,130,150,.14);color:var(--txt-3);font-weight:600;white-space:nowrap}
#jobsSheet .job-tag.claim{background:rgba(15,123,79,.13);color:#0f7b4f}
#jobsSheet .job-tag.noshow{background:rgba(200,40,40,.12);color:var(--bad)}
#jobsSheet .job-disp{display:block;margin-top:3px;font-size:12px;color:var(--txt-3)}

/* v479: the rep card and the job lists. Their sticky title bar sat 20px
   below where it scrolls from, so it covered the top of the first row of
   tiles (and the Sold / Unsold switch). Pinned to the sheet's top edge. */
#brSheet .sheet > .sheet-head,
#jobsSheet .sheet > .sheet-head{top:-20px}

/* v480: job cards in the job sheet look like the Customers cards (Logan,
   Oct 6). Name, grey lines, amount on the right; tags as a grey line with
   Claim Filed and Customer No Show in colour; the Unsold follow-up folded
   into the card under one "Follow-up:" line; Sold / Unsold in the title bar. */
#jobsSheet .sheet-head{flex-wrap:wrap}
#jobsSheet .sheet-head > #jobsSheetSeg{flex:0 0 100%}
#jobsSheet .drillcard .who small{display:block}
#jobsSheet .drillcard .metric{text-align:right;font-weight:700;font-variant-numeric:tabular-nums;white-space:nowrap}
#jobsSheet .drillcard .metric.v-bad{color:var(--bad);font-size:13px}
#jobsSheet .drillcard .metric.v-ok{color:var(--txt-3);font-size:13px;font-weight:600}
#jobsSheet .job-out{margin-top:2px}
#jobsSheet .job-tags{display:block}
#jobsSheet .job-tag-claim{color:#0f7b4f;font-weight:600}
#jobsSheet .job-tag-noshow{color:var(--bad);font-weight:600}
#jobsSheet .job-disp{display:block}
#jobsSheet .fu-bar{margin-top:8px;padding-top:6px;border-top:1px solid var(--line)}
#jobsSheet .fu-line{display:flex;align-items:center;justify-content:space-between;gap:8px}
#jobsSheet .fu-line .fu-status{display:inline;margin:0}
#jobsSheet .fu-toggle{background:none;border:0;padding:6px 0 6px 8px;font:inherit;font-size:12px;
  font-weight:600;color:#0b8a8f;cursor:pointer;white-space:nowrap}
#jobsSheet .fu-bar .fu-ctl{display:none;margin-top:4px}
#jobsSheet .fu-bar.fu-open .fu-ctl{display:block}
#jobsSheet .fu-bar .fu-ctl .seg{margin:0 0 6px}
#jobsSheet .drillcard{margin-bottom:8px}
#jobsSheet .fu-bar .fu-ctl .seg-btn{font-size:12px;padding-left:4px;padding-right:4px}

/* v481: one job card everywhere (Logan, Oct 6: "We want consistency").
   Every list of jobs - Marketing's category and campaign sheets, the Demo
   rate list, Sold lists, Finance's to-invoice lists, What closed and the
   rep card lists - uses the Customers card: name, grey lines, the figure on
   the right in the same size and weight as Customers, its label under it. */
.drillcard .who small{display:block}
.drillcard .metric,
#jobsSheet .drillcard .metric{flex:0 0 auto;text-align:left;font-size:16px;font-weight:400;
  font-variant-numeric:tabular-nums;white-space:nowrap}
.drillcard .metric.v-bad,
#jobsSheet .drillcard .metric.v-bad{color:var(--bad);font-size:13px;font-weight:600}
.drillcard .metric.v-ok,
#jobsSheet .drillcard .metric.v-ok{color:var(--txt-3);font-size:13px;font-weight:600}

/* v483: the Trade picker on a Production -> By trade job card. */
.drillcard .trade-set{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:12px;color:var(--txt-3)}
.drillcard .trade-set select{flex:1 1 auto;min-width:0;font:inherit;font-size:13px;padding:7px 8px;
  border:1px solid var(--line);border-radius:9px;background:var(--card);color:var(--txt)}
.drillcard .trade-done{color:#0f7b4f;font-weight:600;margin-top:4px}

/* ---------- trends and the 30% line on Scorecard tiles (v489) ---------- */
.kpi .trend{font-size:11.5px;font-weight:700;margin-top:6px;line-height:1.3}
.kpi .trend.up{color:#0f7a4a}
.kpi .trend.down{color:#b42318}
.kpi .trend.flat{color:var(--txt-3)}
.kpi .pace{display:inline-block;font-size:11px;font-weight:700;margin-top:7px;padding:2px 8px;border-radius:999px;line-height:1.4}
.kpi .pace.ok{background:#e6f5ec;color:#0f7a4a}
.kpi .pace.bad{background:#fdecea;color:#b42318}
.kpi .pace.wait{background:#eef1f5;color:var(--txt-3)}
.kpi .spark{display:flex;align-items:flex-end;gap:6px;margin-top:8px;color:var(--teal-ink)}
.kpi .spark svg{display:block;overflow:visible}
.kpi .spark span{font-size:10px;white-space:nowrap;color:var(--txt-3);line-height:1}
.kpi .spark rect{fill:currentColor;opacity:.3}
.kpi .spark rect.now{opacity:.85}
.kpi .spark .ln{fill:none;stroke:currentColor;stroke-width:1.6;stroke-linejoin:round}
.kpi .spark circle{fill:currentColor}
.kpi .spark .tg{stroke:#b42318;stroke-width:1;stroke-dasharray:2 2;opacity:.55}

/* ---------- Home (v490) ---------- */
#view-home .home-more{display:inline-block;margin:4px 2px 8px;padding:4px 0;font-weight:700;color:var(--teal-ink)}
.home-grp{margin-bottom:16px}
.home-grp-h{display:flex;align-items:center;gap:8px;margin:2px 2px 4px}
.home-grp-h b{font-size:14px}
.home-n{font-size:11px;font-weight:800;background:#fdecea;color:#b42318;border-radius:999px;padding:1px 8px;line-height:1.5}
.home-note{font-size:12px;color:var(--txt-3);margin:0 2px 8px;line-height:1.4}
.home-total{font-size:13px;color:var(--txt-3);margin:0 2px 8px}
.home-total b{font-size:20px;color:var(--teal-ink);margin-right:4px}
@media (min-width:900px){#homeKpis{grid-template-columns:repeat(2,minmax(0,1fr));max-width:680px}}
/* v491: the rep watcher adds its "Rep ·" line as a second <small> beside
   ours; on Home both read run together ("Rep · Mike SmallwoodOct 6"). One
   line each, and the rep name now comes only from that line. */
#view-home .item .who small{display:block}

/* ---------- Home, Waiting on payment: days past due (v492) ----------
   Amber up to 30 days, orange to 60, red past that -- and the words are
   always written out, so the colour never carries the meaning alone. */
.due-pills{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
.due-pill{display:inline-block;font-size:11px;font-weight:800;padding:2px 9px;border-radius:999px;line-height:1.5}
.due-new{background:#eef1f5;color:var(--txt-3)}
.due-amber{background:#fff3d4;color:#7a5200}
.due-orange{background:#ffe6cf;color:#9a4300}
.due-red{background:#fdecea;color:#b42318}
.home-late{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:13px;font-weight:700;margin:0 2px 10px}

/* ---------- Home: "You" row for managers who sell (v493) ---------- */
.home-you{margin-bottom:6px}
.home-you-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap;font-size:13px;color:var(--txt-3)}
.home-you-row b{font-size:20px;font-weight:800;letter-spacing:-.4px;color:var(--teal-ink);margin-right:2px}
.home-you-row span + span b{color:inherit;color:var(--txt-1,#111827)}

/* ---------- Home: sales by month (v494) ---------- */
#view-home .kpi .spark{display:none}
.home-chart{padding:14px 14px 12px}
.home-chart .hc-sum{font-size:13px;color:var(--txt-2);margin:0 2px 6px}
.home-chart .hc-sum b{font-size:16px;color:var(--teal-ink)}
.home-chart .up{color:#067647;font-weight:700}
.home-chart .down{color:#b42318;font-weight:700}
.home-chart .hc-svg{display:block;width:100%;height:auto;overflow:visible}
.home-chart .even{color:var(--txt-3);font-weight:700}
.home-chart .hc-svg .g{stroke:var(--line);stroke-width:1}
.home-chart .hc-svg text{font-size:11px;fill:var(--txt-3)}
.home-chart .hc-svg .hit{fill:transparent;cursor:pointer}
.home-chart .hc-svg .ty{fill:var(--teal-ink)}
.home-chart .hc-svg .ly{fill:var(--teal-ink);opacity:.28}
.home-chart .hc-svg .pace{fill:none;stroke:var(--teal-ink);stroke-width:1.5;stroke-dasharray:4 3}
.home-chart .hc-svg .m.on .hit{fill:var(--teal-ink);opacity:.07}
.home-chart .hc-svg .m.on .xl{fill:var(--txt);font-weight:700}
.home-chart .hc-legend{display:flex;flex-wrap:wrap;gap:4px 14px;font-size:12px;color:var(--txt-3);margin:6px 2px 0}
.home-chart .hc-legend i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:5px;vertical-align:-1px;background:var(--teal-ink)}
.home-chart .hc-legend i.ly{opacity:.28}
.home-chart .hc-legend i.pace{background:none;border:1.5px dashed var(--teal-ink)}
.home-chart .hc-info{margin:10px 2px 0;padding-top:10px;border-top:1px solid var(--line);font-size:14px;line-height:1.5}
.home-chart .hc-info .hc-ly{font-size:13px;color:var(--txt-3)}

/* ---------- Home: Ran it / No-show (v496) ---------- */
.co-btns{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}
.co-btn{font:inherit;font-size:13px;font-weight:700;padding:6px 14px;border-radius:999px;border:1px solid var(--line);background:#fff;color:var(--txt);cursor:pointer}
.co-btn.co-ran{color:#fff;background:var(--teal-ink);border-color:var(--teal-ink)}
.co-btn.co-ns{color:#b42318}
.co-btn[data-armed="1"]{background:#b42318;border-color:#b42318;color:#fff}
.co-btn[disabled]{opacity:.5;cursor:default}
.co-msg{font-size:12px;color:var(--txt-3);margin-top:6px}
.co-msg.bad{color:#b42318}
.co-row.co-gone{opacity:.55;transition:opacity .3s}

/* v497: duplicate sales job note */
#view-home .item .who small.co-dup{color:#8a6508;margin-top:4px}

/* ---------- Home: open quotes by age, Scorecard: closing rate by week (v498) ---------- */
.hq-bars{display:flex;flex-direction:column;gap:6px;margin:8px 0 2px}
.hq-row{display:grid;grid-template-columns:92px 1fr auto;align-items:center;gap:10px;width:100%;padding:7px 8px;border:0;border-radius:8px;background:none;font:inherit;color:var(--txt);text-align:left;cursor:pointer}
.hq-row:disabled{cursor:default;opacity:.55}
.hq-row.on{background:rgba(15,94,99,.08)}
.hq-row .hq-l{font-size:13px;color:var(--txt-2)}
.hq-row.on .hq-l{color:var(--txt);font-weight:700}
.hq-row .hq-bar{height:14px;border-radius:4px;background:var(--line);overflow:hidden}
.hq-row .hq-bar i{display:block;height:100%;border-radius:4px;background:var(--teal-ink)}
.hq-row.old .hq-bar i{background:#b54708}
.hq-row .hq-v{font-size:13px;color:var(--txt-3);white-space:nowrap}
.hq-row .hq-v b{color:var(--txt)}
.home-chart .hq-warn{color:#b54708;font-weight:700}
.home-chart .hq-list{margin-top:8px}
.home-chart .hq-list .item{padding-left:0;padding-right:0}
.cr-trend .cr-seg{margin:0 0 10px}
.cr-trend .cr-svg .ln{fill:none;stroke:var(--teal-ink);stroke-width:2}
.cr-trend .cr-svg .goal{stroke:#b54708;stroke-width:1.5;stroke-dasharray:5 4}
.cr-trend .cr-svg .goal-l{fill:#b54708;font-weight:700}
.cr-trend .cr-svg .pt{fill:#b42318;stroke:var(--card,#fff);stroke-width:2}
.cr-trend .cr-svg .pt.hit30{fill:#067647}
.cr-trend .cr-svg .pt.thin{fill:var(--card,#fff);stroke:var(--txt-3);stroke-width:1.5}
.cr-trend .cr-svg .pt.cur{stroke-dasharray:3 2}
.cr-trend .hc-legend i.cr-dot{border-radius:50%;background:#067647}
.cr-trend .hc-legend i.cr-thin{border-radius:50%;background:none;border:1.5px solid var(--txt-3)}
.cr-trend .hc-legend i.cr-goal{height:0;border-top:2px dashed #b54708;border-radius:0;background:none;vertical-align:3px}
.cr-trend .cr-note{margin:8px 2px 0;font-size:12px;color:var(--txt-3)}

/* ---------- v500: rep funnel, spend vs sold by channel ---------- */
.rf-team{padding:14px;margin-bottom:10px}
.rf-team .rf-title{font-weight:700;font-size:15px}
.rf-team .rf-sub{font-size:12px;color:var(--txt-3);margin:2px 0 10px}
/* v502: every bar the same length -- the count sits in a fixed column and the note goes under the bar, so the tracks line up on a phone */
.rf-step{display:grid;grid-template-columns:64px 1fr 36px;align-items:center;column-gap:10px;row-gap:2px;margin:8px 0}
.rf-step .rf-l{font-size:13px;color:var(--txt-2)}
.rf-step .rf-track{height:14px;border-radius:4px;background:var(--line);overflow:hidden}
.rf-step .rf-track i{display:block;height:100%;border-radius:4px}
.rf-step .rf-n{font-size:14px;color:var(--txt);text-align:right}
.rf-step .rf-note{grid-column:2 / 4;font-size:12px;color:var(--txt-3)}
.rf-out{font-size:12px;color:var(--txt-3);margin-top:8px}
.rf-legend{display:flex;flex-wrap:wrap;gap:4px 12px;font-size:11px;color:var(--txt-3);margin-top:8px}
.rf-legend i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:4px;vertical-align:-1px}
.rf-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;background:var(--line);margin-top:8px}
.rf-bar i{display:block;height:100%}
.rf-cap{display:block;font-size:11px;color:var(--txt-3);margin-top:4px}
.rf-book{background:var(--teal-ink);opacity:.35}
.rf-show2{background:var(--teal-ink);opacity:.6}
.rf-sold{background:var(--teal-ink)}
.rf-dem{background:var(--teal-ink);opacity:.5}
.rf-show{background:#98a2b3}
.rf-open{background:#f2c46d}
.rf-lost{background:#f4a7a0}
#mkRoi{padding:14px}
#mkRoi .up{color:#067647;font-weight:700}
#mkRoi .down{color:#b42318;font-weight:700}
.roi-legend{display:flex;gap:14px;font-size:12px;color:var(--txt-3);margin:6px 0 4px}
.roi-legend i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:5px;vertical-align:-1px}
.roi-row{padding:10px 0;border-top:1px solid var(--line)}
.roi-row:first-of-type{border-top:0}
.roi-h{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px;font-size:14px}
.roi-x{font-size:13px}
.roi-bar{display:flex;align-items:center;gap:8px;margin:3px 0}
.roi-bar i{display:block;height:12px;border-radius:3px;flex:0 0 auto}
.roi-bar span{font-size:12px;color:var(--txt-3);white-space:nowrap}
.roi-sold{background:var(--teal-ink)}
.roi-spend{background:#b54708}
.roi-note{margin:8px 0 0;font-size:12px;color:var(--txt-3)}

/* ---------- v503: gross profit forecast (admins) ---------- */
.gp-card .gp-top{display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
.gp-card .gp-big{font-size:28px;font-weight:800;color:var(--teal-ink);line-height:1.1}
.gp-card .gp-sub{font-size:13px;color:var(--txt-3);margin-top:2px}
.gp-card .gp-top > span{font-size:13px;white-space:nowrap;margin-top:6px}
.gp-card .gp-bar{height:12px;border-radius:6px;background:var(--line);overflow:hidden;margin:12px 0 8px;border:1.5px dashed var(--teal-ink)}
.gp-card .gp-bar i{display:block;height:100%;background:var(--teal-ink)}
.gp-card .gp-legend{display:flex;flex-direction:column;gap:3px;font-size:13px;color:var(--txt-2)}
.gp-card .gp-legend b{color:var(--txt)}

/* v504: the first field under a sheet header sat under the header's edge (Reason on Cancel this job); the same gap the other sheets got */
.sheet > .sheet-head + .fld{padding-top:24px}

/* v505: branch chips on the closing rate by week chart */
.cr-trend .cr-branches{display:flex;flex-wrap:wrap;gap:8px;margin:-2px 0 12px}

/* v506: the four branch chips on one line (Logan, Oct 10). Smaller chips that share the width; on a very narrow screen the row scrolls sideways instead of wrapping. */
.cr-trend .cr-branches{flex-wrap:nowrap;gap:6px;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.cr-trend .cr-branches::-webkit-scrollbar{display:none}
.cr-trend .cr-branches .chip{flex:1 0 auto;padding:6px 10px;font-size:13px;white-space:nowrap;text-align:center;margin:0}


/* v641 -- "Need more room to type": on a phone the bottom tab bar hides while
   the chat box has the keyboard (app.js adds body.chat-typing on focus). */
@media (max-width: 899px) {
  body.chat-typing .tabs { display: none; }
}
