:root{
--bg:#F5F7F9;--surface:#FFFFFF;--surface-2:#FAFBFC;--surface-3:#F0F3F6;
--ink:#0D1826;--ink-2:#4A586B;--ink-3:#7B8899;--ink-4:#66727F;--ink-5:#A2ADBB;
--line:#E5E9EE;--line-2:#D3D9E1;
--navy:#0E2740;--navy-2:#0A1D30;--navy-line:rgba(255,255,255,.10);--navy-ink:#B8C6D6;--navy-ink-2:#7E92A8;
--brand:#2E6BA8;--brand-ink:#1C4E80;--brand-soft:#E7F0F8;
--slate:#64748B;--slate-soft:#EDF0F4;
--blue:#2E6BA8;--blue-soft:#E7F0F8;
--teal:#0E7C6B;--teal-soft:#E1F0ED;
--violet:#5F51BE;--violet-soft:#ECEAF9;
--amber:#A9711A;--amber-soft:#FAF0DC;
--green:#12795A;--green-soft:#E2F0EB;
--red:#BE4340;--red-soft:#FAEAE9;
--faint:#93A0AF;--faint-soft:#F0F2F5;
--r:10px;--r-sm:8px;
--sh:0 1px 2px rgba(13,24,38,.05);--sh-2:0 1px 3px rgba(13,24,38,.07),0 8px 24px rgba(13,24,38,.06);
--ring:0 0 0 3px rgba(46,107,168,.18);
/* Tells the browser which palette its OWN widgets should use — the native date
   and time pickers, form controls, scrollbars, autofill and the caret. It was
   never declared, so every one of them rendered light: the date picker opened
   as a white calendar in the middle of a dark app, because the browser had no
   way to know the app was dark. It has to be set in all THREE theme blocks,
   the same way the tokens are. */
color-scheme:light;
}
[data-theme="dark"]{
color-scheme:dark;
--bg:#0A1017;--surface:#111A24;--surface-2:#151F2A;--surface-3:#1A2632;
--ink:#E9EEF4;--ink-2:#9FADBE;--ink-3:#72808F;--ink-4:#8894A2;--ink-5:#5A6775;
--line:#1E2A37;--line-2:#2A3846;
--navy:#080E15;--navy-2:#05090E;--navy-line:rgba(255,255,255,.07);--navy-ink:#9DAEC1;--navy-ink-2:#67788B;
--brand:#579BDD;--brand-ink:#7DB4E8;--brand-soft:#132738;
--slate:#93A2B5;--slate-soft:#1B2531;
--blue:#579BDD;--blue-soft:#132738;
--teal:#3FBBA5;--teal-soft:#0D2A27;
--violet:#9A8DEC;--violet-soft:#1D1B33;
--amber:#D9A03F;--amber-soft:#2C2312;
--green:#3DB88E;--green-soft:#0C2721;
--red:#E4736F;--red-soft:#2C1615;
--faint:#75828F;--faint-soft:#182029;
--sh:0 1px 2px rgba(0,0,0,.4);--sh-2:0 2px 6px rgba(0,0,0,.45),0 12px 32px rgba(0,0,0,.4);
--ring:0 0 0 3px rgba(87,155,221,.22);
}
/* Honor the OS preference when the user hasn't made an explicit choice.
   An explicit [data-theme] (from the sidebar toggle, persisted) always wins. */
@media (prefers-color-scheme:dark){:root:not([data-theme]){
color-scheme:dark;
--bg:#0A1017;--surface:#111A24;--surface-2:#151F2A;--surface-3:#1A2632;
--ink:#E9EEF4;--ink-2:#9FADBE;--ink-3:#72808F;--ink-4:#8894A2;--ink-5:#5A6775;
--line:#1E2A37;--line-2:#2A3846;
--navy:#080E15;--navy-2:#05090E;--navy-line:rgba(255,255,255,.07);--navy-ink:#9DAEC1;--navy-ink-2:#67788B;
--brand:#579BDD;--brand-ink:#7DB4E8;--brand-soft:#132738;
--slate:#93A2B5;--slate-soft:#1B2531;
--blue:#579BDD;--blue-soft:#132738;
--teal:#3FBBA5;--teal-soft:#0D2A27;
--violet:#9A8DEC;--violet-soft:#1D1B33;
--amber:#D9A03F;--amber-soft:#2C2312;
--green:#3DB88E;--green-soft:#0C2721;
--red:#E4736F;--red-soft:#2C1615;
--faint:#75828F;--faint-soft:#182029;
--sh:0 1px 2px rgba(0,0,0,.4);--sh-2:0 2px 6px rgba(0,0,0,.45),0 12px 32px rgba(0,0,0,.4);
--ring:0 0 0 3px rgba(87,155,221,.22);
}}
*{box-sizing:border-box}
/* A <button> with no author `color` falls through to the UA default
   `buttontext`, which is BLACK — and black on this app's dark surfaces is
   unreadable. It bit four widgets at once (.rem in Coming up, .ag in Next 7
   days, .fr in the pipeline funnel, .hot in Hottest deals): in each, the child
   spans that DID set a colour looked fine while the ones that didn't rendered
   black, so it read as "some text is broken" rather than as one missing rule.
   Inheriting is the right default — every button that wants its own colour
   (.btn, .btn.primary, .nav button, .sidebtn, .iconbtn ...) sets one and still
   wins on specificity. Same failure the .iline/.costrow inputs had. */
button{color:inherit}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--ink);font:400 13.5px/1.5 "Instrument Sans",-apple-system,system-ui,sans-serif;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
h1,h2,h3{margin:0;font-weight:600;letter-spacing:-.014em}
a{color:var(--brand);text-decoration:none}
a:hover{color:var(--brand-ink);text-decoration:underline}
button{font:inherit}
[hidden]{display:none!important}
.tnum{font-variant-numeric:tabular-nums}
::selection{background:var(--brand-soft);color:var(--brand-ink)}
*::-webkit-scrollbar{width:10px;height:10px}
*::-webkit-scrollbar-thumb{background:var(--line-2);border-radius:8px;border:3px solid transparent;background-clip:content-box}
*::-webkit-scrollbar-track{background:transparent}

/* ---------- shell ---------- */
/* THE SHELL IS LOCKED. .app is a fixed-height box that never scrolls; the only
   scroller in the application is .scrollarea. 100dvh (not 100vh) so that
   iOS Safari's collapsing toolbars don't push the bottom of the app off-screen
   — the 100vh line before it is the fallback for browsers without dvh.
   grid-template-columns is transitioned because collapsing the nav animates it. */
.app{display:grid;grid-template-columns:236px minmax(0,1fr);height:100vh;height:100dvh;overflow:hidden;
  transition:grid-template-columns .18s ease,grid-template-rows .18s ease}
/* overflow:hidden on the column, scroll on the nav ONLY. The brand and the
   account card are anchored; the list between them is what gives. Without
   this the sidebar was simply taller than a 768px screen and the account card
   sat below the fold with nothing to scroll it into view. */
.side{background:var(--navy);color:#EDF2F7;display:flex;flex-direction:column;padding:18px 14px 14px;gap:16px;border-right:1px solid var(--navy-2);overflow:hidden}
.brand{display:flex;align-items:center;gap:11px;padding:0 6px}
.mark{width:34px;height:34px;border-radius:9px;flex:none;object-fit:cover;box-shadow:inset 0 1px 0 rgba(255,255,255,.18),0 1px 3px rgba(0,0,0,.35)}
.bt{display:flex;flex-direction:column;line-height:1.2}
.bt b{font-weight:600;font-size:14px;color:#F4F8FC;letter-spacing:-.01em}
.bt em{font-style:normal;font-size:11px;color:var(--navy-ink-2);letter-spacing:.01em}
/* A group heading is a button: the whole row opens and closes the group, and
   the caret turns. The first sits flush under the brand; every later one buys
   itself space above, which is what makes four groups read as four groups
   rather than one list with words in it. */
.navgrp+.navgrp{margin-top:11px}
.navlab{display:flex;align-items:center;gap:6px;width:100%;background:transparent;border:0;
  font:inherit;font-size:10px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;
  color:var(--navy-ink-2);padding:4px 8px;margin-bottom:4px;border-radius:7px;cursor:pointer}
.navlab:hover{color:#E4EBF3;background:rgba(255,255,255,.05)}
.navlab:focus-visible{outline:2px solid #7FB3E6;outline-offset:-2px}
.nlcaret{display:inline-flex;flex:none}
.nlcaret svg{width:13px;height:13px;transform:rotate(90deg);transition:transform .15s}
.navgrp.shut .nlcaret svg{transform:rotate(0deg)}
.nltext{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* A closed group still has to be able to say "something in here needs you". */
.nldot{margin-left:auto;width:6px;height:6px;border-radius:50%;flex:none;background:var(--navy-ink-2)}
.nldot.warn{background:var(--red)}
/* Amber: this group holds a badge that could not be read. Distinct from red
   (work waiting) and from plain (a count), because "unknown" is neither. */
.nldot.err{background:var(--amber)}
.nldot.here{background:#7FB3E6}
.navitems{display:flex;flex-direction:column;gap:2px}
.navgrp.shut .navitems{display:none}
/* Scrollable, but with no bar sitting there permanently: with the groups
   closable the list rarely needs to scroll at all, and a visible track beside
   fourteen items reads as clutter. The bar appears while the pointer is in the
   list, so it is never a surprise that it moves. */
.nav{display:flex;flex-direction:column;gap:2px;flex:1 1 auto;min-height:0;overflow-y:auto;
  overscroll-behavior:contain;scrollbar-width:none}
.nav::-webkit-scrollbar{width:6px}
.nav::-webkit-scrollbar-thumb{background:transparent;border-radius:3px}
.nav:hover::-webkit-scrollbar-thumb{background:rgba(255,255,255,.16)}
.nav::-webkit-scrollbar-track{background:transparent}
.nav button{display:flex;align-items:center;gap:10px;width:100%;text-align:left;background:transparent;border:0;color:var(--navy-ink);padding:7px 9px;border-radius:8px;cursor:pointer;font-size:13.5px;font-weight:500;letter-spacing:-.005em}
.nav button svg{width:16px;height:16px;flex:none;opacity:.75}
.nav button:hover{background:rgba(255,255,255,.06);color:#F2F6FA}
.nav button[aria-current="true"]{background:rgba(255,255,255,.11);color:#fff;font-weight:600}
.nav button[aria-current="true"] svg{opacity:1;color:#7FB3E6}
.nb{margin-left:auto;font-size:11px;font-weight:600;color:var(--navy-ink-2);font-variant-numeric:tabular-nums}
.nb.warn{background:var(--red);color:#fff;border-radius:999px;min-width:18px;text-align:center;padding:0 5px;line-height:17px}
/* "!" — the read failed, so this badge has no number to show. Same pill as
   .warn so it reads as a state and not as a count, amber so it never claims
   there is work when what we actually have is no answer. */
.nb.err{background:var(--amber);color:#2A1B03;border-radius:999px;min-width:18px;text-align:center;padding:0 5px;line-height:17px;cursor:help}
.sidefoot{margin-top:auto;flex:none;display:flex;flex-direction:column;gap:2px}
.sidebtn{display:flex;align-items:center;gap:10px;width:100%;background:transparent;border:0;color:var(--navy-ink);padding:7px 9px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500}
.sidebtn svg{width:16px;height:16px;opacity:.7}
.sidebtn:hover{background:rgba(255,255,255,.06);color:#F2F6FA}
.sidebtn[aria-current="true"]{background:rgba(255,255,255,.11);color:#fff;font-weight:600}
.sidebtn[aria-current="true"] svg{opacity:1;color:#7FB3E6}
a.sidebtn{text-decoration:none}
/* The account. The whole left side is one button into Settings — the gear is
   the affordance, not a fifth utility row — and sign out sits outside it so a
   stray click on the card can never sign you out. The avatar takes the same
   per-person hue the chat avatars use, so one person is one colour wherever
   they appear, and the save state rides on it as a corner dot. */
.userbox{margin-top:10px;padding:6px 6px 8px;border-radius:12px;background:rgba(255,255,255,.05);
  border:1px solid var(--navy-line);display:flex;flex-direction:column;gap:1px}
.uhead{flex:1;min-width:0;display:flex;align-items:center;gap:9px;background:transparent;border:0;
  padding:5px 6px;border-radius:9px;cursor:pointer;text-align:left;font:inherit;color:inherit}
.uhead:hover{background:rgba(255,255,255,.06)}
.uhead:focus-visible{outline:2px solid #7FB3E6;outline-offset:-2px}
.avwrap{position:relative;flex:none;display:block}
.av{width:32px;height:32px;border-radius:9px;display:grid;place-items:center;
  font-size:11.5px;font-weight:700;color:#fff;background:hsl(var(--h,210) 42% 42%);
  box-shadow:inset 0 1px 0 rgba(255,255,255,.18)}
/* Name over address, as one block beside the avatar. Both truncate rather than
   wrap: a card that changes height as somebody edits their display name makes
   the whole sidebar move. */
.uinfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.uhead b{display:block;min-width:0;font-weight:600;font-size:12.5px;color:#F2F6FA;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis;letter-spacing:-.005em}
#whoEmail{display:block;min-width:0;font-size:10.5px;color:var(--navy-ink-2);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ugear{flex:none;display:grid;place-items:center;color:var(--navy-ink-2)}
.ugear svg{width:15px;height:15px}
.uhead:hover .ugear{color:#E4EBF3}
.savechip{display:inline-flex;align-items:center;gap:6px;font-size:10.5px;color:var(--navy-ink-2);letter-spacing:.01em}
/* On the avatar, not under the name: "Live" as a third line of text was the
   least important thing in the card and the loudest. */
.userbox .savechip{position:absolute;right:-3px;bottom:-3px;padding:0}
.userbox .savechip i{box-shadow:0 0 0 2px var(--navy)}
.savechip i{width:6px;height:6px;border-radius:50%;background:var(--navy-ink-2)}
.savechip[data-s="saved"] i{background:#3DBF8E;box-shadow:0 0 0 3px rgba(61,191,142,.16)}
.savechip[data-s="saving"] i{background:#D9A03F}
.savechip[data-s="error"] i{background:#E4736F}
.iconbtn{margin-left:auto;flex:none;background:transparent;border:0;color:var(--navy-ink-2);cursor:pointer;padding:5px;border-radius:7px;display:grid;place-items:center}
.iconbtn svg{width:15px;height:15px}
.iconbtn:hover{background:rgba(255,255,255,.08);color:#fff}

.main{display:flex;flex-direction:column;min-width:0;overflow:hidden}
.top{display:flex;align-items:center;gap:14px;padding:0 24px;height:60px;flex:none;background:var(--surface);border-bottom:1px solid var(--line)}
.ttl{display:flex;align-items:baseline;gap:9px;min-width:0}
.ttl h1{font-size:17px;letter-spacing:-.02em}
.count{font-size:12.5px;color:var(--ink-3);white-space:nowrap;font-variant-numeric:tabular-nums;flex:0 1 auto}
.count b{color:var(--ink-2);font-weight:600}
.search{margin-left:14px;flex:1;max-width:340px;display:flex;align-items:center;gap:8px;background:var(--surface-3);border:1px solid transparent;border-radius:8px;padding:0 11px;height:34px}
.search:focus-within{background:var(--surface);border-color:var(--brand);box-shadow:var(--ring)}
.search svg{width:15px;height:15px;color:var(--ink-3);flex:none}
.search input{border:0;background:transparent;color:var(--ink);font:inherit;font-size:13px;width:100%;outline:none}
.search input::placeholder{color:var(--ink-3)}
.topacts{margin-left:auto;display:flex;align-items:center;gap:8px}
/* ---------- the bell ----------
   A popover anchored to the button, not a drawer: it is a glance, and the
   screen behind it is the context for what it says. */
.bellwrap{position:relative;flex:none}
.bellbtn{margin-left:0;width:34px;height:34px;border-radius:9px;color:var(--ink-3);position:relative}
.bellbtn:hover{background:var(--surface-2);color:var(--ink)}
.bellbtn[aria-expanded="true"]{background:var(--surface-2);color:var(--ink)}
.bellbtn svg{width:17px;height:17px}
.belldot{position:absolute;top:3px;right:3px;min-width:8px;height:8px;border-radius:999px;
  background:var(--red);box-shadow:0 0 0 2px var(--surface);font-size:9px;font-weight:700;
  line-height:8px;color:transparent}
/* Past one, the dot carries the number — but it stays a dot, so a busy morning
   does not become a badge the width of the button. */
.belldot:not(:empty){min-width:15px;height:15px;line-height:15px;top:1px;right:1px;
  color:#fff;padding:0 3px;text-align:center;font-variant-numeric:tabular-nums}
.bellpop{position:absolute;top:calc(100% + 8px);right:0;width:min(400px,92vw);max-height:min(560px,70vh);
  display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--line);
  border-radius:12px;box-shadow:var(--sh-2);z-index:44;overflow:hidden}
.bellhead{display:flex;align-items:center;gap:9px;padding:11px 13px;border-bottom:1px solid var(--line)}
.bellhead b{font-size:13.5px;font-weight:600}
.bellnew{font-size:11px;font-weight:700;color:#fff;background:var(--red);border-radius:999px;padding:1px 7px}
.bellhead .lnk2{margin-left:auto;font-size:12px}
.bellrows{overflow-y:auto;display:flex;flex-direction:column}
.bellempty{padding:18px 15px;font-size:12.5px;color:var(--ink-3);line-height:1.6}
.actrow{display:flex;align-items:flex-start;gap:10px;width:100%;text-align:left;background:none;
  border:0;border-bottom:1px solid var(--line);padding:11px 13px;cursor:pointer;font:inherit}
.actrow:last-child{border-bottom:0}
.actrow:hover{background:var(--surface-2)}
/* Unseen rows carry a rail rather than a background: the row is still a row,
   and the eye finds the edge without the list turning into stripes. */
.actrow.fresh{box-shadow:inset 2px 0 0 var(--brand)}
.actav{flex:none;width:26px;height:26px;border-radius:8px;display:grid;place-items:center;
  font-size:10px;font-weight:700;color:#fff;background:hsl(var(--h,210) 42% 42%)}
.actav.img{background-size:cover;background-position:center;color:transparent}
.actmid{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px}
.acttop{font-size:12.5px;color:var(--ink-2)}
.acttop b{color:var(--ink);font-weight:600}
.actwhat{font-size:12.5px;font-weight:500;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.actwhat em{font-style:normal;font-weight:400;color:var(--ink-3)}
/* The change itself when the server could summarise it ("county Morgan →
   Madison"), the bare field names when it could not. One line either way: a
   feed row is a headline, and anything that needs two lines needs the record
   instead. */
.actf{font-size:11px;color:var(--ink-4);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.actwhen{flex:none;font-size:11px;color:var(--ink-4);white-space:nowrap;padding-top:1px}
@media(max-width:760px){.bellpop{position:fixed;top:auto;right:8px;left:8px;width:auto;max-height:66vh}}

/* The panel-wide environment switcher. It lives in the header on every screen,
   including the ones that read no product instance, because a control that
   comes and goes is one you have to hunt for — and the question it answers
   ("which instance am I editing?") is never irrelevant. Every environment is
   coloured: red for prod, amber for stage, violet for dev. */
.topenv{display:flex;align-items:center;flex:none}
/* prod carries its warning whether or not it is the one you are on: the button
   you must not press by accident should look like it before you press it. */
.topenv [data-env="prod"]{color:var(--red)}
.topenv [data-env="prod"]:hover{color:var(--red)}
body[data-env="prod"] .topenv .envseg{border-color:var(--red)}
body[data-env="prod"] .topenv .envseg button.on{color:var(--red)}
body[data-env="prod"] #envName{color:var(--red)}
body[data-env="stage"] .topenv .envseg{border-color:var(--amber)}
body[data-env="stage"] .topenv .envseg button.on{color:var(--amber)}
body[data-env="dev"] .topenv .envseg{border-color:var(--violet)}
body[data-env="dev"] .topenv .envseg button.on{color:var(--violet)}
/* Same signal in the sidebar, under the product name. */
.brand .bt em{transition:color .12s}
body[data-env="stage"] #envName{color:var(--amber)}
body[data-env="dev"] #envName{color:var(--violet)}

/* IT IS NOT ON THE SCREENS IT DOES NOT MOVE.

   The switcher governs four things — VSI users, Issues, Uploads and the
   dashboard's "Your VSI access" card. Everything else in the panel lives on a
   single database that is none of the three product instances. But the control
   sat in the top bar on every screen, beside the page title, which reads as
   that page's scope: somebody went looking for three copies of an invoice that
   has only ever existed once, because the header said prod and the invoice was
   still there on dev.

   The first attempt at this DIMMED it instead of removing it, on the argument
   that you would otherwise have to go looking to find which instance you were
   on. That was wrong twice. A control faded to 42% reads as disabled, and this
   one still worked — so it explained nothing and invited the click it had just
   appeared to forbid. And the argument had already been answered by the other
   half of the same change: #envName says "VSI app · Production" on every
   screen, in the environment's colour, so the identity never depended on the
   switcher being present.

   paintEnvSwitch sets `hidden`. This rule is what makes that stick — the UA's
   [hidden]{display:none} is in the user-agent origin, so the author's
   .topenv{display:flex} above beats it no matter the specificity, and without
   this line the element stays on screen with hidden set. */
.topenv[hidden]{display:none}

/* Lives in the top bar, not in the sidebar, because it has to stay reachable
   when the sidebar it controls is gone. */
.navtoggle{flex:none;width:32px;height:32px;display:grid;place-items:center;cursor:pointer;
  background:transparent;border:1px solid var(--line);border-radius:8px;color:var(--ink-3);padding:0}
.navtoggle:hover{background:var(--surface-3);color:var(--ink);border-color:var(--line-2)}
.navtoggle:focus-visible{outline:0;box-shadow:var(--ring)}
.navtoggle svg{width:16px;height:16px}
.navtoggle .pnl{transition:transform .18s ease;transform-origin:center}
body.navhidden .navtoggle .pnl{transform:scaleX(-1)}
.scrollarea{--pt:20px;flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  padding:var(--pt) 24px 64px;display:flex;flex-direction:column;gap:16px}

/* ---------- buttons ---------- */
.btn{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:500;padding:0 13px;height:34px;border-radius:8px;border:1px solid var(--line-2);background:var(--surface);color:var(--ink);cursor:pointer;white-space:nowrap;transition:background .12s,border-color .12s}
.btn svg{width:15px;height:15px;opacity:.8}
.btn:hover{border-color:var(--ink-4);background:var(--surface-2)}
.btn:focus-visible{outline:none;box-shadow:var(--ring)}
.btn.primary{background:var(--brand);border-color:var(--brand);color:#fff}
.btn.primary:hover{background:var(--brand-ink);border-color:var(--brand-ink)}
.btn.ghost{background:transparent;border-color:transparent;color:var(--ink-2)}
.btn.ghost:hover{background:var(--surface-3);color:var(--ink)}
.btn.danger{color:var(--red);border-color:var(--line-2)}
.btn.danger:hover{border-color:var(--red);background:var(--red-soft)}
.btn.sm{height:29px;font-size:12.5px;padding:0 10px}
.btn .bts{display:none}

/* ---------- metrics ---------- */
.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
@media(max-width:1080px){.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}}
.kpi{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);padding:14px 16px 15px;box-shadow:var(--sh);display:flex;flex-direction:column;gap:3px;min-width:0}
.kpi .k{font-size:10.5px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-3)}
.kpi .v{font-size:27px;font-weight:600;letter-spacing:-.03em;line-height:1.15;font-variant-numeric:tabular-nums}
.kpi .v small{font-size:15px;font-weight:500;color:var(--ink-3);letter-spacing:-.01em}
.kpi .s{font-size:11.5px;color:var(--ink-3)}
.kpi .track{height:4px;border-radius:3px;background:var(--surface-3);overflow:hidden;margin:8px 0 6px}
.kpi .track b{display:block;height:100%;border-radius:3px;background:var(--brand)}
.kpi.accent{background:linear-gradient(155deg,#153A5E,#0C2036);border-color:#0A1D30;color:#EDF3F9}
.kpi.accent .k{color:#8FA9C2}.kpi.accent .s{color:#93AABF}
.kpi.accent .v{color:#fff}.kpi.accent .v small{color:#8FA9C2}
.kpi.accent .track{background:rgba(255,255,255,.14)}
.kpi.accent .track b{background:#5FA5E4}

/* ---------- pipeline bar ---------- */

/* ---------- toolbar ---------- */
.toolbar{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.chip{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;font-weight:500;height:30px;padding:0 11px;border-radius:999px;border:1px solid var(--line-2);background:var(--surface);color:var(--ink-2);cursor:pointer;white-space:nowrap}
.chip:hover{border-color:var(--ink-4);color:var(--ink)}
.chip[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:var(--surface)}
.chip i{width:7px;height:7px;border-radius:2px;flex:none}
.chip .n{font-variant-numeric:tabular-nums;color:var(--ink-3);font-weight:500}
.chip[aria-pressed="true"] .n{color:var(--surface);opacity:.7}
.tsep{width:1px;height:20px;background:var(--line);margin:0 4px}

/* ── The faceted filter bar (Leads + Pipeline) ────────────────────────────
   Each facet is one WELL: its name welded to its options, so the group wraps
   as a whole instead of spilling its last two options onto the next line —
   which is how PLATFORM used to begin on row one and finish on row two.
   Deliberately the same segmented language as .envseg in the header, because
   this app already has a control that means "pick one from this set".
   NOTE the class is .facet, not .fgrp: .fgrp is the Uploads/Issues groups. */
.facet{display:inline-flex;align-items:center;flex-wrap:wrap;gap:2px;padding:2px;
  max-width:100%;background:var(--surface-2);border:1px solid var(--line);border-radius:10px}
.facet-l{display:inline-flex;align-items:center;height:25px;padding:0 7px 0 6px;
  font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  color:var(--ink-4);white-space:nowrap;user-select:none}
.fopt{display:inline-flex;align-items:center;gap:5px;height:25px;padding:0 9px;
  border:0;border-radius:8px;background:none;font:inherit;font-size:12px;font-weight:500;
  color:var(--ink-2);cursor:pointer;white-space:nowrap;line-height:1}
.fopt:hover{background:var(--surface-3);color:var(--ink-2)}
.fopt i{width:7px;height:7px;border-radius:2px;flex:none;opacity:.8}
.fopt b{font-weight:600;font-size:11px;font-variant-numeric:tabular-nums;color:var(--ink-4)}
/* A count of nought is information, not an error — say it quietly. */
.fopt.zero b{opacity:.5}
/* Selected reads as a RAISED tile, exactly like the environment switcher, and
   takes the facet's own colour so which cut is on is answerable at a glance
   without reading the labels. --fc is set per option in renderToolbar. */
/* The facet's own colour, pulled toward the page's ink so it clears contrast
   in BOTH themes: raw --amber on a white tile measured 4.15:1, and the same
   token is far too light for white text in dark. The mix darkens it on light
   and lifts it on dark while the hue still says which facet this is. If a
   browser cannot do color-mix the declaration is dropped and the option keeps
   .fopt's own --ink-2, which is legible either way. */
.fopt[aria-pressed="true"]{background:var(--surface);color:var(--ink);
  font-weight:600;box-shadow:var(--sh)}
.fopt[aria-pressed="true"]{color:color-mix(in srgb, var(--fc, var(--ink)) 72%, var(--ink))}
.fopt[aria-pressed="true"] i{opacity:1}
.fopt[aria-pressed="true"] b{color:inherit;opacity:.75}
.fopt:focus-visible{outline:2px solid var(--brand);outline-offset:-2px}

/* Standalone toggles: neither is one choice out of a set, so neither sits in
   a well. Same height as a facet so the row still reads as one line. */
.ftog{display:inline-flex;align-items:center;gap:6px;height:31px;padding:0 11px;
  border:1px solid var(--line-2);border-radius:10px;background:var(--surface);
  color:var(--ink-2);font:inherit;font-size:12px;font-weight:500;cursor:pointer;white-space:nowrap}
.ftog:hover{border-color:var(--ink-4);color:var(--ink)}
.ftog svg{width:14px;height:14px;opacity:.7}
.ftog b{font-weight:600;font-size:11px;font-variant-numeric:tabular-nums;color:var(--ink-4)}
.ftog[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:var(--surface)}
.ftog[aria-pressed="true"] b{color:var(--surface);opacity:.7}
.ftog[aria-pressed="true"] svg{opacity:1}
.ftog:focus-visible{outline:2px solid var(--brand);outline-offset:1px}
/* Reset is an escape hatch, not a filter — no border until you reach for it. */
.ftog.reset{border-color:transparent;background:none;color:var(--brand);margin-left:auto}
.ftog.reset:hover{background:var(--brand-soft);border-color:transparent;color:var(--brand)}
.ftog.reset svg{opacity:1}
@media(max-width:900px){
  /* The label is the one part that can go: at this width the colour dots and
     the options themselves still say which facet is which. */
  .facet-l{padding:0 6px;font-size:9.5px}
  .fopt{padding:0 8px;gap:5px}
  .ftog.reset{margin-left:0}
}
.tlab{font-size:10.5px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-4);margin-right:2px}

/* ---------- pills ---------- */
.pill{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;font-weight:500;padding:2px 9px;border-radius:999px;white-space:nowrap;line-height:1.5;border:1px solid transparent}
.st-prospect{background:var(--slate-soft);color:var(--slate)}
.st-contacted{background:var(--blue-soft);color:var(--blue)}
.st-interested{background:var(--teal-soft);color:var(--teal)}
.st-demo{background:var(--violet-soft);color:var(--violet)}
.st-negotiation{background:var(--amber-soft);color:var(--amber)}
.st-won{background:var(--green-soft);color:var(--green)}
.st-lost{background:var(--red-soft);color:var(--red)}
.st-parked{background:var(--faint-soft);color:var(--faint)}
/* VSI is the one platform drawn as a SOLID chip rather than a soft one: the
   column is scanned for our own districts. Blue, not navy — navy resolves to
   #080E15 in dark, so the dashboard's "Platform in use" bar was a black smear
   on a black card. --blue is the logo's hue (#283C5A, ~216deg) at a lightness
   that reads on both themes. */
.pf-vsi{background:var(--blue);color:#fff}
.pf-fleetio{background:var(--red-soft);color:var(--red)}
.pf-paper{background:var(--slate-soft);color:var(--slate)}
.pf-other{background:var(--violet-soft);color:var(--violet)}
.pf-none{background:transparent;color:var(--ink-4);border-color:var(--line-2)}
.pf{background:var(--pcs,var(--slate-soft));color:var(--pc,var(--slate))}
/* A PAIR, on purpose. --blue lightens to #579BDD in dark, where white text on
   it is 3:1 — so dark gets a deep blue with light ink instead. The old rule had
   only the [data-theme] arm, which the sidebar toggle stamps; "system" theme
   removes that attribute, so an OS-dark browser fell through to the light rule.
   Keep the two in step. */
[data-theme="dark"] .pf-vsi{background:#1C4067;color:#D2E5F7}
@media (prefers-color-scheme:dark){:root:not([data-theme]) .pf-vsi{background:#1C4067;color:#D2E5F7}}
.unconf{opacity:.6}

/* ---------- settings: platforms ---------- */

/* Settings as one tile with a tab bar. Stacked cards put Google Calendar
   below a platform list that grows every time somebody adds a platform, and a
   setting's discoverability falls off with every row above it. */
.settile{padding:0;overflow:hidden}
/* A settings row: what it is on the left, the control on the right. Used by
   the three things that moved out of the sidebar footer. */
.sethead.sep{border-top:1px solid var(--line)}
.setrow{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:14px 20px 16px}
.setrowtext{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 200px}
.setrowtext b{font-size:13px;font-weight:600}
.setrowtext span{font-size:12px;color:var(--ink-3)}
.setrow .btn{flex:none}
.settabs{display:flex;gap:2px;padding:6px 6px 0;border-bottom:1px solid var(--line);
  overflow-x:auto;scrollbar-width:none}
.settabs::-webkit-scrollbar{display:none}
.settabs button{border:0;background:none;font:inherit;font-size:12.5px;font-weight:500;
  color:var(--ink-3);padding:9px 14px;cursor:pointer;white-space:nowrap;
  border-radius:var(--r-sm) var(--r-sm) 0 0;position:relative}
.settabs button:hover{color:var(--ink-2);background:var(--surface-2)}
/* The underline sits ON the container's border, so the active tab reads as
   joined to the pane rather than floating above it. */
.settabs button.on{color:var(--ink);font-weight:600}
.settabs button.on::after{content:"";position:absolute;left:10px;right:10px;bottom:-1px;
  height:2px;border-radius:2px 2px 0 0;background:var(--brand)}
.settabs button:focus-visible{outline:2px solid var(--brand);outline-offset:-2px}
/* No padding: every child here — .sethead, .platadd, .setnote, .gcalrow —
   already carries its own 20px gutter, which is what let these sections be
   full-bleed cards before. Adding pane padding would double every one. */
.setpane{padding:0}
/* The tab bar is the tile's top edge now, so the first section heading must
   not draw a second line right under it. */
.setpane>.sethead:first-child{border-top:0}
/* ...and the last note must not leave a rule hanging above the tile's edge. */
.setpane>*:last-child{border-bottom:0}
@media(max-width:640px){
  .settabs button{padding:9px 11px;font-size:12px}
}
.setcard{max-width:700px;padding:0}
.sethead{padding:18px 20px 15px;border-bottom:1px solid var(--line)}
.sethead h2{font-size:18px;font-weight:600}
.sethead p{margin:5px 0 0;color:var(--ink-3);font-size:13px;max-width:62ch}
.platlist{display:flex;flex-direction:column}
.platrow{display:flex;align-items:center;gap:13px;padding:11px 20px;border-bottom:1px solid var(--line);flex-wrap:wrap}
.platrow.deleting{background:var(--red-soft)}
.pf-sw{width:13px;height:13px;border-radius:4px;flex:none;box-shadow:inset 0 0 0 1px rgba(0,0,0,.1)}
/* The "core"/"locked" badge is a SIBLING of .pn-lock in the markup, not a child
   of it — so .pn-lock's inline-flex and gap:8px, written to sit a name beside
   its badge, never applied to the badge at all. Both were children of this
   column, and a flex item in a column stretches: the badge rendered as a
   full-width block on its own line, 453px wide on a desktop and 206px on a
   phone, for a word like "core".
   Row-wrap puts the name and its badge on one line the way the markup reads,
   and the district count keeps its own line by claiming a full basis. */
.platname{flex:1 1 150px;min-width:120px;display:flex;flex-wrap:wrap;align-items:center;gap:3px 8px}
.pn-lock{font-weight:600;font-size:14px}
.lk{font-size:9px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-4);background:var(--surface-3);border:1px solid var(--line);border-radius:4px;padding:1px 5px}
.platuse{flex:1 0 100%;font-size:11.5px;color:var(--ink-4)}
input.pn{font:inherit;font-size:14px;font-weight:500;color:var(--ink);background:var(--surface);border:1px solid transparent;border-radius:7px;padding:5px 9px;max-width:230px;width:100%}
input.pn:hover{border-color:var(--line-2)}
input.pn:focus{outline:none;border-color:var(--brand);box-shadow:var(--ring)}
.platclrs{display:flex;gap:5px;flex-wrap:wrap;flex:none}
.platclrs.muted{font-size:11px;color:var(--ink-4);align-self:center}
.clr{width:19px;height:19px;border-radius:6px;border:2px solid transparent;cursor:pointer;padding:0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
.clr:hover{transform:scale(1.12)}
.clr.on{border-color:var(--surface);box-shadow:0 0 0 2px var(--ink)}
.platord{display:flex;gap:3px;flex:none}
.ic{display:inline-flex;align-items:center;justify-content:center;width:29px;height:29px;border-radius:7px;border:1px solid var(--line);background:var(--surface);color:var(--ink-3);cursor:pointer;font-size:10px;line-height:1}
.ic:hover:not(:disabled):not(.dis){border-color:var(--ink-3);color:var(--ink)}
.ic:disabled,.ic.dis{opacity:.35;cursor:default}
.ic svg{width:15px;height:15px}
.platdel{flex:none}
.platdel .ic:hover{border-color:var(--red);color:var(--red)}
.del-actions{display:flex;align-items:center;gap:9px;flex-wrap:wrap;flex:1 1 auto;justify-content:flex-end}
.del-q{font-size:13px;color:var(--red);font-weight:500}
.del-sel{font:inherit;font-size:13px;padding:5px 8px;border-radius:7px;border:1px solid var(--line-2);background:var(--surface);color:var(--ink)}
.platadd{display:flex;align-items:center;gap:10px;padding:15px 20px;border-bottom:1px solid var(--line);flex-wrap:wrap}
.platadd input.pn.add{max-width:210px;border-color:var(--line-2)}
.setnote{padding:13px 20px;margin:0;font-size:12px;color:var(--ink-4)}
/* Google Calendar mirror — status row in Settings */
.gcalrow{display:flex;align-items:center;gap:13px;padding:14px 20px;border-bottom:1px solid var(--line);flex-wrap:wrap}
.gcaldot{width:9px;height:9px;border-radius:50%;flex:none;background:var(--slate)}
.gcaldot.ok{background:var(--green)}
.gcaldot.bad{background:var(--red)}
.gcaldot.idle{background:var(--amber)}
.gcalstat{flex:1 1 220px;min-width:180px;display:flex;flex-direction:column;gap:2px}
.gcalstat b{font-size:14px;font-weight:600}
.gcalstat span{font-size:11.5px;color:var(--ink-4)}
.btn.sm{font-size:13px;padding:7px 13px;display:inline-flex;align-items:center;gap:6px}
.btn.sm svg{width:15px;height:15px}
.btn.danger{background:var(--red);border-color:var(--red);color:#fff}
.btn.danger:hover{filter:brightness(1.06)}

/* ---------- uploads: processing queue ---------- */
.up-card{max-width:940px;padding:0}
.up-top{display:flex;align-items:center;gap:12px;padding:11px 18px;border-bottom:1px solid var(--line);flex-wrap:wrap}
.up-envs{display:flex;align-items:center;gap:7px}
.up-top [data-up-refresh],.up-top .chip.quiet{margin-left:auto;display:inline-flex;align-items:center;gap:6px}
.up-top [data-up-refresh] svg,.up-top .chip.quiet svg{width:14px;height:14px}
/* The refresh is a fallback, not the thing you came for — the list is live.
   It reads as available rather than offered. */
.chip.quiet{border-color:transparent;background:none;color:var(--ink-3)}
.chip.quiet:hover{background:var(--surface-2);color:var(--ink-2)}

/* Three states of ONE setting, so one control. Three pills of equal weight
   read as three separate switches. */
.envseg{display:inline-flex;gap:2px;padding:2px;background:var(--surface-2);
  border:1px solid var(--line);border-radius:9px}
.envseg button{border:0;background:none;font:inherit;font-size:12px;font-weight:600;
  color:var(--ink-3);padding:5px 13px;border-radius:7px;cursor:pointer;line-height:1.2}
.envseg button:hover{color:var(--ink-2)}
.envseg button.on{background:var(--surface);color:var(--ink);box-shadow:var(--sh)}
.envseg button:focus-visible{outline:2px solid var(--brand);outline-offset:-1px}

/* Groups, with air between them instead of hairline dividers. A row of chips
   split by 1px rules reads as a toolbar; this reads as three questions. */
.up-filters{display:flex;align-items:center;gap:8px 20px;padding:11px 18px;
  border-bottom:1px solid var(--line);flex-wrap:wrap}
.fgrp{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}

/* Sections and environment on one row. Both answer "which list am I looking
   at", and as two stacked rows they were most of the chrome above the data. */
/* The row's height comes from the tabs, which must sit flush with the bottom
   border for their active underline. That left the env switch pinned to the
   bottom too — 1px under the rule above it, which read as touching. The top
   padding is what gives it room; the tabs still reach the border. */
.issbar{display:flex;align-items:flex-end;justify-content:space-between;gap:22px;
  padding:10px 12px 0;border-bottom:1px solid var(--line);flex-wrap:wrap}
.issbar .scopetabs{border-bottom:0;padding:0;flex:1 1 auto;min-width:0}
.issbar .envseg{margin:0 0 9px}

/* Four axes as one control. Four chip rows read as four separate controls and
   cost roughly twice the height; a labelled cell per axis reads as one bar and
   answers "what is filtered" by which cells are tinted. */
.issfilters{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:14px 18px;
  border-bottom:1px solid var(--line)}
.fbar{display:inline-flex;align-items:stretch;background:var(--surface);
  border:1px solid var(--line);border-radius:9px;overflow:hidden;max-width:100%;flex-wrap:wrap}
.fcell{display:inline-flex;align-items:center;gap:7px;padding:0 13px;height:40px;cursor:pointer;
  border-right:1px solid var(--line)}
.fcell:last-child{border-right:0}
.fclab{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
  color:var(--ink-5);white-space:nowrap}
.fcell select{appearance:none;-webkit-appearance:none;background:transparent;border:0;
  color:var(--ink-2);font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;
  outline:none;padding-right:2px;max-width:150px}
/* Tinted only when it is actually narrowing something. */
.fcell.on{background:var(--brand-soft)}
.fcell.on select{color:var(--brand-ink)}
.fcell:focus-within{outline:2px solid var(--brand);outline-offset:-2px}
.lnk.clearf{font-size:12.5px;font-weight:600;color:var(--brand-ink)}
/* Wrapped, the right-hand dividers land mid-air and a half-empty last row
   reads as a broken control. On a phone the cells stack as full-width rows
   with the divider underneath, which is the same idea rotated. */
@media(max-width:640px){
  .issfilters{padding:12px 14px}
  .fbar{display:grid;grid-template-columns:1fr;width:100%}
  .fcell{border-right:0;border-bottom:1px solid var(--line);justify-content:space-between;height:42px}
  .fcell:last-child{border-bottom:0}
  .fcell select{max-width:none;text-align:right}
  .issbar{padding:0 10px}
  .issbar .envseg{margin-bottom:10px}
}

/* Issues is nine columns; Uploads is five. The shared 940px cap squeezed the
   issues table for no reason, so it gets its own width. */
.isscard{max-width:1180px}
/* A safety net, not a layout. The handoff asked for `min-width:1080px` on the
   inner wrapper so a narrow window would scroll rather than clip the rightmost
   columns — but measured against the real grid the premise does not hold. The
   title column is `minmax(0,1fr)` and ellipsises, so at 1000px AND at 760px
   every one of the nine columns is still on screen and nothing runs past the
   card's edge.
   What the fixed width DID do was trap the expanded panel, which lives inside
   this wrapper, at 1080px — so on any window under ~1180px you had to scroll
   sideways to reach Save. Left as `auto` with no floor: it catches real
   overflow and is inert the rest of the time. */
.issscroll{overflow-x:auto}

/* Sections, not filters. A tab bar rather than another row of chips, because
   "whose work is this" is a different question from "what kind is it", and
   putting them in one row of pills makes them look like the same question. */
.scopetabs{display:flex;gap:2px;padding:0 12px;border-bottom:1px solid var(--line);overflow-x:auto;
  scrollbar-width:none}
.scopetabs::-webkit-scrollbar{display:none}
.scopetabs button{border:0;background:none;font:inherit;font-size:12.5px;font-weight:500;
  color:var(--ink-3);padding:11px 12px 10px;cursor:pointer;position:relative;white-space:nowrap;
  display:inline-flex;align-items:center;gap:6px}
.scopetabs button:hover{color:var(--ink-2)}
.scopetabs button.on{color:var(--ink);font-weight:600}
.scopetabs button.on::after{content:"";position:absolute;left:10px;right:10px;bottom:-1px;
  height:2px;background:var(--brand);border-radius:2px}
.scopetabs button:focus-visible{outline:2px solid var(--brand);outline-offset:-2px}
.scopetabs .n{font-size:10.5px;font-weight:700;color:var(--ink-3);background:var(--surface-2);
  border-radius:999px;padding:1px 6px;font-variant-numeric:tabular-nums}
.scopetabs button.on .n{background:var(--brand-soft);color:var(--brand-ink)}
.up-list{display:flex;flex-direction:column}
.up-group{border-bottom:1px solid var(--line)}
.up-ghead{display:flex;align-items:baseline;gap:10px;padding:12px 18px 9px;background:var(--surface-2)}
.up-ghead b{font-size:14px;font-weight:600}
.up-gsub{font-size:11.5px;color:var(--ink-4)}
.up-gpend{margin-left:auto;font-size:11px;font-weight:600;color:var(--amber);background:var(--amber-soft);border-radius:999px;padding:2px 9px;align-self:center}
.up-req{border-top:1px solid var(--line)}
.up-rhead{display:grid;grid-template-columns:62px 48px 1fr auto auto 18px;align-items:center;gap:12px;padding:11px 18px;cursor:pointer}
.up-rhead:hover{background:var(--surface-2)}
.up-type{font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;padding:2px 0;border-radius:5px;text-align:center}
.up-fleet{background:var(--blue-soft);color:var(--blue)}
.up-parts{background:var(--violet-soft);color:var(--violet)}
.up-files{font-size:12px;color:var(--ink-3);display:inline-flex;align-items:center;gap:4px;white-space:nowrap;font-variant-numeric:tabular-nums}
.up-files svg{width:14px;height:14px;color:var(--ink-4)}
.up-by{font-size:13px;color:var(--ink-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.up-date{font-size:12px;color:var(--ink-4);white-space:nowrap;font-variant-numeric:tabular-nums}
.upst{font-size:11px;font-weight:600;padding:3px 10px;border-radius:999px;white-space:nowrap;
  border:1px solid transparent;text-align:center}
.up-pending{background:var(--amber-soft);color:var(--amber)}
.up-processing{background:var(--blue-soft);color:var(--blue)}
.up-complete{background:var(--green-soft);color:var(--green)}
.up-rejected{background:var(--red-soft);color:var(--red)}
.up-caret{color:var(--ink-4);display:inline-flex}
.up-caret svg{width:16px;height:16px;transform:rotate(90deg);transition:transform .15s}
.up-caret.open svg{transform:rotate(-90deg)}
.up-body{padding:2px 18px 16px;background:var(--surface-2)}
.up-filelist{display:flex;flex-direction:column;gap:6px;margin:10px 0}
.up-file{display:flex;align-items:center;gap:12px;background:var(--surface);border:1px solid var(--line);border-radius:8px;padding:8px 12px}
.up-fname{font-size:13px;font-weight:500;flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.up-fmeta{font-size:11.5px;color:var(--ink-4);white-space:nowrap}
.up-note{font-size:13px;color:var(--ink-2);background:var(--amber-soft);border:1px solid var(--amber);border-radius:8px;padding:9px 12px;margin:4px 0 10px}
.up-note b{color:var(--amber)}
.up-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.up-cmeta{font-size:11.5px;color:var(--ink-4)}
.up-reject{display:flex;align-items:center;gap:9px;flex-wrap:wrap}
.up-reject .nf{flex:1 1 240px;min-width:0}
.up-reject input.pn{max-width:none;border-color:var(--line-2)}
.btn.danger-o{background:transparent;border-color:var(--red);color:var(--red)}
.btn.danger-o:hover{background:var(--red-soft)}
@media(max-width:640px){
  .up-rhead{grid-template-columns:56px 1fr auto 16px;gap:9px}
  .up-files,.up-date{display:none}
}

/* ---------- issues: VSI ticket backlog ---------- */
/* The number leads the row. Fixed width and tabular figures so the column is
   a column — a district reads one of these out over the phone, and a ragged
   left edge is the thing that makes you scan instead of glance. */
.is-rhead{grid-template-columns:74px 58px 78px minmax(0,1fr) auto auto 22px auto 18px}
/* Who owns it, as a face rather than a name: the row already spends its width
   on the title, and a backlog is scanned for "which of these are mine". */
.tk-asg{display:grid;place-items:center}
.tk-asg.none{color:var(--ink-4);font-size:11px}
.tk-no{font-size:11.5px;font-weight:700;letter-spacing:.01em;color:var(--ink-2);
  font-variant-numeric:tabular-nums;white-space:nowrap}
.tk-no.pending{color:var(--ink-4);font-weight:500}
.tk-meta b{font-variant-numeric:tabular-nums;color:var(--ink-2)}
/* Filled soft for a real value, outlined for none — so an empty cell reads as
   empty rather than as another colour to decode. */
.tk-cat,.tk-prio{font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
  padding:3px 0;border-radius:6px;text-align:center;border:1px solid transparent}
.tk-cat.tk-none,.tk-prio.tk-none{border-color:var(--line-2)}
/* Sits inside .tk-title (see issueRowHTML) so the row's nine columns stay put.
   Quiet: it marks the exception, and the title is still what you read. */
.tk-type{display:inline-block;font-size:9.5px;font-weight:700;letter-spacing:.05em;
  text-transform:uppercase;padding:1px 5px;border-radius:4px;margin-right:6px;
  vertical-align:1px;background:var(--surface-3);color:var(--ink-3);border:1px solid var(--line-2)}
.tk-feature{background:var(--teal-soft);color:var(--teal)}
.tk-bug{background:var(--red-soft);color:var(--red)}
.tk-spike{background:var(--violet-soft);color:var(--violet)}
.tk-cat.tk-none,.tk-prio.tk-none{background:transparent;color:var(--ink-4)}
.tk-immediate{background:var(--red-soft);color:var(--red)}
.tk-high{background:var(--amber-soft);color:var(--amber)}
.tk-medium{background:var(--blue-soft);color:var(--blue)}
.tk-low{background:var(--slate-soft);color:var(--slate)}
.tk-casual{background:var(--faint-soft);color:var(--ink-4)}
.tk-title{font-size:13.5px;font-weight:600;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
/* New is the ABSENCE of a status, so it is outlined like the other empties.
   In progress and Waiting get their own colours — they were sharing with Open
   and with each other, which made three different states look like two. */
.upst.is-new{background:transparent;color:var(--ink-3);border-color:var(--line-2)}
.upst.is-open{background:var(--blue-soft);color:var(--blue)}
.upst.is-progress{background:var(--amber-soft);color:var(--amber)}
.upst.is-waiting{background:var(--violet-soft);color:var(--violet)}
.upst.is-resolved{background:var(--green-soft);color:var(--green)}
.upst.is-closed{background:var(--surface-3);color:var(--ink-4)}
.tk-fld{margin:0;display:flex;flex-direction:column;gap:6px}
.tk-flabel{font-size:10.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-4);margin-bottom:3px}
.tk-fval{font-size:13.5px;color:var(--ink-2);line-height:1.5}
.tk-img{font-size:13px}
.tk-meta{font-size:11.5px;color:var(--ink-5);margin:8px 0 12px}
/* Two bordered cards, not two columns: what the district said, and what we are
   going to do about it. The border is what makes a two-line Details field read
   as a finished card instead of a column that ran out of content. */
.tkpanel{display:grid;grid-template-columns:1.15fr 1fr;gap:16px;align-items:stretch;
  padding:18px;background:var(--bg)}
.tkcard{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);
  padding:18px 20px;display:flex;flex-direction:column;gap:16px;min-width:0}
/* Pinned to the card's bottom edge, so a short ticket does not strand it. */
.tkleft .tk-meta{margin:auto 0 0;padding-top:14px;border-top:1px solid var(--line)}
.tkleft .tk-fld+.tk-fld{padding-top:14px;border-top:1px solid var(--line)}
.tkf{display:flex;flex-direction:column;gap:6px;min-width:0}
/* ONE label style on this panel. The old mix of a shouted STATUS beside a
   capitalised Priority was the loudest thing in it. */
.tkl{font-size:10.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;
  color:var(--ink-3)}
.tkl em{font-style:normal;font-weight:500;letter-spacing:.02em;text-transform:none;
  color:var(--ink-5);margin-left:6px}
.tkpair{display:grid;grid-template-columns:1fr 1fr;gap:14px;padding-top:14px;
  border-top:1px solid var(--line)}
.tkmine{align-self:flex-end;margin-top:-8px}
.tkright .tk-actions{margin:auto 0 0;justify-content:flex-end;padding-top:14px;
  border-top:1px solid var(--line)}
@media(max-width:900px){
  .tkpanel{grid-template-columns:1fr}
}

/* ---------- a ticket's discussion ----------
   Full width under the two cards, in the same gutter and on the same surface
   as them, because it is the third panel of one record. It is also the only
   thing on this screen whose height is not ours to predict, which is the other
   reason it is not a column. */
.tkthread{padding:0 18px 18px;background:var(--bg)}
.thcard{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);
  padding:18px 20px;display:flex;flex-direction:column;gap:14px;min-width:0}
.thnote,.thempty{font-size:13px;color:var(--ink-3);line-height:1.5}
.thlist{display:flex;flex-direction:column;gap:16px}
.thmsg{display:flex;flex-direction:column;gap:6px;min-width:0}
/* Replies are indented once and never again — iOS's board is one level deep
   and a reply to a reply has nowhere to render there. */
.thkids{margin-left:38px;display:flex;flex-direction:column;gap:12px}
.thbub{display:flex;gap:10px;align-items:flex-start;min-width:0}
.thav{flex:none;width:28px;height:28px;border-radius:9px;display:grid;place-items:center;
  font-size:10.5px;font-weight:700;color:#fff;background:hsl(var(--h,210) 42% 44%)}
/* Ours reads as ours at a glance. An operator's first question about any line
   in a thread is whether VSI already answered it. */
.thbub.vsi .thav{background:var(--brand)}
.thbub.vsi .thb{background:var(--brand-soft);border-color:transparent}
.thb{min-width:0;flex:1;background:var(--surface-3);border:1px solid var(--line);
  border-radius:12px;padding:9px 13px 10px}
.thwho{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;margin-bottom:3px}
.thwho b{font-size:13px;font-weight:600;color:var(--ink)}
.thvsi{font-size:9.5px;font-weight:700;letter-spacing:.06em;color:var(--brand-ink);
  background:var(--surface);border-radius:4px;padding:1px 5px}
.thwhen{font-size:11px;color:var(--ink-5);font-variant-numeric:tabular-nums}
.thtxt{font-size:13.5px;color:var(--ink-2);line-height:1.5;overflow-wrap:anywhere}
.thactions{display:flex;gap:6px;margin-left:38px}
.thcompose{display:flex;flex-direction:column;gap:9px;align-items:flex-start}
.thcompose .btn{flex:none}
.thcompose .nf{width:100%}
@media(max-width:640px){
  /* At 375px a 38px indent leaves a reply bubble 250px wide before its own
     padding. The thread reads as a thread from the bubble tint alone. */
  .thkids,.thactions{margin-left:14px}
  .tkthread{padding:0 12px 14px}
  .thcard{padding:14px 14px 16px}
}
.tk-work{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-sm);padding:12px 14px;display:flex;flex-direction:column;gap:11px}
.tk-row{display:flex;align-items:center;gap:11px;flex-wrap:wrap}
.tk-row.col{flex-direction:column;align-items:stretch;gap:5px}
.tk-l{font-size:11.5px;font-weight:600;color:var(--ink-3);min-width:60px}
.tk-l em{font-weight:400;color:var(--ink-4);font-style:normal}
input.pn.tk-status{max-width:220px}
.tk-ta{width:100%;resize:vertical;font:inherit;font-size:13.5px;color:var(--ink);background:var(--surface);border:1px solid var(--line-2);border-radius:8px;padding:7px 9px}
.tk-ta:focus{outline:none;border-color:var(--brand);box-shadow:var(--ring)}
.tk-quick{display:flex;gap:5px;flex-wrap:wrap}
.chip.sm2{font-size:11.5px;padding:3px 9px}
.tk-actions{display:flex;gap:9px;margin-top:2px}
@media(max-width:640px){
  /* The status column was `auto`, so "Waiting on submitter" took 130px of a
     375px row and left the TITLE 70px — 15 characters of a 46-character
     sentence, on the one line that says what the ticket actually is. An
     upload row survives that squeeze because its middle column is a person's
     name; an issue row does not. So the title gets the whole line here and
     the status drops underneath it, where it reads as belonging to the title
     rather than competing with it. */
  .is-rhead{grid-template-columns:56px minmax(0,1fr) 16px;
    grid-template-areas:"kind title caret" "no stat asg"; row-gap:5px}
  .is-rhead .tk-no{grid-area:no}
  .is-rhead .tk-asg{grid-area:asg;justify-self:end}
  .is-rhead .tk-cat{grid-area:kind}
  .is-rhead .tk-title{grid-area:title}
  .is-rhead .upst{grid-area:stat;justify-self:start}
  .is-rhead .up-caret{grid-area:caret}
  /* Two lines, then ellipsis: at 280px and 13.5px almost every title fits on
     one, the longest needs two, and nothing turns a row into a paragraph. */
  .is-rhead .tk-title{white-space:normal;display:-webkit-box;-webkit-line-clamp:2;
    -webkit-box-orient:vertical;overflow:hidden}
  .is-rhead .tk-prio,.is-rhead .up-by,.is-rhead .up-date{display:none}
}
.estmark{color:var(--ink-4);font-size:10px;margin-left:4px;letter-spacing:.02em}

/* pill-shaped inline select */
.pillsel{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:152px;padding:2px 7px 2px 10px;border-radius:999px;font-size:11.5px;font-weight:500;line-height:1.55;border:1px solid transparent}
.pillsel .pt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pillsel .cv{width:11px;height:11px;flex:none;opacity:.5}
.pillsel select{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer;border:0;font:inherit}
.pillsel:hover{filter:brightness(.96)}
.pillsel:focus-within{box-shadow:var(--ring)}
.pillsel select option{background:var(--surface);color:var(--ink)}

/* ---------- cards (shared surface) ---------- */
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--sh);overflow:hidden}

/* ---------- leads table ----------
   ONE scroll only: the page (.scrollarea) scrolls. The table is NOT a scroll
   container on either axis — no nested scroll-in-scroll. A fluid fixed layout
   keeps every column inside the content width (no horizontal scroll), and the
   column header sticks to the page scroll so it stays legible down a long list. */
.tablecard{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--sh)}
.tablewrap{overflow:visible}
.leadtable{border-collapse:separate;border-spacing:0;width:100%;table-layout:fixed;font-size:13px}
/* column widths (fixed layout — sum to 100) */
.leadtable .c-status{width:11.5%}
.leadtable .c-platform{width:10%}
.leadtable .c-district{width:17%}
.leadtable .c-sup{width:15%}
.leadtable .c-phone{width:11%}
.leadtable .c-addr{width:18.5%}
.leadtable .c-fleet{width:8%}
.leadtable .c-drive{width:7%}
.leadtable .c-go{width:2%}
/* contract column set (fixed layout — sums to 100). A SECOND set of 8, not a
   9th column: the table can never grow past its container (.tablewrap is
   overflow:visible by design), so a new column can only take pixels from an
   existing one. Toggling the set costs zero pixels. */
.leadtable.cs-contract .c-cstat{width:11.5%}
.leadtable.cs-contract .c-vstat{width:11.5%}
.leadtable.cs-contract .c-district{width:20%}
.leadtable.cs-contract .c-cval{width:10.5%}
/* Renews, Issued, Due and Paid are written-out dates; each of these columns is
   wide enough for the longest one, "September 30th, 2026", at 1440px. */
.leadtable.cs-contract .c-cend{width:15.5%}
.leadtable.cs-contract .c-cbus{width:6%}
.leadtable.cs-contract .c-sup{width:16%}
.leadtable.cs-contract .c-fleet{width:7%}
.leadtable thead th{position:sticky;top:calc(var(--pt,20px)*-1);z-index:3;background:var(--surface);text-align:left;font-weight:600;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-3);padding:0 14px;height:40px;border-bottom:1px solid var(--line);box-shadow:0 1px 0 var(--line),0 6px 10px -8px rgba(13,24,38,.14);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;user-select:none}
.leadtable thead th:first-child{border-top-left-radius:var(--r)}
.leadtable thead th.c-go{border-top-right-radius:var(--r)}
.leadtable thead th:hover{color:var(--ink)}
.leadtable thead th.no{cursor:default}
.leadtable thead th .ar{color:var(--brand);font-size:9px;margin-left:3px}
.leadtable tbody td{padding:11px 14px;border-bottom:1px solid var(--line);vertical-align:middle}
.leadtable tbody tr:last-child td{border-bottom:0}
.leadtable tbody tr:last-child td:first-child{border-bottom-left-radius:var(--r)}
.leadtable tbody tr:last-child td:last-child{border-bottom-right-radius:var(--r)}
.leadtable tbody tr{cursor:pointer}
.leadtable tbody tr:hover td{background:var(--surface-2)}
.leadtable tbody tr.state td{background:var(--brand-soft)}
.leadtable tbody tr.state:hover td{background:var(--brand-soft);filter:brightness(.985)}
.nm{font-weight:600;letter-spacing:-.008em;display:flex;align-items:center;gap:7px;flex-wrap:wrap;row-gap:3px}
.tybadge{font-size:9.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-4);border:1px solid var(--line-2);border-radius:4px;padding:0 4px;line-height:15px;flex:none}
.sub{font-size:11.5px;color:var(--ink-3);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
td.addr .a{color:var(--ink-2);font-size:12.5px;word-break:break-word}
.mp{margin-left:5px;color:var(--ink-4);display:inline-flex;vertical-align:-2px}
.mp svg{width:13px;height:13px}
.mp:hover{color:var(--brand)}
.dash{color:var(--ink-4)}
.drive b{font-weight:600;font-variant-numeric:tabular-nums}
.drive small{display:block;color:var(--ink-4);font-size:10.5px;font-variant-numeric:tabular-nums}
.phone a{color:var(--ink-2);font-variant-numeric:tabular-nums;font-size:12.5px}
.rowgo{color:var(--ink-4);opacity:0;display:inline-flex;transition:opacity .12s}
.rowgo svg{width:15px;height:15px}
.leadtable tbody tr:hover .rowgo{opacity:1}
.empty{padding:56px 20px;text-align:center;color:var(--ink-3);font-size:13px}
/* An empty screen is a thing to design. The old one-liner in a 56px-padded box
   left a card that was mostly nothing and said almost nothing with it. */
.emptyx{display:flex;flex-direction:column;align-items:center;text-align:center;
  gap:9px;padding:52px 24px 56px}
/* The border is doing the work in LIGHT mode, where --surface-2 sits a couple
   of percent off the card behind it and the badge all but disappears. */
.emptyx .exic{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;
  background:var(--surface-2);border:1px solid var(--line);color:var(--ink-4);margin-bottom:3px}
.emptyx .exic svg{width:19px;height:19px}
.emptyx h3{font-size:15px;font-weight:600;color:var(--ink);margin:0;letter-spacing:-.005em}
.emptyx p{margin:0;font-size:13px;line-height:1.6;color:var(--ink-3);max-width:46ch}
.emptyx p b{font-weight:600;color:var(--ink-2)}
.emptyx .btn{margin-top:5px}
.loading{padding:70px 20px;text-align:center;color:var(--ink-3)}

/* Tablet: drop the lowest-priority columns so the table keeps fitting the
   content width (still one page scroll, no horizontal scroll). Hidden fields
   remain one tap away in the row drawer. */
@media (max-width:1180px){ .leadtable .c-addr{display:none} .leadtable.cs-contract .c-sup{display:none} }
@media (max-width:1040px){ .leadtable .c-drive{display:none} .leadtable.cs-contract .c-fleet{display:none} }

/* Phone: the table restyles into stacked cards — a spreadsheet doesn't belong
   on a 390px screen. Same DOM, no thead, one card per lead. */
@media (max-width:640px){
  .tablecard{border:0;background:transparent;box-shadow:none}
  .leadtable{table-layout:auto}
  /* EVERY column class must be here. One left out keeps its percentage under
     table-layout:auto and distorts the whole card grid. */
  .leadtable .c-status,.leadtable .c-platform,.leadtable .c-district,.leadtable .c-sup,
  .leadtable .c-phone,.leadtable .c-addr,.leadtable .c-fleet,.leadtable .c-drive,
  .leadtable .c-cstat,.leadtable .c-vstat,.leadtable .c-cval,.leadtable .c-cend,
  .leadtable .c-cbus,
  /* The .cs-contract widths are (0,3,0); a (0,2,0) reset LOSES to them and the
     card grid collapses into percentage-width slivers. Match the specificity. */
  .leadtable.cs-contract .c-cstat,.leadtable.cs-contract .c-vstat,
  .leadtable.cs-contract .c-district,.leadtable.cs-contract .c-cval,
  .leadtable.cs-contract .c-cend,.leadtable.cs-contract .c-cbus,
  .leadtable.cs-contract .c-sup,.leadtable.cs-contract .c-fleet{width:auto}
  .leadtable thead{display:none}
  .leadtable tbody{display:flex;flex-direction:column;gap:10px}
  .leadtable tbody tr{display:grid;grid-template-columns:1fr 1fr;gap:11px 12px;align-items:start;
    background:var(--surface);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--sh);padding:14px 15px}
  .leadtable tbody tr:hover td{background:transparent}
  .leadtable tbody tr.state{border-color:var(--brand);box-shadow:inset 0 0 0 1px var(--brand),var(--sh)}
  .leadtable tbody tr.state td{background:transparent}
  .leadtable tbody td{padding:0;border:0!important;border-radius:0}
  .leadtable td.c-district{grid-column:1/-1;order:1}
  .leadtable td.c-district .nm{font-size:16px}
  .leadtable td.c-status{order:2}
  .leadtable td.c-platform{order:3}
  .leadtable td.c-sup{order:4;min-width:0}
  .leadtable td.c-phone{order:5}
  .leadtable td.c-addr{grid-column:1/-1;order:6;display:block}
  .leadtable td.c-fleet{order:7;display:table-cell}
  .leadtable td.c-drive{order:8;display:table-cell}
  /* Every new class needs an explicit order — the default 0 would render the
     cell ABOVE the card title (c-district is order:1) — and anything a tablet
     tier hid must be explicitly restored: the phone card deliberately shows
     MORE than the tablet table. */
  .leadtable td.c-cstat{order:2}
  .leadtable td.c-vstat{order:3}
  .leadtable td.c-cval{order:4}
  .leadtable td.c-cend{order:5}
  .leadtable td.c-cbus{order:7;display:table-cell}
  .leadtable.cs-contract td.c-sup{order:8;display:table-cell;min-width:0}
  .leadtable.cs-contract td.c-fleet{order:9;display:table-cell}
  .leadtable td.c-go{display:none}
  .leadtable td.c-status .pillsel,.leadtable td.c-platform .pillsel{width:100%;max-width:none}
  .leadtable td.c-cstat .mpill,.leadtable td.c-vstat .mpill{width:100%;text-align:center}
  .leadtable td.c-sup .sub{max-width:none}
  .leadtable td[data-label]::before{content:attr(data-label);display:block;font-size:10px;font-weight:600;
    letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);margin-bottom:3px}
}

/* ---------- kanban ---------- */
.kanban{display:flex;gap:14px;overflow-x:auto;align-items:flex-start;padding-bottom:10px}
.kcol{flex:0 0 262px;display:flex;flex-direction:column;gap:8px}
.khead{display:flex;align-items:center;gap:8px;padding:0 3px 8px;border-bottom:2px solid var(--line)}
.khead i{width:7px;height:7px;border-radius:2px}
.khead .kt{font-size:12px;font-weight:600;letter-spacing:.02em}
.khead .kc{margin-left:auto;font-size:11.5px;font-weight:600;color:var(--ink-3);font-variant-numeric:tabular-nums}
.kcol.dragover .khead{border-bottom-color:var(--brand)}
.kcol.dragover .kbody{background:var(--brand-soft);border-radius:var(--r-sm)}
.kbody{display:flex;flex-direction:column;gap:8px;min-height:60px;padding:2px}
.kcard{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-sm);padding:10px 12px;box-shadow:var(--sh);cursor:grab}
.kcard:hover{border-color:var(--line-2);box-shadow:var(--sh-2)}
.kcard.dragging{opacity:.35}
.kcard .kn{font-weight:600;font-size:13px;letter-spacing:-.008em;margin-bottom:7px}
.kmeta{display:flex;flex-wrap:wrap;gap:6px;align-items:center;font-size:11.5px;color:var(--ink-3)}
.ksup{margin-top:7px;padding-top:7px;border-top:1px solid var(--line);font-size:11.5px;color:var(--ink-2)}
.kempty{font-size:11.5px;color:var(--ink-4);padding:12px 3px}
/* Desktop reads the whole sentence; a touch device gets only the half that is
   true for it, via the :after swap below. */
.kanhint{font-size:11.5px;color:var(--ink-4);margin:0 0 10px;padding:0 2px}
@media(hover:none),(pointer:coarse){
  .kanhint{font-size:0}
  .kanhint:after{content:"Tap a card to open it and change its stage";font-size:11.5px}
}

/* Pipeline on a phone. The board already scrolls horizontally — that is the
   right shape for a kanban and it stays — but a fixed 262px column on a 375px
   screen leaves a 40px sliver of the next stage and no sense of paging. The
   column is sized to the viewport and snapped, so one swipe is exactly one
   stage.

   Dragging is NOT available here and cannot be: the board moves cards with the
   HTML5 drag events (dragstart/dragover/drop in the #viewbody handlers), and
   those never fire from a touch. The card is still a tap target that opens the
   district drawer, where the Status select does the same job — so the hint
   under the header says that rather than leaving a dead gesture to discover.
   `cursor:grab` is removed for the same reason: it advertises an interaction
   this device does not have. */
@media(hover:none),(pointer:coarse){
  .kcard{cursor:pointer}
}
@media (max-width:640px){
  .kanban{gap:10px;scroll-snap-type:x mandatory;scroll-padding-left:2px;
          -webkit-overflow-scrolling:touch}
  .kcol{flex:0 0 min(280px,84vw);scroll-snap-align:start}
  /* The header is the only thing naming the stage once a column fills the
     screen, so it stays put while the cards scroll under it. */
  .khead{position:sticky;top:0;z-index:1;background:var(--bg);padding-top:2px}
  .kcard{padding:12px 13px}
  .kcard .kn{font-size:14px}
}

/* ---------- reminders ---------- */
.remind{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);padding:13px 16px 14px;box-shadow:var(--sh)}
.remind-h{display:flex;align-items:center;gap:8px;margin-bottom:10px}
.remind-h svg{width:15px;height:15px;color:var(--ink-3)}
.remind-h h2{font-size:12.5px;font-weight:600}
.remind-h span{font-size:11.5px;color:var(--ink-3)}
.remind-list{display:flex;gap:9px;overflow-x:auto;padding-bottom:2px}
.rem{flex:0 0 auto;display:flex;flex-direction:column;gap:2px;align-items:flex-start;text-align:left;cursor:pointer;border:1px solid var(--line);border-left:3px solid var(--blue);background:var(--surface-2);border-radius:var(--r-sm);padding:8px 13px;min-width:158px;max-width:250px}
.rem:hover{border-color:var(--line-2);border-left-color:var(--blue);background:var(--surface)}
.rem.overdue{border-left-color:var(--red)}.rem.today{border-left-color:var(--amber)}
.rem .w{display:flex;align-items:center;gap:5px;font-size:10.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--blue)}
.rem .w svg{width:12px;height:12px;flex:none}
.rem.overdue .w{color:var(--red)}.rem.today .w{color:var(--amber)}
.rem .n{font-weight:600;font-size:13px;letter-spacing:-.008em}
.rem .r{font-size:11.5px;color:var(--ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:222px}

/* ---------- calendar ---------- */
.cal{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--sh);padding:14px 16px 18px}
.cal-top{display:flex;align-items:center;gap:10px;margin-bottom:14px}
.cal-title{font-size:15px;font-weight:600;letter-spacing:-.015em;min-width:158px}
.cal-nav{display:inline-flex;border:1px solid var(--line-2);border-radius:8px;overflow:hidden}
.cal-nav button{width:30px;height:30px;display:grid;place-items:center;background:var(--surface);border:0;color:var(--ink-2);cursor:pointer}
.cal-nav button+button{border-left:1px solid var(--line-2)}
.cal-nav button:hover{background:var(--surface-3);color:var(--ink)}
.cal-nav svg{width:14px;height:14px}
.cal-hint{margin-left:auto;font-size:11.5px;color:var(--ink-4)}
.cal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr))}
.cal-dow{font-size:10px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-4);padding:0 0 8px 2px}
.cal-body{border:1px solid var(--line);border-radius:var(--r-sm);overflow:hidden}
.cal-cell{min-height:104px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);padding:6px 7px;cursor:pointer;overflow:hidden;background:var(--surface)}
.cal-cell:nth-child(7n){border-right:0}
.cal-cell.out{background:var(--surface-2);cursor:default}
.cal-cell:hover:not(.out){background:var(--surface-2)}
.cal-num{font-size:11.5px;font-weight:500;color:var(--ink-3);font-variant-numeric:tabular-nums;margin-bottom:4px}
.cal-cell.today .cal-num{color:#fff;background:var(--brand);border-radius:50%;width:20px;height:20px;display:grid;place-items:center;font-weight:600;margin-bottom:2px}
.cal-trips{display:flex;flex-direction:column;gap:3px}
.cal-trip{display:flex;align-items:center;gap:4px;font-size:10.5px;line-height:1.35;padding:2px 6px;border-radius:4px;white-space:nowrap;overflow:hidden;background:var(--amber-soft);color:var(--amber)}
.cal-trip.done{background:var(--green-soft);color:var(--green)}
/* A MEETING IS NOT A FIELD VISIT. Amber on this calendar means "somebody is
   driving somewhere"; a video call with the team is a different kind of
   commitment and gets the brand colour, so a month with both in it says which
   is which before you read a word of it. Held ones go green with the rest. */
.cal-trip.k-meet{background:var(--brand-soft);color:var(--brand-ink)}
.cal-trip.k-meet.done{background:var(--green-soft);color:var(--green)}
/* The chip keeps its status colour; the leading glyph is the only thing that
   says visit or call, so a filled month still reads at a glance. */
.cal-trip .ck{display:inline-flex;flex:none}
.cal-trip .ck svg{width:10px;height:10px}
/* The district name is the only thing here that may be cut. `>:last-child`
   used to carry the ellipsis, and the last ELEMENT child is the <b> — the
   district name is a bare text node — so in a narrow cell the chip clipped
   the CLOCK and left the name to run off the edge unmarked. "1:00 PM" came
   out as "1:00 …", which is worse than no time at all: it hides the half of
   the day it happens in. The time and the glyph are fixed; the name shrinks. */
.cal-trip .ck,.cal-trip b{flex:none}
.cal-trip .cn{min-width:0;overflow:hidden;text-overflow:ellipsis}
.cal-trip b{font-weight:600;font-variant-numeric:tabular-nums}

/* Dots and the day panel are phone-only; above the breakpoint the grid's own
   chips are readable and nothing here applies. */
.cal-dots,.cal-day{display:none}

/* ---------- calendar: the phone month ----------
   Measured at 375px the cell is 43px wide and the entry chip needs 88px, so it
   rendered ~2 characters. Text does not fit and cannot be made to, so the cell
   carries DOTS and the reading moves to .cal-day below the grid. Cell height
   drops with it: 104px existed to stack three or four chips, and a row of dots
   needs a fifth of that — which is what takes the month from 520px of mostly
   empty grid to something you can see under the header. */
@media (max-width:640px){
  .cal{padding:12px 12px 14px}
  .cal-top{flex-wrap:wrap;gap:8px;margin-bottom:10px}
  .cal-title{min-width:0;flex:1 1 auto;font-size:16px}
  /* 48px wide and 104px tall, wrapped into a column, this ONE span was what
     made the header taller than three rows of the month. The instruction it
     carries is also no longer true here — a phone taps a day to open it, not
     to schedule — and the panel below states its own affordances. */
  .cal-hint{display:none}
  .cal-nav button{width:38px;height:38px}
  .cal-dow{font-size:0;letter-spacing:0;text-align:center;padding:0 0 6px}
  .cal-dow:after{content:attr(data-d);font-size:9.5px;font-weight:600;letter-spacing:.06em}

  .cal-cell{min-height:52px;padding:5px 2px 4px;display:flex;flex-direction:column;align-items:center;gap:3px}
  .cal-num{font-size:12.5px;margin-bottom:0}
  .cal-cell.today .cal-num{width:22px;height:22px}
  /* The chips stay in the DOM — same markup at every width — and are simply
     not shown at this one. */
  .cal-trips{display:none}
  .cal-dots{display:flex;align-items:center;justify-content:center;gap:3px;min-height:7px;flex-wrap:wrap}
  .cal-dot{width:6px;height:6px;border-radius:50%;background:var(--amber)}
  .cal-dot.done{background:var(--green)}
  /* A call has no drive attached to it, so it reads as a ring rather than a
     filled dot — the same visit/call distinction the chip makes with its icon,
     which is the one thing a 6px dot still has room to say. */
  .cal-dot.k-call{background:transparent;box-shadow:inset 0 0 0 1.5px var(--amber)}
  .cal-dot.k-meet{background:var(--brand)}
  .cal-dot.k-meet.done{background:var(--green)}
  .cal-dot.k-call.done{background:transparent;box-shadow:inset 0 0 0 1.5px var(--green)}
  .cal-more{font-size:8.5px;font-style:normal;color:var(--ink-4);line-height:1;font-variant-numeric:tabular-nums}
  .cal-cell.sel{background:var(--brand-soft);box-shadow:inset 0 0 0 1px var(--brand)}

  .cal-day{display:block;margin-top:12px}
  .cal-dhead{display:flex;align-items:center;gap:10px;margin-bottom:9px}
  .cal-dhead b{display:block;font-size:14px;font-weight:600;letter-spacing:-.01em}
  .cal-dhead span{display:block;font-size:11.5px;color:var(--ink-3);margin-top:1px}
  .cal-dhead .btn{margin-left:auto;flex:none}
  .cal-dempty{font-size:12.5px;color:var(--ink-4);text-align:center;padding:14px 8px 4px}
  .cal-drows{display:flex;flex-direction:column;gap:7px}
  /* The row is the [data-trip] the chip used to be: it is the only way to open
     an entry once the cell is a dot. */
  .cal-drow{display:flex;align-items:center;gap:10px;width:100%;text-align:left;cursor:pointer;
    background:var(--surface-2);border:1px solid var(--line);border-left:3px solid var(--amber);
    border-radius:var(--r-sm);padding:10px 12px;min-height:44px;font:inherit;color:var(--ink)}
  .cal-drow.done{border-left-color:var(--green)}
  .cal-drow.k-meet{border-left-color:var(--brand)}
  .cal-drow.k-meet.done{border-left-color:var(--green)}
  .cal-dk{display:inline-flex;flex:none;color:var(--ink-4)}
  .cal-dk svg{width:14px;height:14px}
  .cal-dmain{min-width:0;flex:1 1 auto}
  .cal-dmain b{display:block;font-size:13.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .cal-dmain em{display:block;font-style:normal;font-size:11.5px;color:var(--ink-3);margin-top:1px;
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .cal-dtime{flex:none;text-align:right;font-size:12.5px;font-weight:600;font-variant-numeric:tabular-nums}
  .cal-dtime i{display:block;font-style:normal;font-size:9.5px;font-weight:600;letter-spacing:.06em;
    text-transform:uppercase;color:var(--ink-4);margin-top:1px}
  .cal-drow.done .cal-dtime i{color:var(--green)}
}

/* ---------- overlays ---------- */
.scrim{position:fixed;inset:0;background:rgba(10,20,32,.42);backdrop-filter:blur(1px);opacity:0;pointer-events:none;transition:opacity .16s;z-index:40}
#tmscrim,#mmscrim{z-index:42}
.scrim.open{opacity:1;pointer-events:auto}
.drawer{position:fixed;top:0;right:0;height:100%;width:min(500px,96vw);background:var(--surface);border-left:1px solid var(--line);box-shadow:-12px 0 40px rgba(10,20,32,.16);transform:translateX(100%);transition:transform .2s cubic-bezier(.32,.72,0,1);z-index:41;display:flex;flex-direction:column}
.drawer.open{transform:none}
.dhead{padding:18px 22px 15px;border-bottom:1px solid var(--line);display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex:none}
.dhead h2{font-size:19px;letter-spacing:-.022em}
.dhead .dty{font-size:11.5px;color:var(--ink-3);margin-top:3px}
.dclose{background:transparent;border:0;color:var(--ink-3);cursor:pointer;padding:5px;border-radius:7px;display:grid;place-items:center}
.dclose svg{width:16px;height:16px}
.dclose:hover{background:var(--surface-3);color:var(--ink)}
/* Touch: 16px glyph + 5px padding is a 26px box, and the drawer header's
   align-stretch made it 22px WIDE. It is the control every editor session
   ends on. */
@media(hover:none),(pointer:coarse){
  /* flex:none matters more than the size here — .dhead is a flex row and the
     default flex-shrink:1 let a long record title squeeze the button down to
     22px wide however big it was told to be. */
  .dclose{flex:none;width:40px;height:40px;padding:0}
}
.dbody{padding:18px 22px 24px;overflow-y:auto;flex:1}
/* WRAPS. A draft invoice's footer now carries six actions (save, issue, issue
   and send, print, delete, close) and a sent one five; without this the last
   of them sat outside the modal, clipped and unclickable, which is the one
   failure mode a button row must not have. */
.dfoot{padding:13px 22px;border-top:1px solid var(--line);display:flex;flex-wrap:wrap;gap:9px;align-items:center;flex:none;background:var(--surface-2)}
.seclab{font-size:10.5px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-4);margin:22px 0 11px;display:flex;align-items:center;gap:9px}
.seclab:first-child{margin-top:0}
.seclab::after{content:"";flex:1;height:1px;background:var(--line)}
.seclab.bare::after{display:none}
.seclab.bare{flex-wrap:wrap;gap:8px}
.seclab.bare .btn.sm{flex:none}
.seclab .rule{flex:1;height:1px;background:var(--line)}
.fld{margin-bottom:13px}
.fld label{display:block;font-size:11.5px;font-weight:500;color:var(--ink-2);margin-bottom:5px}
.fld input,.fld select,.fld textarea,
.iline input,.costrow input{width:100%;font:inherit;font-size:13px;background:var(--surface);border:1px solid var(--line-2);border-radius:var(--r-sm);padding:8px 10px;color:var(--ink);transition:border-color .12s,box-shadow .12s}
/* Every control on a row has to be the same height, and left alone they are
   not: a <select> computed 36px, a text input 37 and a date input 39, because
   each type sizes to its own content. Status sitting 3px shorter than the Date
   beside it is small and it is the kind of small that makes a form look
   unfinished. Fixed rather than min-height, or the date input keeps its 39. */
.fld input:not([type=checkbox]):not([type=radio]),.fld select{height:38px}
/* The little calendar/clock glyph is the only thing that opens the native
   picker, and at its default size it is a ~16px target with no hover state
   and no pointer cursor — it does not look like a control. Padding enlarges
   the hit box without moving the field, and the negative margin takes back
   the space so the glyph stays where it was. color-scheme (declared at the
   top of this file) is what makes it light on the dark theme; before that it
   was a dark glyph on a dark field. */
.fld input[type=date]::-webkit-calendar-picker-indicator,
.fld input[type=time]::-webkit-calendar-picker-indicator,
.fld input[type=month]::-webkit-calendar-picker-indicator{
  cursor:pointer;opacity:.5;padding:6px;margin:-6px -4px -6px 0;border-radius:6px}
.fld input[type=date]::-webkit-calendar-picker-indicator:hover,
.fld input[type=time]::-webkit-calendar-picker-indicator:hover,
.fld input[type=month]::-webkit-calendar-picker-indicator:hover{opacity:1;background:var(--surface-3)}
/* A disabled Until must not look pickable. */
.fld input:disabled{opacity:.5;cursor:not-allowed}
.fld input:disabled::-webkit-calendar-picker-indicator{cursor:not-allowed;opacity:.25}

/* OUR CALENDAR, NOT CHROME'S — see dpMount/bindDatePicker in app.js.

   The real <input type="date"> is hidden and keeps the value under its own id;
   what you see is .dpface, a read-only text field we own outright, with a real
   button beside it. There is no native picker in this markup to suppress,
   which is the point — the previous attempt kept the native input on screen
   and tried to neuter it, and Chrome opened its calendar from the keyboard
   anyway, on top of ours.

   Only on a fine pointer (html.dpick-on). Touch keeps the native control,
   which on a phone is a full-height OS picker worth more than anything a
   272px popover can do. */
.dpwrap{position:relative;display:block}
.dpface{width:100%;cursor:pointer;padding-right:38px !important}
.dpface::placeholder{color:var(--ink-3)}
.dpface:disabled{cursor:not-allowed}
/* Inside the field, on the side the native glyph used to sit, so the affordance
   has not moved — it is just a control now rather than a picture of one. */
.dpbtn{position:absolute;top:50%;right:7px;transform:translateY(-50%);
  width:26px;height:26px;display:grid;place-items:center;padding:0;border:0;border-radius:7px;
  background:none;color:var(--ink-4);cursor:pointer;transition:color .12s,background .12s}
.dpbtn svg{width:15px;height:15px}
.dpwrap:hover .dpbtn{color:var(--ink-2)}
.dpbtn:hover{background:var(--surface-3);color:var(--ink)}
.dpbtn:disabled{cursor:not-allowed;opacity:.4;background:none}

/* Fixed, so a modal's overflow cannot clip it and a drawer's scroll cannot
   carry it away. Above the modal scrim. */
.dpick{position:fixed;z-index:200;width:272px;padding:10px;
  background:var(--surface);border:1px solid var(--line-2);border-radius:var(--r);box-shadow:var(--sh-2)}
.dpick[hidden]{display:none}
.dphead{display:flex;align-items:center;gap:4px;margin-bottom:8px}
/* The title is the zoom-out: month grid, then years. Reaching a contract that
   ends in 2029 was thirty-nine taps on a next-month arrow. */
.dptitle{flex:1;min-width:0;font:inherit;font-size:13px;font-weight:600;color:var(--ink);
  background:none;border:0;border-radius:7px;padding:5px 8px;cursor:pointer}
.dptitle:hover{background:var(--surface-3)}
.dpnav{width:28px;height:28px;display:grid;place-items:center;padding:0;border:0;border-radius:7px;
  background:none;color:var(--ink-3);cursor:pointer;flex:none}
.dpnav svg{width:15px;height:15px}
.dpnav:hover{background:var(--surface-3);color:var(--ink)}
.dpdow,.dpgrid{display:grid;grid-template-columns:repeat(7,1fr);gap:2px}
.dpdow span{text-align:center;font-size:10px;font-weight:600;letter-spacing:.04em;
  text-transform:uppercase;color:var(--ink-4);padding-bottom:4px}
.dpd{height:32px;display:grid;place-items:center;padding:0;border:0;border-radius:7px;
  background:none;font:inherit;font-size:12.5px;color:var(--ink);cursor:pointer}
.dpd:hover{background:var(--surface-3)}
/* The days either side of the month are shown rather than blanked, so the
   weeks read as weeks — but quietly enough not to be mistaken for this
   month's. */
.dpd.out{color:var(--ink-5)}
.dpd:disabled{color:var(--ink-5);opacity:.4;cursor:not-allowed;background:none}
/* Today is a RING and the selection is a FILL, so a day that is both still
   says both. Two fills could only ever show one of them. */
.dpd.today,.dpm.today{box-shadow:inset 0 0 0 1.5px var(--line-2);font-weight:600}
.dpd.sel,.dpm.sel{background:var(--brand);color:#fff;font-weight:600}
.dpd.sel:hover,.dpm.sel:hover{background:var(--brand)}
.dpd.sel.today,.dpm.sel.today{box-shadow:inset 0 0 0 1.5px var(--brand-ink)}
/* Months and years, three to a row — twelve of either fits one page, so
   neither view ever scrolls. */
.dpmg{display:grid;grid-template-columns:repeat(3,1fr);gap:4px}
.dpm{height:40px;display:grid;place-items:center;padding:0;border:0;border-radius:7px;
  background:none;font:inherit;font-size:12.5px;color:var(--ink);cursor:pointer}
.dpm:hover{background:var(--surface-3)}
/* The three answers these fields are given most of the time. */
.dpquick{display:flex;flex-wrap:wrap;gap:5px;margin-top:9px;padding-top:9px;border-top:1px solid var(--line)}
.dpq{font:inherit;font-size:11.5px;padding:4px 9px;border-radius:999px;cursor:pointer;
  background:var(--surface-2);border:1px solid var(--line);color:var(--ink-2)}
.dpq:hover{border-color:var(--line-2);color:var(--ink);background:var(--surface-3)}
.dpq.clr{margin-left:auto;color:var(--ink-3)}
.dpq.clr:hover{color:var(--red);border-color:var(--red)}
.fld textarea{min-height:70px;resize:vertical;line-height:1.55}

/* ================= note fields ================= */
/* A free-text field that can be dictated into. The mic is absolutely
   positioned inside the box, so the field itself reserves a right gutter and
   nothing is ever typed underneath it. */
.nf{position:relative;display:block}
/* The gutter clears the mic. On a textarea it also clears the scrollbar the
   field grows into: the mic is pushed inboard of the 10px track so the two
   never sit on top of each other. */
.nf>input{padding-right:38px}
.nf>textarea{padding-right:44px}

/* SIZING. Every note field used to be one height — .fld textarea's flat 70px,
   about three lines — whether it held a street address or a full call history,
   and it neither grew with the text nor capped: dragging the grip could make it
   taller than the drawer that contains it. Now each field declares the range it
   is allowed to occupy, JS grows it to fit inside that range, and past the top
   it SCROLLS instead of pushing the Save button off the bottom of the panel.
   resize:vertical is kept — the grip still wins, and once dragged the automatic
   sizing gets out of the way for good (see nfGrow). */
.nf>textarea{display:block;min-height:0;line-height:1.6;overflow-y:auto;resize:vertical;
  transition:border-color .12s,box-shadow .12s}
.nf-sm>textarea{min-height:62px;max-height:190px}
.nf-md>textarea{min-height:96px;max-height:260px}
.nf-lg>textarea{min-height:132px;max-height:340px}
/* An address is one line, or two — not the three a notes field opens at, which
   left a street address sitting at the top of an empty box. 38px is the height
   of the Phone and Email inputs above it — nfGrow lands it on 39, within a
   pixel of the rest of the column — and it grows from there when an address
   wraps. No grip either: nobody drags an address taller, and the grip and
   the mic together made the box look like it had controls in it. With the grip
   gone the mic's default 5px inset centres it exactly in the collapsed field.
   AFTER .nf>textarea — same specificity, so file order is what decides. */
.nf-addr>textarea{min-height:38px;max-height:96px;resize:none}
/* A one-line field has no resize grip and no scrollbar, so the mic sits
   centred and closer to the edge. */
.nf-one>textarea,.nf-one>input{resize:none}
.nf-one .micbtn{right:5px}

.micbtn{position:absolute;top:5px;right:9px;width:28px;height:28px;display:grid;place-items:center;
  padding:0;border:0;border-radius:8px;background:transparent;color:var(--ink-5);cursor:pointer;
  transition:background .12s,color .12s}
/* Top-right, deliberately clear of the bottom-right resize grip — the same
   placement vsi-web's TextAreaField settled on. On a single-line field there is
   no grip, so it centres instead. */
.nf-one .micbtn{top:50%;transform:translateY(-50%)}
.micbtn svg{width:15px;height:15px}
.micbtn:hover{background:var(--surface-3);color:var(--ink-2)}
.micbtn:focus-visible{outline:none;box-shadow:var(--ring)}
.micbtn.on{background:var(--red);color:#fff;animation:micpulse 1.4s ease-in-out infinite}
.micbtn.on:hover{background:var(--red);color:#fff}
.micbtn.bad{color:var(--red)}
@keyframes micpulse{0%,100%{opacity:1}50%{opacity:.55}}
@media(prefers-reduced-motion:reduce){.micbtn.on{animation:none}}
/* A live mic is worth showing on the field itself, not just the button — while
   dictating, the field is the thing being written to. */
.nf:has(.micbtn.on)>textarea,.nf:has(.micbtn.on)>input{border-color:var(--red);box-shadow:0 0 0 3px var(--red-soft)}
/* Touch: 28px is under thumb size, and these are used one-handed in a parking
   lot more than at a desk. */
@media(hover:none),(pointer:coarse){
  .micbtn{width:36px;height:36px}
  .micbtn svg{width:17px;height:17px}
  .nf>input{padding-right:46px}
  .nf>textarea{padding-right:52px}
}
.fld input:focus,.fld select:focus,.fld textarea:focus{outline:none;border-color:var(--brand);box-shadow:var(--ring)}
.fld input::placeholder,.fld textarea::placeholder{color:var(--ink-3)}
/* Two-up field row — 30 of them across the editors, so this rule alone
   decides whether the back office is usable on a phone. A hard `1fr 1fr`
   made 147px columns in the 360px drawer, and a <select> cannot scroll its
   own label: "Shelby County (ALSHEL01)" needs 240px and rendered as "Shelby
   County…", i.e. you could not read which county the account was scoped to
   while editing exactly that. Email and Role were clipped the same way.
   auto-fit keys off the CONTAINER, not the viewport, so the 445px desktop
   drawer keeps its two columns (2x180+11 = 371) and the 305px phone one
   drops to a single column. 180px is the floor for a labelled field —
   anything under it clips a real value somewhere in these forms. */
.fld2{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:11px}
/* Date / start / length, which belong on one line because they are one
   decision. auto-fit for the same reason .fld2 uses it: the phone drops them
   to a column without a second breakpoint. 120px, not 180 — a time input and
   a duration select are both narrower than a labelled text field, and holding
   them to 180 forced a three-field row to wrap on a 375px screen. */
.fld3{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:11px}
/* A warning that is ADVICE, not a refusal — the double-booking still books if
   you want it to. Amber rather than red, and it says so in words. */
.fldwarn{display:flex;align-items:flex-start;gap:7px;margin-top:7px;padding:8px 10px;
  font-size:11.5px;line-height:1.45;color:var(--amber);
  background:var(--amber-soft);border:1px solid var(--amber);border-radius:var(--r-sm)}
.fldwarn svg{width:13px;height:13px;flex:none;margin-top:2px}
.fldwarn b{font-weight:600}

/* The agenda, read back on the meeting sheet. Pre-wrap because it was typed in
   a textarea and the paragraph breaks somebody put in it are part of what they
   wrote — collapsing them turns an agenda into a wall. */
.mtagenda{margin:0 0 4px;font-size:13px;line-height:1.55;color:var(--ink-2);white-space:pre-wrap}
.mtinv{display:flex;flex-wrap:wrap;gap:6px;margin-top:2px}
.mtinvp{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--ink-2);
  background:var(--surface-2);border:1px solid var(--line);border-radius:999px;padding:3px 10px 3px 3px}
.mtinvp .ravatar{width:20px;height:20px;font-size:9px}

/* THE PEOPLE PICKER — chips above, search below, list overlaid.
   Reuses the address picker's shape on purpose: one way to search for a thing
   and click it, not two that look different. */
.pick{position:relative}
.pchips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:7px}
.pchip{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--ink);
  background:var(--surface-2);border:1px solid var(--line-2);border-radius:999px;padding:3px 4px 3px 3px}
.pchip .ravatar{width:20px;height:20px;font-size:9px;flex:none}
/* The organiser's own chip. It has no remove button, so it gets the padding
   the button would have occupied and a quieter surface — it is a statement of
   fact, not a control. */
.pchip.locked{padding-right:11px;background:var(--surface-3);color:var(--ink-2)}
.pchipx{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;
  padding:0;border:0;border-radius:999px;background:none;color:var(--ink-4);cursor:pointer}
.pchipx svg{width:11px;height:11px}
.pchipx:hover{background:var(--red-soft);color:var(--red)}
.pfield{display:flex;align-items:center;gap:9px;padding:0 11px;background:var(--surface);
  border:1px solid var(--line-2);border-radius:var(--r-sm)}
.pfield:focus-within{border-color:var(--brand);box-shadow:var(--ring)}
.pfield>svg{width:15px;height:15px;flex:none;color:var(--ink-4)}
.pfield input{flex:1;min-width:0;height:36px;font:inherit;font-size:13px;color:var(--ink);
  background:none;border:0;padding:0}
.pfield input:focus{outline:none}
.pfield input::placeholder{color:var(--ink-4)}
/* Overlaid, so adding the fourth person does not push the Schedule button
   down the sheet while you are reaching for it. */
.plist{position:absolute;z-index:6;top:100%;left:0;right:0;margin-top:5px;max-height:210px;overflow-y:auto;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-sm);
  box-shadow:var(--sh-2);padding:4px}
.popt{display:flex;align-items:center;gap:9px;width:100%;text-align:left;background:none;border:0;
  border-radius:6px;padding:6px 8px;cursor:pointer;font:inherit;font-size:13px;color:var(--ink)}
.popt .ravatar{width:22px;height:22px;font-size:9px;flex:none}
.popt b{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* .on is the keyboard cursor; hover paints the same, so the two can never
   disagree about which row is about to be chosen. */
.popt:hover,.popt.on{background:var(--brand-soft)}
.popt:hover b,.popt.on b{color:var(--brand-ink)}
.pnone{padding:8px 9px;font-size:12px;color:var(--ink-3)}

/* "3 of 8" on a row in a recurring run. */
.mtser{display:inline-block;margin-left:7px;font-size:10.5px;font-weight:600;color:var(--ink-3);
  background:var(--surface-3);border-radius:4px;padding:1px 6px;vertical-align:middle}
.chk{display:flex;align-items:center;gap:7px;font-size:11.5px;color:var(--ink-3);margin-top:6px;cursor:pointer}
/* Entry-type picker. Radios, not buttons: the checked state IS the value, so
   nothing has to be read back out of a class, and it is keyboard-navigable and
   labelled for free. */
.kindpick{display:grid;grid-template-columns:1fr 1fr;gap:8px}
/* `.kindpick .kopt`, not `.kopt`. This is a <label> inside a .fld, so the
   plain class (0,1,0) LOST to `.fld label` (0,1,1) — the picker was rendering
   display:block, which is why its icons sat on a text baseline four pixels
   above the words instead of centred beside them, why the labels came out at
   11.5px instead of 13.5px, and why the pair sat high in its own 40px box.
   Everything below was being declared and thrown away. */
.kindpick .kopt{position:relative;display:flex;align-items:center;justify-content:center;gap:7px;
  height:40px;margin:0;cursor:pointer;
  border:1px solid var(--line-2);border-radius:9px;background:var(--surface);
  font-size:13.5px;font-weight:500;color:var(--ink-2)}
.kindpick .kopt svg{flex:none}
.kopt svg{width:16px;height:16px;opacity:.7}
.kopt input{position:absolute;opacity:0;width:0;height:0}
.kopt:hover{border-color:var(--ink-4)}
.kopt:focus-within{box-shadow:var(--ring);border-color:var(--brand)}
.kopt.on{border-color:var(--brand);background:var(--brand-soft);color:var(--brand-ink);font-weight:600}
.kopt.on svg{opacity:1}
.chk input{width:auto;accent-color:var(--brand)}
.chk.gap{margin-bottom:15px}
.dmeta{font-size:11.5px;color:var(--ink-3);line-height:1.7;background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm);padding:10px 12px;margin-top:4px}
.dmeta svg{width:13px;height:13px;vertical-align:-2px;margin-right:5px;color:var(--ink-4)}
/* Touch: the checkbox itself is 13x13, but it is wrapped in a <label>, so the
   real target is the whole 305px line — only its HEIGHT was short at 19px.
   "open map" had no such wrapper: 53x14, and it is the one control here
   someone actually uses standing next to the vehicle. Padding enlarges an
   inline element's hit box without moving it; the negative margin cancels the
   layout shift, and the neighbouring lines are plain text, so the few pixels
   of overlap cover nothing clickable. */
@media(hover:none),(pointer:coarse){
  .chk{min-height:26px;margin-top:8px}
  .chk input{width:16px;height:16px}
  .dmeta a{display:inline-block;padding:6px 4px;margin:-6px -4px}
}
/* The inline note on a just-logged entry. Full width of the row, because the
   thing being typed is a sentence, and sitting where the outcome will render
   so the field and its result occupy the same place. */
/* A field the panel ANSWERS rather than asks about. Reads as a value, not as
   a disabled input — there is nothing here to type. */
/* The lead's owner, in the District cell. Small on purpose: the answer it
   gives is "is this mine", which a face answers faster than a name, and the
   column's job is still the district. */
.ownav{flex:none;width:17px;height:17px;border-radius:50%;display:grid;place-items:center;
  font-size:8.5px;font-weight:700;color:#fff;background:hsl(var(--h,210) 42% 42%);
  margin-left:6px;vertical-align:middle;box-shadow:0 0 0 2px var(--surface)}
.ownav.img{background-size:cover;background-position:center;color:transparent}

.derived{display:flex;flex-direction:column;gap:1px;padding:6px 0 2px}
.derived b{font-size:13px;font-weight:600;color:var(--ink)}
.derived span{font-size:11px;color:var(--ink-4)}
.derived.stale b{color:var(--amber)}
.derived.none b{color:var(--ink-4);font-weight:500}

.lognote{display:flex;align-items:center;gap:8px;margin-top:6px}
.lognote input{flex:1;min-width:0;font:inherit;font-size:12.5px;color:var(--ink);
  background:var(--surface-2);border:1px solid var(--brand);border-radius:8px;padding:6px 9px}
/* The next step is the quieter of the two: the outcome is about the call you
   just had, this one is a commitment about the deal. Same row, softer edge. */
.lognote #logNext{border-color:var(--line-2)}
.lognote #logNext:focus{border-color:var(--brand)}
@media(max-width:620px){.lognote{flex-wrap:wrap}.lognote input{flex:1 1 100%}}
.lognote input:focus{outline:2px solid var(--brand);outline-offset:-1px}
.lognotehint{flex:none;font-size:10.5px;color:var(--ink-4);white-space:nowrap}
@media(max-width:560px){.lognotehint{display:none}}

.logrow{background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm);padding:10px 12px;margin-bottom:8px;cursor:pointer}
.logrow:hover{border-color:var(--line-2);background:var(--surface)}
.logdate{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--ink-3)}
.logdate b{color:var(--ink);font-weight:600;font-variant-numeric:tabular-nums}
.logkind{display:inline-flex;align-items:center;gap:4px;font-size:9.5px;font-weight:700;text-transform:uppercase;
  letter-spacing:.06em;padding:1px 6px;border-radius:4px;background:var(--surface-3);color:var(--ink-3)}
.logkind svg{width:10px;height:10px}
.logrow.k-call .logkind{background:var(--violet-soft);color:var(--violet)}
.logst{font-size:9.5px;font-weight:600;text-transform:uppercase;letter-spacing:.07em;padding:1px 6px;border-radius:4px}
.logst.planned{background:var(--amber-soft);color:var(--amber)}
.logst.done{background:var(--green-soft);color:var(--green)}
.logreason{font-size:13px;font-weight:500;margin-top:5px}
.logout{font-size:12.5px;color:var(--green);margin-top:2px}
.lognotes{font-size:12px;color:var(--ink-3);margin-top:3px;line-height:1.5}
.mini{font-size:12.5px;color:var(--ink-4);padding:2px}

.modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-48%) scale(.99);width:min(500px,94vw);max-height:88vh;display:flex;flex-direction:column;background:var(--surface);border:1px solid var(--line);border-radius:14px;box-shadow:var(--sh-2);z-index:43;opacity:0;pointer-events:none;transition:opacity .16s,transform .16s}
.modal.open{opacity:1;pointer-events:auto;transform:translate(-50%,-50%) scale(1)}
/* The send sheet, which carries a US Letter page. At the standard 500px the
   preview renders about 7pt — legible to nobody, and a preview nobody can read
   is not a check on anything. Added and removed per-open, never left on #tm. */
.modal.wide{width:min(840px,94vw)}

/* ---------- the full-page sheet ----------
   For writing a document rather than filling in a form. At 500px a quote with
   three line items and a second line under each is a column of 60px inputs you
   scroll through one at a time — you cannot see the thing you are writing.

   IT IS STILL #tm, NOT A VIEW. Every realtime snapshot rebuilds #viewbody with
   innerHTML, so a half-typed quote living there would be erased by a colleague
   flipping an unrelated status pill somewhere else in the panel. #tm and
   #drawer are the only two containers no snapshot touches, so "full page" is
   done by making the modal full-bleed rather than by moving the editor. */
.modal.page{width:min(1180px,96vw);max-height:94vh}
@media (max-width:860px){
  /* Actually full-bleed on a phone, the same override .modal.lightbox makes —
     the centring transform has to go in BOTH states or it animates in from
     off-screen. */
  .modal.page{position:fixed;inset:0;top:0;left:0;transform:none;
    width:100%;height:100%;max-height:none;border:0;border-radius:0}
  .modal.page.open{transform:none}
}
/* Fields beside the document, not above it: the line items are the part that
   needs room, and the dates and the tax rate are not. */
.qgrid{display:grid;grid-template-columns:320px minmax(0,1fr);gap:0 30px;align-items:start}
@media (max-width:860px){ .qgrid{grid-template-columns:minmax(0,1fr)} }
.qgrid .qcol-side{min-width:0}
.qgrid .qcol-main{min-width:0}
/* The side column's first label would otherwise sit 22px lower than the main
   column's, because .seclab carries a top margin and .fld does not. */
.qgrid .qcol-side>.fld:first-child{margin-top:0}
/* With room to spare the description gets it — that is the whole point of the
   wider sheet — and the numbers keep their fixed widths.

   min-width, NOT an unguarded rule: .iline has a phone layout of its own
   further down (grid-template-areas, one breakpoint at 560px), and this
   selector is (0,2,0) — left unguarded it beats that layout inside its own
   media query and the row collapses. Guarding here is simpler than restating
   the phone grid there. */
@media (min-width:561px){
  .modal.page .iline{grid-template-columns:minmax(0,1fr) 84px 104px 104px 28px}
}
/* A modal over a locked body: same chaining, and a long member list is
   exactly where a thumb over-drags. */
.mmbody{padding:18px 22px;overflow-y:auto;overflow-x:hidden;overscroll-behavior:contain}
.mmlist{display:flex;flex-direction:column;gap:7px}
.mmrow{display:flex;align-items:center;gap:10px;background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm);padding:9px 12px}
.mmrow .em{font-size:12.5px;font-weight:500;flex:1;word-break:break-all}
.mmrow .nm2{font-size:11px;color:var(--ink-3);font-weight:400}
.note{font-size:11.5px;color:var(--ink-2);line-height:1.6;background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm);padding:11px 13px;margin-top:14px}

.toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%) translateY(14px);background:var(--ink);color:var(--bg);padding:10px 16px;border-radius:9px;font-size:13px;font-weight:500;box-shadow:var(--sh-2);opacity:0;pointer-events:none;transition:.2s;z-index:60}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}

/* ---------- gate ---------- */
.gate{position:fixed;inset:0;display:grid;place-items:center;background:var(--bg);z-index:100;padding:24px}
.gatecard{width:100%;max-width:396px;background:var(--surface);border:1px solid var(--line);border-radius:14px;box-shadow:var(--sh-2);overflow:hidden}
.gatetop{background:linear-gradient(155deg,#173D63,#0A1D30);padding:26px 26px 24px;display:flex;align-items:center;gap:13px}
.gatetop .mark{width:40px;height:40px;border-radius:10px}
.gatetop h1{color:#fff;font-size:19px;letter-spacing:-.02em}
.gatetop p{color:#93AABF;font-size:12px;margin:3px 0 0}
.gatebody{padding:22px 26px 26px}
.gerr{background:var(--red-soft);color:var(--red);border:1px solid var(--red-soft);border-radius:var(--r-sm);padding:9px 12px;font-size:12.5px;margin-top:13px;font-weight:500}
.gmsg{font-size:12px;color:var(--ink-3);margin-top:15px;line-height:1.6}
.gdenied{color:var(--red);font-weight:600;font-size:14px}
.gate .btn{width:100%;justify-content:center;height:38px}

.railnav{display:none}
@media(max-width:900px){.railnav{display:grid;width:38px;height:38px;flex:none;border-radius:9px}}
/* Same reasoning as .railnav, one breakpoint tighter: below 700px the
   conversation list is a separate screen from the room, so the room header's
   bell is not reachable from it at all. Above that both panes are on screen
   together and a second bell is only clutter. */
.railbell{display:none}
@media(max-width:700px){
  .railbell{display:grid;width:38px;height:38px;flex:none;border-radius:9px;justify-self:end}
  .railbell.on{color:var(--brand);background:var(--brand-soft)}
}
.navtoggle .ntw,.navtoggle .ntm{place-items:center}
.navtoggle .ntw{display:grid}
.navtoggle .ntm{display:none}

@media(min-width:901px){
  body.navhidden .app{grid-template-columns:0 minmax(0,1fr)}
  body.navhidden .side{overflow:hidden;padding-left:0;padding-right:0;border-right-width:0}
}

/* ================= phone + tablet shell =================
   WAS a horizontal scroll strip in a first grid row. Measured on a 375px
   phone, `.sidefoot{margin-left:auto}` inside that `overflow-x:auto` row put
   Access, the theme toggle and SIGN OUT ~1530px past the right edge, behind a
   scroll gesture with no affordance pointing at it. Fourteen destinations do
   not fit on a phone row, and the ones that fell off were the account
   controls.

   The rail is now an off-canvas drawer: the same vertical markup as the
   desktop rail, positioned out of flow, brought in over a scrim. Nothing about
   the sidebar's own layout changes, which is why the nav, the badges and the
   footer all keep working untouched.

   height:100dvh (not 100vh) so iOS Safari's collapsing toolbars cannot push
   the bottom of the shell under the viewport. .scrollarea stays the only
   scroller at every width — that is what keeps the Leads table's sticky thead
   working, and it is why the drawer is `position:fixed` rather than a grid
   row that would reintroduce a document scroll. */
@media(max-width:900px){
  .app{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr);height:100vh;height:100dvh;min-height:0}

  .side{
    position:fixed;top:0;bottom:0;left:0;z-index:46;
    width:min(284px,82vw);
    /* The rail is its own scroller here: 14 destinations plus the footer
       exceed a landscape phone's height. */
    overflow-y:auto;overscroll-behavior:contain;
    padding-top:calc(18px + env(safe-area-inset-top,0px));
    padding-bottom:calc(14px + env(safe-area-inset-bottom,0px));
    border-right:1px solid var(--navy-2);
    transform:translateX(-100%);
    transition:transform .22s cubic-bezier(.4,0,.2,1);
    will-change:transform;
  }
  body.navopen .side{transform:none;box-shadow:0 0 40px rgba(0,0,0,.4)}
  /* Measured: 14 destinations at touch size + the footer came to 906px against
     an 812px phone, putting Sign out under the fold. The section label is the
     one thing here carrying no destination, and the rail is the only child
     group that needed 20px of separation on a desktop. Both go, which lands a
     modern phone (852px+) on a drawer that needs no scroll at all; a smaller
     one scrolls the last row into view rather than hiding it off-canvas. */
  .side{gap:12px}
  .navlab{display:none}
  /* Off-canvas means off the tab order too — otherwise Tab walks into a panel
     nobody can see. */
  body:not(.navopen) .side{visibility:hidden}
  body.navopen .side{visibility:visible}

  .navscrim{z-index:45}

  /* The panel glyph means "collapse this column", which is not what the
     control does here. */
  .navtoggle .ntw{display:none}
  .navtoggle .ntm{display:grid}
  .navtoggle{width:40px;height:40px;border-radius:10px}

  /* ONE row, not three. The old rule let .top wrap, and title + actions +
     a full-width search stacked into ~132px of a 812px phone before any
     content — 16% of the screen, permanently. Search gets its own compact
     row under the title; the actions stay on the title line. */
  .top{height:auto;min-height:56px;padding:10px 14px;gap:10px;flex-wrap:wrap;
       padding-top:calc(10px + env(safe-area-inset-top,0px))}
  /* A real minimum, not 0. With a zero basis the title collapsed to one
     letter and let the environment switcher share row one; keeping 110px
     reserved pushes the switcher onto row two, beside the search. */
  .ttl{flex:1 1 110px;min-width:110px;overflow:hidden}
  .ttl h1{font-size:16px;flex:none;white-space:nowrap}
  .ttl .count{min-width:0;overflow:hidden;text-overflow:ellipsis}
  .topacts{margin-left:auto;flex:none;gap:6px}
  /* Row two, switcher first and the search taking what is left. Ordered, not
     sized: the search keeps a zero basis so it can never push the title off
     row one, which is what a 150px basis did. */
  .topenv{order:3;flex:0 0 auto}
  .topenv .envseg button{padding:5px 10px}
  .search{margin-left:0;order:4;max-width:none;flex:1 1 0;min-width:120px;height:38px}
  .top .btn{height:38px}
  .scrollarea{--pt:14px;padding:var(--pt) 14px calc(48px + env(safe-area-inset-bottom,0px))}
}

/* "Export" and "New entry" are desktop-sized words sitting next to each other
   on a 375px title row. The download icon carries Export on its own; the
   primary action keeps a word because a bare + is not a label. */
@media(max-width:560px){
  .top .btn.ghost .btl{display:none}
  .top .btn.ghost{padding:0 10px}
  .top .btn.primary .btl{display:none}
  .top .btn.primary .bts{display:inline}
}

@media(prefers-reduced-motion:reduce){*{transition:none!important}}

/* ================= chat ================= */
/* THE LAYOUT FIX. #viewbody has no rule of its own, so it is a content-sized
   flex item and the height chain from .scrollarea dies there — measured, the
   message log rendered 1955px tall with the composer 1300px below the fold.
   Both rules are scoped to the chat view (body.chatview, set in renderView):
   applying them globally would restyle the Leads table and its sticky thead. */
body.chatview .scrollarea{overflow:hidden;padding-bottom:var(--pt)}
body.chatview #viewbody{flex:1;min-height:0;display:flex;flex-direction:column}
/* Export dumps districts and "Schedule visit" opens a trip — neither does
   anything for a conversation — and the search box beside them filters the
   Leads table, not the rail. Chat brings its own search (.railsearch), so the
   header keeps only the title and the conversation count. */
body.chatview .topacts,body.chatview .search{display:none}

.chatroot{flex:1;min-height:0;display:grid;grid-template-columns:264px minmax(0,1fr);
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r);
  box-shadow:var(--sh);overflow:hidden}

.chatrail{display:flex;flex-direction:column;min-height:0;border-right:1px solid var(--line);background:var(--surface-2)}
.railtop{display:flex;flex-direction:column;gap:6px;padding:12px;border-bottom:1px solid var(--line)}
.railtop .btn{justify-content:flex-start;width:100%}
/* Chat's own room filter. The global #q in .top cannot serve chat: .top is
   hidden below 900px, so room search was silently desktop-only, and its
   placeholder ("Search district or supervisor") described the Leads table. */
.railsearch{display:flex;align-items:center;gap:8px;height:32px;padding:0 10px;margin-top:2px;
  background:var(--surface-3);border:1px solid transparent;border-radius:8px}
.railsearch:focus-within{background:var(--surface);border-color:var(--brand);box-shadow:var(--ring)}
.railsearch svg{width:14px;height:14px;color:var(--ink-3);flex:none}
.railsearch input{flex:1;min-width:0;border:0;background:transparent;color:var(--ink);font:inherit;font-size:13px;outline:none;-webkit-appearance:none}
.railsearch input::-webkit-search-decoration,.railsearch input::-webkit-search-cancel-button{-webkit-appearance:none}
.railsearch input::placeholder{color:var(--ink-3)}
.raillist{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;
  overscroll-behavior:contain;padding:8px 8px 16px}
.raillab{font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  color:var(--ink-3);padding:14px 8px 6px}
.railempty{margin:0;padding:2px 8px 6px;font-size:12.5px;color:var(--ink-3)}
/* How many are open to join, on the heading — the number is the reason to
   look at the section at all. */
.railn{margin-left:6px;font-size:10px;font-weight:700;letter-spacing:0;
  color:var(--ink-3);background:var(--surface-3);border-radius:999px;padding:1px 6px}
/* A channel you are not in reads one step back: the name is not bold, and the
   row carries the word that says what clicking it offers. */
.roomrow.open .rmid b{font-weight:500;color:var(--ink-2)}
.roomrow.open .ravatar{opacity:.72}
.rjoin{flex:none;font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
  color:var(--brand-ink);background:var(--brand-soft);border-radius:999px;padding:2px 7px}
.roomrow.open:hover .rjoin{background:var(--brand);color:#fff}
/* The unjoined channel's log. Not an error and not an empty room — an
   invitation, with the count that says whether anyone is in there. */
.chatempty.joinpitch{display:flex;flex-direction:column;align-items:center;gap:9px;max-width:44ch;
  margin-left:auto;margin-right:auto;text-align:center}
.chatempty.joinpitch b{font-size:14.5px;font-weight:600;color:var(--ink)}
.chatempty.joinpitch span{font-size:13px;color:var(--ink-3);line-height:1.5}
.roomrow{display:flex;align-items:center;gap:9px;width:100%;padding:8px;border:0;border-radius:var(--r-sm);
  background:none;cursor:pointer;text-align:left;color:var(--ink);font:inherit}
.roomrow:hover{background:var(--surface-3)}
.roomrow.on{background:var(--brand-soft)}
.ravatar{flex:none;width:30px;height:30px;border-radius:9px;display:grid;place-items:center;
  font-size:11.5px;font-weight:700;color:#fff;background:hsl(var(--h,210) 42% 42%)}
/* An avatar with a photo. The initials stay in the markup — they are what a
   screen reader reads and what shows if the file 404s — but the picture covers
   them. One rule for every avatar in the panel. */
.av.img,.ravatar.img,.thav.img,.mav.img,.rav.img{background-size:cover;background-position:center;
  background-repeat:no-repeat;color:transparent}
.rmid{min-width:0;flex:1;display:flex;flex-direction:column;gap:1px}
/* The DM picker. Deliberately the same shape as a .roomrow — picking someone
   to message and the conversation you end up in should not look like two
   different kinds of thing — but it is its own class: .roomrow carries rail
   state (.on, .un, the unread dot) that means nothing in a chooser. */
.picklist{display:flex;flex-direction:column;gap:4px;max-height:46vh;overflow-y:auto;margin:0 -6px;padding:0 6px}
.pickrow{display:flex;align-items:center;gap:10px;width:100%;padding:8px;border:0;border-radius:var(--r-sm);
  background:none;cursor:pointer;text-align:left;font:inherit;color:inherit}
.pickrow:hover{background:var(--surface-3)}
.pickrow:focus-visible{outline:2px solid var(--brand);outline-offset:-2px}
.pickrow .rmid b{font-size:13px;font-weight:600}
.pickrow .rmid em{font-size:11.5px;color:var(--ink-3);font-style:normal;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Says "this conversation already exists" — clicking opens it rather than
   starting a second one, which the derived DM id makes impossible anyway. */
.pickon{flex:none;font-size:10px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;
  color:var(--ink-3);background:var(--surface-3);border-radius:5px;padding:2px 7px}

/* Channel settings. .seclab already divides the drawers; danger red-tints the
   last one so the two destructive rows are not the same weight as a rename. */
.seclab.danger{color:var(--red)}
.csn{margin-left:6px;font-size:11px;font-weight:600;color:var(--ink-3);
  background:var(--surface-3);border-radius:999px;padding:1px 7px}
.csrow{display:flex;align-items:center;gap:12px;padding:10px 0}
.csrow+.csrow{border-top:1px solid var(--line)}
.csrow>div{min-width:0;flex:1}
.csrow b{display:block;font-size:12.5px;font-weight:600;color:var(--ink)}
/* The consequence, not a restatement of the button. */
.csrow em{display:block;font-style:normal;font-size:11.5px;color:var(--ink-3);
  line-height:1.45;margin-top:2px}
.csrow .btn{flex:none}
/* The topic replaces the "open" chip, so it has to be able to run long
   without pushing the actions off the header. */
.chip.topic{color:var(--ink-2);background:var(--surface-3);
  max-width:min(52ch,46vw);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Sits above the guest toggles, so the absence of your own name reads as
   deliberate rather than as a bug. */
.nohost{margin:0 0 8px}
.rmid b{font-size:13.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rmid em{font-style:normal;font-size:11.5px;color:var(--ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.roomrow.un .rmid b{font-weight:700}
.rdot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--brand)}

.chatmain{display:flex;flex-direction:column;min-height:0;min-width:0;position:relative}
.chathead{display:grid;grid-template-columns:1fr auto;gap:4px 12px;align-items:center;
  padding:12px 16px;border-bottom:1px solid var(--line);background:var(--surface)}
.chattitle{display:flex;align-items:center;gap:10px;min-width:0;flex-wrap:wrap}
.chattitle h2{margin:0;font-size:16px;font-weight:700;letter-spacing:-.01em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.chattitle .hash{color:var(--ink-4);margin-right:1px}
.chathead .chip{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;
  padding:3px 8px;border-radius:999px;border:1px solid;white-space:nowrap}
.chathead .chip svg{width:13px;height:13px}
.chip.lock{color:var(--teal);background:var(--teal-soft);border-color:var(--teal)}
.chip.open{color:var(--ink-3);background:var(--surface-3);border-color:var(--line-2)}
.chatacts{display:flex;align-items:center;gap:6px;justify-self:end}
.chatacts .iconbtn.on{color:var(--brand);background:var(--brand-soft)}
/* Three states, because "never asked" and "asked and it failed" are not the
   same thing and used to paint identically. */
[data-chat-push].busy{opacity:.55}
[data-chat-push].bad{color:var(--red)}
.railbell.bad{color:var(--red)}
.pusherr{margin:0;padding:6px 16px 0;font-size:11.5px;color:var(--red);line-height:1.45}
.chatmeta{grid-column:1/-1;font-size:11.5px;color:var(--ink-3);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.chatback{display:none}

/* A short conversation belongs at the BOTTOM of the log, next to the composer,
   not stranded at the top of 600px of white. The auto-margin spacer is used
   instead of justify-content:flex-end because flex-end makes overflowing
   content unreachable at the top in several engines; a first flex item with
   margin-top:auto collapses to nothing as soon as the log overflows. */
/* overscroll-behavior, and overflow-x SPELLED OUT. Every other view scrolls
   in .scrollarea, which contains its overscroll; chat turns .scrollarea off
   and scrolls here instead, and the guard did not come with it. On a phone
   body.chatview is position:fixed, so a drag that runs past the end of this
   log has nowhere to chain to and Safari rubber-bands the whole fixed shell —
   header, composer and all slide under the finger. `contain` keeps the drag
   in the log.
   overflow-x is the other half: declaring only overflow-y computes the other
   axis to `auto`, not `visible`, so the log was a horizontal scroller too and
   anything a pixel too wide let the screen be dragged sideways as well. */
.chatlog{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;
  overscroll-behavior:contain;padding:14px 16px 6px;
  display:flex;flex-direction:column;gap:2px;scroll-behavior:smooth}
.chatlog::before{content:"";margin-top:auto;flex:none}
/* Cap the measure. Unbounded, a message on a 2560px display runs past 200
   characters per line, which is roughly twice a readable line. */
.chatlog>.msg,.chatlog>.daydiv,.chatlog>.loadmore{width:100%;max-width:860px}
/* .mread is NOT in that list. It carries a 42px left margin to sit under the
   message body, and width:100% on top of that made every receipt row 42px
   wider than the log — enough to make the log a horizontal scroller, which
   `overflow-x:hidden` then turned into content clipped off the right edge
   with no way to reach it. The row is a few small items; it sizes itself. */
.mread{max-width:calc(100% - 42px)}
@media(prefers-reduced-motion:reduce){.chatlog{scroll-behavior:auto}}
.chatempty{margin:auto;padding:40px 20px;text-align:center;color:var(--ink-3);font-size:13px}
/* In the log, the auto-margin spacer above competes with .chatempty's own two
   auto margins and the free space splits three ways, parking the message
   two-thirds down. Dropping its top margin makes it one share each. */
.chatlog>.chatempty,.chatlog>.loading{margin:0 auto auto}
.loadmore{display:grid;place-items:center;padding:4px 0 12px}
.daydiv{display:flex;align-items:center;gap:10px;margin:14px 0 8px;color:var(--ink-3);font-size:11px;font-weight:600}
.daydiv::before,.daydiv::after{content:"";flex:1;height:1px;background:var(--line)}

.msg{display:grid;grid-template-columns:32px minmax(0,1fr) auto;gap:0 10px;padding:3px 0;border-radius:var(--r-sm)}
.msg:hover{background:var(--surface-2)}
.msg.grp{padding-top:0}
.mav{width:32px;height:32px;border-radius:10px;display:grid;place-items:center;
  font-size:11.5px;font-weight:700;color:#fff;background:hsl(var(--h,210) 42% 42%)}
.mav.sp{background:none}
.mbody{min-width:0}
.mhead{display:flex;align-items:baseline;gap:8px;margin-bottom:1px}
.mhead b{font-size:13px;font-weight:700}
.mhead time{font-size:11px;color:var(--ink-3)}
.mtext{margin:0;font-size:14px;line-height:1.5;color:var(--ink);overflow-wrap:anywhere;white-space:pre-wrap}
.mtext.del{color:var(--ink-3);font-style:italic}
/* Links in a message. underline-offset keeps the rule clear of the descenders
   in a URL, which are dense — g, p, y and the slashes all reach it. */
.mlink{color:var(--brand-ink);text-decoration:underline;text-underline-offset:2px;
  text-decoration-color:color-mix(in srgb,var(--brand-ink) 45%,transparent);
  overflow-wrap:anywhere}
.mlink:hover{text-decoration-color:currentColor}

/* Read receipts. Aligned to the message column, not the avatar gutter, so the
   faces sit under the text they refer to. -2px pulls them up against that
   message rather than letting them float between two. */
/* Indented past the 32px avatar and its 10px gap so the faces line up with
   the message text. NOT right-aligned for your own messages — this log is
   left-aligned by sender name, Slack-style, not a two-sided bubble thread,
   so a right-aligned strip would be the only thing on that edge. */
.mread{display:flex;align-items:center;gap:3px;margin:-2px 0 4px 42px}
.rav{width:16px;height:16px;border-radius:50%;display:grid;place-items:center;
  font-size:8px;font-weight:700;letter-spacing:-.02em;
  color:#fff;background:hsl(var(--h,210) 42% 46%);
  box-shadow:0 0 0 1.5px var(--surface)}
.rav+.rav{margin-left:-5px}
.rav.more{background:var(--surface-3);color:var(--ink-3);font-size:8.5px}
/* Words, not just faces. On your own last message the row is always drawn, so
   that "nobody has read this" reads as a state instead of as a chat that does
   not do receipts at all. The hover title names both sides. */
.mread .rstate{font-size:10px;font-weight:600;letter-spacing:.02em;color:var(--ink-3);margin-left:4px}
.mread .rstate.none{color:var(--ink-4);font-weight:500;margin-left:0}
/* The faces are aria-hidden decoration; this is what a screen reader gets. */
.sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0}
/* Reactions. Inside .mbody, so they sit under the text they belong to and wrap
   with it rather than claiming a grid column of their own. */
.mreacts{display:flex;flex-wrap:wrap;align-items:center;gap:5px;margin:4px 0 2px}
.rx{display:inline-flex;align-items:center;gap:4px;height:24px;padding:0 8px 0 6px;
  border-radius:999px;border:1px solid var(--line);background:var(--surface);
  font:inherit;font-size:12px;color:var(--ink-2);cursor:pointer;line-height:1}
.rx:hover{border-color:var(--line-2);background:var(--surface-2)}
/* Yours is the one you can take back, so it is the one that looks pressed. */
.rx.on{border-color:color-mix(in srgb,var(--brand) 55%,transparent);background:var(--brand-soft);color:var(--brand-ink)}
.rxg{font-size:14px;line-height:1}
.rxn{font-weight:600;font-variant-numeric:tabular-nums}
.rxpick{display:inline-flex;gap:2px;padding:3px;border-radius:999px;
  border:1px solid var(--line-2);background:var(--surface);box-shadow:var(--sh-2)}
.rxopt{width:30px;height:30px;border:0;border-radius:50%;background:none;cursor:pointer;
  font-size:17px;line-height:1;display:grid;place-items:center;transition:transform .12s}
.rxopt:hover{background:var(--surface-3);transform:scale(1.12)}
.rxopt.on{background:var(--brand-soft)}
.lnk.rxadd.on{color:var(--brand-ink)}
.lnk.rxadd svg{width:16px;height:16px}
.macts{opacity:0;display:flex;align-items:center;gap:8px;padding-right:4px}
.msg:hover .macts,.msg:focus-within .macts{opacity:1}
.lnk{display:inline-flex;align-items:center;gap:4px;border:0;background:none;padding:0;cursor:pointer;
  font:inherit;font-size:11.5px;color:var(--ink-3)}
.lnk svg{width:14px;height:14px}
.lnk:hover{color:var(--ink)}
.lnk:hover span{text-decoration:underline}
.lnk.danger{color:var(--red)}

/* The pill has to float just above the composer. It used to be absolutely
   positioned against .chatmain at a hard-coded bottom:96px, which was already
   1px INSIDE the composer on desktop (57px form + 42px footer) and buried
   under it on a phone, where the footer wraps to three lines. As a zero-height
   flex item between the log and the form it lands in the right place at every
   width with no magic number. */
.jump{position:relative;height:0;overflow:visible;display:grid;place-items:center;pointer-events:none;z-index:2}
.jump button{position:absolute;bottom:8px;pointer-events:auto;display:inline-flex;align-items:center;gap:6px;
  border:1px solid var(--line-2);background:var(--surface);color:var(--ink);
  border-radius:999px;padding:5px 12px;font:inherit;font-size:12px;font-weight:600;
  box-shadow:var(--sh-2);cursor:pointer}
.jump svg{width:14px;height:14px;transform:rotate(90deg)}

.chatform{display:grid;grid-template-columns:minmax(0,1fr) auto auto auto;gap:8px;align-items:end;
  padding:10px 16px 8px;border-top:1px solid var(--line);background:var(--surface)}
/* It scrolls the moment a message runs to a second line, and it is the one
   scroller a thumb rests on, so it chains like the log did. */
.chatform textarea{width:100%;resize:none;max-height:132px;min-height:38px;
  overscroll-behavior:contain;
  border:1px solid var(--line-2);border-radius:var(--r-sm);background:var(--surface);
  color:var(--ink);font:inherit;font-size:14px;line-height:1.45;padding:9px 11px}
.chatform textarea:focus{outline:none;border-color:var(--brand);box-shadow:var(--ring)}
.chatform textarea:disabled{background:var(--surface-3);color:var(--ink-3)}
.chatform textarea::placeholder{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.chatform .btn{height:38px;padding:0 14px}
.chatform .btn svg{width:16px;height:16px}
/* The composer's mic is a grid cell, not an overlay — it lines up with Send
   and with the bottom of a textarea that grows to 132px. */
.chatmic,.attbtn{position:static;width:38px;height:38px;border:1px solid var(--line-2);border-radius:var(--r-sm);
  background:var(--surface);color:var(--ink-3)}
.chatmic svg,.attbtn svg{width:16px;height:16px}
.chatmic:hover,.attbtn:hover{background:var(--surface-2);border-color:var(--ink-4);color:var(--ink-2)}
.chatmic.on{border-color:var(--red)}
.chatmic:disabled,.attbtn:disabled{background:var(--surface-3);color:var(--ink-5);cursor:not-allowed}
.chatmic:disabled:hover,.attbtn:disabled:hover{background:var(--surface-3);color:var(--ink-5);border-color:var(--line-2)}
/* ---------- chat attachments ---------- */
/* Row 1 of the composer grid; collapses to nothing while [hidden]. */
.attrow{grid-column:1/-1;display:flex;flex-direction:column;gap:6px;min-width:0}
.atttile{display:flex;align-items:center;gap:10px;min-height:44px;min-width:0;
  background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm);padding:6px 8px}
.attthumb{width:34px;height:34px;flex:none;border-radius:6px;object-fit:cover;background:var(--surface-3)}
.attdoc{display:grid;place-items:center;color:var(--ink-3)}
.attdoc svg{width:17px;height:17px}
.attmeta{min-width:0;flex:1 1 auto;display:flex;flex-direction:column}
.attmeta b{font-size:12.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.attmeta em{font-style:normal;font-size:11px;color:var(--ink-4)}
/* Same idiom as .kpi .track — no new colour tokens, which would otherwise have
   to be added to all three theme blocks or be a light-only value on a dark path. */
.attbar{flex:none;width:64px;height:4px;border-radius:3px;background:var(--surface-3);overflow:hidden}
.attbar i{display:block;height:100%;width:0;border-radius:3px;background:var(--brand);transition:width .15s}
.attx{flex:none}
.atterr{margin:0;font-size:11.5px;color:var(--red);line-height:1.45}
.atterr .lnk2{margin-left:6px}

/* There is no `img` rule anywhere in this stylesheet, so an unsized <img> lays
   out at its intrinsic width. The box is reserved from the stored w/h via --ar
   BEFORE the bytes arrive: paintLog measures scrollHeight before it mutates and
   restores by delta, so an image that gained height after decode would shove
   the reader up the log or break the bottom pin.
   260px is the measured .mbody budget at 375px; min() covers 320px devices. */
.matt{margin:6px 0 0;max-width:min(260px,100%);border-radius:10px;overflow:hidden;
  background:var(--surface-3);aspect-ratio:var(--ar,4/3);cursor:pointer}
.matt img{display:block;width:100%;height:100%;object-fit:cover}
.matt.attgone{opacity:.4}
/* Reuses the Uploads view's file vocabulary rather than inventing a second one. */
.mfile{display:inline-flex;align-items:center;gap:9px;margin:6px 0 0;max-width:100%;
  background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm);
  padding:8px 11px;min-height:44px;cursor:pointer;font:inherit;color:var(--ink);text-align:left}
.mfile:hover{border-color:var(--line-2);background:var(--surface-3)}
.mfile svg{width:16px;height:16px;flex:none;color:var(--ink-4)}

/* Full-bleed, not the centred .modal transform, and a literal dark backdrop —
   var(--ink) inverts to near-white in dark mode. */
.modal.lightbox{position:fixed;inset:0;top:0;left:0;transform:none;width:100%;max-height:none;
  border:0;border-radius:0;background:rgba(8,12,18,.96);display:grid;place-items:center;
  padding:calc(16px + env(safe-area-inset-top,0px)) 16px calc(16px + env(safe-area-inset-bottom,0px))}
.modal.lightbox.open{transform:none}
.modal.lightbox .dclose{position:absolute;top:calc(10px + env(safe-area-inset-top,0px));right:12px;
  z-index:1;color:#E6EDF4;background:rgba(255,255,255,.10);width:40px;height:40px;border-radius:10px}
.lbbody{max-width:100%;max-height:100%;display:grid;place-items:center}
.lbbody img{max-width:100%;max-height:88vh;object-fit:contain;border-radius:8px}

.chatfoot{margin:0;padding:0 16px 10px;font-size:11px;line-height:1.45;color:var(--ink-3);background:var(--surface)}
.chatfoot.err{color:var(--red)}
/* Both wordings are always in the DOM and CSS picks one, so a rotation or a
   window drag re-picks without any JS having to re-run paintComposer. */
.chatfoot .fshort{display:none}

/* ---- narrow shell (≤900px): the sidebar is a horizontal strip and .app
   stops being height-bounded, so chat has to re-establish its own height here
   REGARDLESS of whether it is showing one pane or two. This is a DELIBERATE
   exception to the single-page-scroll rule the Leads work established (see
   .app above) — a composer pinned under a scrolling log is the whole
   interaction, and without it the log grows unbounded and pushes the composer
   thousands of px down the page. 100dvh (not 100vh) so iOS Safari's toolbars
   don't hide the composer; --vvh is set from visualViewport so the on-screen
   keyboard shrinks the pane instead of covering it. */
@media(max-width:900px){
  /* Lock the DOCUMENT, not just the pane. Setting the shell to the visual
     viewport's height fixed its SIZE but not its POSITION: iOS scrolls the
     layout viewport to reveal a focused input, and with a one-viewport-tall
     shell that dragged the whole thing upward — composer over the status bar,
     an empty void where the log had been, keyboard underneath. Taking the body
     out of flow removes the scroll range that motion needs.
     position:fixed rather than overflow:hidden because iOS honours the former
     and routinely ignores the latter for its own reveal-scroll. */
  html.chatview{overscroll-behavior:none}
  body.chatview{position:fixed;top:0;left:0;right:0;bottom:0;width:100%;overflow:hidden;overscroll-behavior:none}
  body.chatview .app{height:var(--vvh,100dvh);min-height:0;overflow:hidden}
  body.chatview .scrollarea{padding:0}
  body.chatview .top{display:none}
  /* Chat is the one view where every vertical pixel is the product. The brand
     block wraps to three lines in the strip and costs ~30px of an 812px phone
     (21% of a 390px landscape phone went to chrome); the room name in the chat
     header already says where you are. */
  body.chatview .brand .bt{display:none}
  body.chatview .side{padding-top:calc(8px + env(safe-area-inset-top,0px));padding-bottom:8px}
  /* "Field log", "Uploads" and "Dark mode" each wrap to two lines in the
     strip, and one wrapped label sets the height of the whole row: 81px of an
     812px phone, 21% of a 390px landscape phone. */
  body.chatview .nav button,body.chatview .sidebtn{white-space:nowrap}
  .chatroot{grid-template-columns:216px minmax(0,1fr);border:0;border-radius:0}
}

/* ---- 700–900px is the TABLET band and it deliberately has no rules of its
   own: it keeps the two-pane desktop layout. A 4:3 tablet held upright is
   744–834px wide — room for a 216px rail and a 500px+ log — and the old
   900px cut-over handed it the phone layout, hiding the conversation list
   behind a back button for no reason.

   ---- phone (≤700px): one pane at a time. */
@media(max-width:700px){
  .chatroot{grid-template-columns:1fr}
  .chatroot[data-pane="room"] .chatrail{display:none}
  .chatroot[data-pane="rail"] .chatmain{display:none}
  .chatroot[data-pane="room"] .chatback{display:inline-grid}
  .chathead{grid-template-columns:auto minmax(0,1fr) auto;gap:0 8px;padding:8px 10px}
  /* min-width:0 is what keeps the room name alive: the h2 can shrink to zero
     and the nowrap chip cannot, so without a shrink limit on the chip a long
     DM name left the header showing ONLY "Private — you and <name>", clipped,
     on top of the notification bell. */
  .chattitle{grid-column:2;flex-wrap:nowrap;min-width:0;gap:7px}
  .chattitle h2{flex:1 1 auto;min-width:0}
  .chatmeta{display:none}
  /* The verbose "Open - anyone can join" chip does not fit next to the actions
     on a phone, and the same disclosure sits in the footer under the composer.
     The DM lock chip stays: that one is a privacy signal, not a nicety — but
     it drops to the bare word, because "you and <name>" only repeats the
     room title standing right next to it. */
  .chip.open{display:none}
  .chip.lock{flex:none;font-size:10.5px;padding:2px 7px}
  .chip.lock svg,.chip.lock .chiplong{display:none}
  /* 25px controls (5px padding + a 15px icon) are barely half of the 44px
     touch target every mobile HIG asks for, and the back chevron is the only
     way out of a room. */
  .chathead .iconbtn{width:40px;height:40px;padding:0;border-radius:10px}
  .chathead .btn.sm{height:36px}
  .chatlog{padding:10px 12px 4px}
  .chatform{padding:8px 12px 6px;gap:6px}
  .chatmic,.attbtn{width:40px;height:40px}
  .chatfoot{padding:0 12px calc(8px + env(safe-area-inset-bottom,0px))}
  .attrow{gap:4px}
  .atttile{min-height:36px;padding:4px 8px}
  .attthumb{width:28px;height:28px}
  /* The full disclosure wraps to three lines on a phone — ~60px of an 812px
     screen, permanently, for static boilerplate. An error still shows in
     full, because an error is the one thing here worth the space. */
  .chatfoot .ffull{display:none}
  .chatfoot .fshort{display:inline}
  /* The message actions sit in a third `auto` grid column, so a 55px "Remove"
     link narrowed the text of every message I sent. Icon-only reclaims ~30px
     of a 375px screen; the confirm step keeps its words. */
  .macts .lnk span{display:none}
  .macts .lnk.danger span,.macts .lnk[data-msg-del-cancel] span{display:inline}
  .railtop{display:grid;grid-template-columns:auto 1fr auto;gap:8px}
  .railtop .railsearch{grid-column:1/-1;height:36px;margin-top:0}
  .railtop [data-chat-new="dm"]{grid-column:1/-1;justify-content:center}
  .roomrow{padding:10px 8px}
}

/* ---- the status bar sits ON TOP of this view ----
   index.html ships `viewport-fit=cover` with
   `apple-mobile-web-app-status-bar-style: black-translucent`, and the manifest
   is `display:standalone` — so in a Home Screen web app the page extends UNDER
   the iOS status bar, ~47-59px of it on a notched iPhone.

   Every other view survives that because `.top` carries
   `padding-top: env(safe-area-inset-top)`. Chat is the one view that hides
   `.top` outright (above, to buy the log its vertical pixels), so NOTHING here
   supplied the inset: `.chathead` started at y=0 and the notifications bell sat
   at y=8-48 — entirely beneath the clock. You had to drag the page down to
   reach it.

   That only ever manifested in the Home Screen app, which is precisely what the
   push instructions tell everyone to install, so enabling notifications led
   straight into the one view where the toggle was hidden.

   Both panes need it: the rail is what you see first, the room header is where
   the bell lives. Padding rather than margin so each header's own background
   fills the status-bar strip instead of leaving a gap. Keyed on `body.chatview`
   (0,2,0) so it beats the plain `.chathead` padding shorthand above whatever
   the source order. */
@media(max-width:900px){
  body.chatview .chathead{padding-top:calc(8px + env(safe-area-inset-top,0px))}
  body.chatview .railtop{padding-top:calc(12px + env(safe-area-inset-top,0px))}
}

/* Five controls do not fit a 375px header: back, title, rename, Leave, bell.
   Measured, the title was squeezed to 144px while the actions took 155px.
   Renaming a channel is a rare admin action and an unpleasant one to do on a
   phone keyboard; the bell is the control people are actively hunting for. So
   rename goes at phone width — it is still there on tablet and desktop — and
   the bell gets room to be obvious. */
@media (max-width:700px){
  .chatacts [data-chat-rename]{display:none}
  .chatacts{gap:4px}
  /* The one control this view exists to surface right now. */
  .chatacts [data-chat-push]{width:40px;height:40px}
  .chatacts [data-chat-push].on{color:var(--brand);background:var(--brand-soft)}
}

/* Hover-reveal is fine with a mouse, but a touch device never hovers — without
   this the Remove control is unreachable on a phone. */
@media(hover:none),(pointer:coarse){
  .macts{opacity:1}
  /* Every message carries a react button now, not only your own, so on a
     phone — where nothing hides until hover — it is drawn quietly. Full
     strength would put a smiley at the end of every line of the log. */
  .macts .lnk.rxadd{opacity:.5}
  .macts .lnk.rxadd.on{opacity:1}
  .rx{height:28px;padding:0 10px 0 8px}
  .rxopt{width:38px;height:38px;font-size:20px}
  /* The icon itself is 14px. Without a hit area around it, the one destructive
     control in chat is a 14x14 target. */
  .macts .lnk{min-width:32px;min-height:32px;justify-content:center}
}

/* A phone on its side is 844px WIDE and 390px tall, so every width-keyed rule
   above treats it as roomy while the log — the only part that matters — gets
   ~100px, two messages. Keyed on height, and placed last so it wins wherever
   the two overlap. */
@media(max-height:520px){
  .chatmeta{display:none}
  .chathead{padding:8px 12px}
  .chatlog{padding:8px 14px 4px}
  .chatform{padding:8px 14px 6px}
  .chatfoot{padding:0 14px calc(6px + env(safe-area-inset-bottom,0px))}
  .chatfoot .ffull{display:none}
  .chatfoot .fshort{display:inline}
  .railtop{padding:8px}
}

/* ---------- contracts, vendor registration, invoices ----------
   No new color token is introduced. Every state reuses an existing --x /
   --x-soft pair, which matters because the dark palette is declared VERBATIM
   in three separate blocks at the top of this file ([data-theme="dark"] and
   the prefers-color-scheme fallback); a new token added to only two of them
   leaves a light-only value on one of the dark paths.
   Every numeric cell gets tabular-nums, which is the house rule on every
   number in the app (.kpi .v, .drive b, .chip .n). Columns of dollars without
   it visibly jitter as the digits change. */
.mpill{display:inline-block;font-size:11px;font-weight:600;line-height:19px;padding:0 8px;border-radius:999px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
.renew b{font-weight:600;font-variant-numeric:tabular-nums;font-size:12.5px}
.renew small{display:block;color:var(--ink-4);font-size:10.5px;font-variant-numeric:tabular-nums}
.renew.soon small{color:var(--amber)}
.renew.warn b,.renew.warn small{color:var(--amber)}
.renew.bad b,.renew.bad small{color:var(--red)}
.leadtable td.c-cval,.leadtable td.c-cbus,.leadtable td.m-val,.leadtable td.m-tcv{font-variant-numeric:tabular-nums}
.leadtable td.c-cval b,.leadtable td.m-val b{font-weight:600}
.cflag{font-size:10.5px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--amber);
  background:var(--amber-soft);border-radius:4px;padding:1px 5px;margin-left:4px}

/* The Contracts view's own table. A SECOND fixed-layout table with its own
   width block summing to 100 and its own tier ladder — that independence is
   the second reason this is a separate view rather than more columns on Leads.
   Its wrapper is deliberately NOT a scroll container either. */
.moneytable .m-dist{width:18%}
.moneytable .m-cstat{width:11%}
.moneytable .m-vstat{width:11.5%}
.moneytable .m-val{width:9.25%}
.moneytable .m-tcv{width:9.25%}
.moneytable .m-end{width:15.5%}
.moneytable .m-who{width:12%}
.moneytable .m-it{width:11.5%}
.moneytable.inv .m-dist{width:24%}
.moneytable.inv .m-cstat{width:12%}
.moneytable.inv .m-end{width:16%}
.moneytable.inv .m-val{width:14%}
@media (max-width:1180px){ .moneytable .m-it{display:none} }
@media (max-width:1040px){ .moneytable .m-tcv{display:none} }
@media (max-width:900px){ .moneytable .m-who{display:none} }
@media (max-width:640px){
  .moneytable .m-dist,.moneytable .m-cstat,.moneytable .m-vstat,.moneytable .m-val,
  .moneytable .m-tcv,.moneytable .m-end,.moneytable .m-who,.moneytable .m-it,
  /* .moneytable.inv is (0,3,0) and would otherwise beat the reset above. */
  .moneytable.inv .m-dist,.moneytable.inv .m-cstat,
  .moneytable.inv .m-end,.moneytable.inv .m-val{width:auto}
  .moneytable td.m-dist{grid-column:1/-1;order:1}
  .moneytable td.m-cstat{order:2}
  .moneytable td.m-vstat{order:3}
  .moneytable td.m-val{order:4}
  .moneytable td.m-tcv{order:5;display:table-cell}
  .moneytable td.m-end{order:6}
  .moneytable td.m-who{order:7;display:table-cell;grid-column:1/-1}
  .moneytable td.m-it{order:8;display:table-cell;grid-column:1/-1}
  .moneytable td.m-cstat .mpill,.moneytable td.m-vstat .mpill{width:100%;text-align:center}
  /* 25% of a contract card was labels over em dashes — IT contact alone was
     blank on 4 of 7 rows, Renews on 3, Annual and Term value on 2, and on the
     Invoices tab Paid is blank on every invoice that is merely sent. The
     status pill already says what a missing paid-date means. */
  .moneytable td.nodata{display:none}
}

/* invoice modal: line items */
/* Two rows, not one: the description's optional second line sits under it and
   spans the whole width, because it is prose and the numbers beside it are not.
   `align-items:center` would centre the tall first row against the short
   second one, so the row aligns to the START and the amount is nudged instead. */
.iline{display:grid;grid-template-columns:minmax(0,1fr) 76px 92px 92px 28px;gap:7px 8px;
  align-items:start;margin-bottom:13px}
.iline input{width:100%;min-width:0}
.iline .il-sub{grid-column:1/-1;font-size:12px;padding:6px 10px;color:var(--ink-2)}
.iline .il-amt,.iline .il-x{margin-top:9px}
.iline .il-qty,.iline .il-unit{text-align:right;font-variant-numeric:tabular-nums}
.il-amt{text-align:right;font-size:12.5px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--ink-2)}
button.il-x{background:none;border:0;color:var(--ink-4);cursor:pointer;display:inline-flex;padding:3px;border-radius:5px}
button.il-x:hover{color:var(--red);background:var(--red-soft)}

button.il-x svg{width:14px;height:14px}
span.il-x{display:block}
.invtot{margin:12px 0 4px;padding-top:10px;border-top:1px solid var(--line)}
.invtot div{display:flex;justify-content:space-between;font-size:13px;padding:3px 0;color:var(--ink-2)}
.invtot b{font-variant-numeric:tabular-nums;font-weight:600;color:var(--ink)}
.invtot .tot{border-top:1px solid var(--line);margin-top:5px;padding-top:8px;font-size:15px}
.invtot .tot b{font-size:17px}
.ro{font-size:13px;color:var(--ink-2);padding:9px 0}
.invrow .logdate .mpill{margin-left:6px}

/* Who signed, under the status pill on an accepted quote. Small and quiet —
   the pill carries the state, this only answers "who agreed". */
.qsig{margin-top:3px;font-size:11.5px;color:var(--ink-3);line-height:1.35}
/* The quotes table's own column widths; .moneytable.qt is (0,3,0) like the
   invoice modifier beside it, and the max-640 reset below has to beat both. */
.moneytable.qt .m-dist{width:26%}
.moneytable.qt .m-cstat{width:18%}
.moneytable.qt .m-end{width:16%}
.moneytable.qt .m-val{width:14%}
@media (max-width:640px){
  .moneytable.qt .m-dist,.moneytable.qt .m-cstat,
  .moneytable.qt .m-end,.moneytable.qt .m-val{width:auto}
}

@media (max-width:560px){
  /* Three columns carrying two different rows: the description and its delete
     button above, the three numbers below, the optional second line across the
     foot.

     The column widths are sized for the NUMBER row, because that is the one
     with three things in it. Previously they were sized for the description
     row — 1fr / 72 / 24 — which left the amount in the 24px delete-button
     column and clipped it to "$5,200.(" on every phone. The delete button is
     the one that can live in an oversized column, because justify-self pins it
     to the edge whatever the column does.

     "s" is the second line under a description, and it needs a row of its own:
     without one it auto-placed outside the named areas and shoved the numbers
     about. */
  .iline{grid-template-columns:72px minmax(0,1fr) 92px;
    grid-template-areas:"d d x" "q u a" "s s s";gap:6px 8px;align-items:center}
  .iline .il-desc{grid-area:d}
  .iline .il-sub{grid-area:s}
  .il-amt{grid-area:a}
  button.il-x,span.il-x{grid-area:x;justify-self:end}
}

/* ---------- sending an invoice ----------
   The envelope, not the letter. What is shown here is only what the panel
   knows for certain — the addresses, the subject, the attachment's name. The
   message body and the PDF are the server's, and a browser-side preview of
   them would be a promise about somebody else's output. */
.testbar{display:flex;gap:10px;align-items:flex-start;margin-bottom:16px;padding:11px 13px;
  border-radius:9px;background:var(--amber-soft);color:var(--ink);font-size:12.5px;line-height:1.5}
.testbar svg{width:17px;height:17px;flex:none;margin-top:1px;color:var(--amber)}
.testbar b{font-weight:600}

.envl{border:1px solid var(--line);border-radius:10px;overflow:hidden}
.envl-r{display:flex;gap:14px;padding:10px 13px;border-bottom:1px solid var(--line);font-size:13px}
.envl-r:last-child{border-bottom:0}
/* A fixed label column so the three values start on one edge — the whole point
   of showing an envelope is that it can be scanned, not read. */
.envl-r>span{flex:none;width:62px;font-size:11px;color:var(--ink-3);padding-top:3px}
.envl-r>div{min-width:0;flex:1;color:var(--ink);word-break:break-word}
/* The same control the rest of the panel uses — .fld can't be reused here
   because the label sits beside the field rather than above it. */
.envl-r input{width:100%;font:inherit;font-size:13px;color:var(--ink);background:var(--surface);
  border:1px solid var(--line-2);border-radius:var(--r-sm);padding:7px 10px;
  transition:border-color .12s,box-shadow .12s}
.envl-r input:focus{outline:none;border-color:var(--brand);box-shadow:var(--ring)}
.envl-r em{display:block;margin-top:5px;font-style:normal;font-size:11.5px;color:var(--ink-3)}
.envl-r em.warn{color:var(--amber)}

.pdfhint{margin:9px 2px 0}

/* THE PDF ITSELF, not a second rendering of it. An iframe because a blob URL
   in one is the only way to show a real PDF without an endpoint to link to,
   and the native viewer brings its own zoom and download for free — which is
   why the toolbar is deliberately NOT suppressed with #toolbar=0.
   White, always: the page inside is paper and does not follow the theme. */
.pdfprev{height:460px;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:#fff}
.pdfprev iframe{display:block;width:100%;height:100%;border:0}
.pdfmsg{height:100%;display:flex;align-items:center;justify-content:center;
  padding:16px;text-align:center;font-size:13px;color:#6b7784;background:var(--surface-2)}
.pdfmsg.bad{color:var(--red)}
@media (max-width:760px){ .pdfprev{height:340px} }

.sendrow{padding:9px 0;border-bottom:1px solid var(--line)}
.sendrow:last-child{border-bottom:0}
.sendwho{display:flex;align-items:center;gap:7px;font-size:13px;color:var(--ink)}
.sendwho b{font-weight:600}
.sendwhen{margin-top:2px;font-size:11.5px;color:var(--ink-3);line-height:1.5}
.sendrow.bad .sendwhen{color:var(--red)}
.sendrow.wait .sendwho b{color:var(--ink-2)}

/* The capability link, shown before it is mailed. Monospaced and selectable —
   an operator asked to resend it needs to be able to copy it. */
.qlink{display:block;font:12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  color:var(--ink);background:var(--surface-2);border:1px solid var(--line);
  border-radius:7px;padding:7px 9px;word-break:break-all;user-select:all}
/* ---------- the address picker ----------
   OUR input and OUR list, so both carry the drawer's own rules — the same
   surface, border, radius, focus ring and type as every other field beside
   them. The first attempt embedded Google's <gmp-place-autocomplete>, whose
   input lives in a shadow root with its own surface and metrics: it rendered
   as a black box with a second magnifier inside a field styled like the rest,
   and nothing reachable from outside could fix it.

   Hidden until the script is actually up, so a blocked or keyless load leaves
   no empty affordance behind. */
.addrpick{position:relative;margin-bottom:9px}
.addrfield{display:flex;align-items:center;gap:9px;padding:0 11px;
  background:var(--surface);border:1px solid var(--line-2);border-radius:var(--r-sm);
  transition:border-color .12s,box-shadow .12s}
.addrfield:focus-within{border-color:var(--brand);box-shadow:var(--ring)}
.addrfield>svg{width:15px;height:15px;flex:none;color:var(--ink-4)}
.addrfield input{flex:1;min-width:0;height:36px;font:inherit;font-size:13px;color:var(--ink);
  background:none;border:0;padding:0}
.addrfield input:focus{outline:none}
.addrfield input::placeholder{color:var(--ink-4)}
.apx{background:none;border:0;padding:4px;border-radius:5px;color:var(--ink-4);cursor:pointer;
  display:inline-flex;flex:none;opacity:0;pointer-events:none;transition:opacity .12s,color .12s}
/* Only offered once there is something to clear. */
.addrfield:focus-within .apx{opacity:1;pointer-events:auto}
.apx svg{width:13px;height:13px}
.apx:hover{color:var(--ink-2);background:var(--surface-2)}

/* The suggestions. Overlaid rather than in flow, so the fields below do not
   jump every time somebody types a character. */
.aclist{position:absolute;z-index:6;top:calc(100% + 5px);left:0;right:0;
  background:var(--surface);border:1px solid var(--line);border-radius:var(--r-sm);
  box-shadow:var(--sh-2);overflow:hidden;padding:4px}
.acopt{display:flex;align-items:flex-start;gap:9px;width:100%;text-align:left;
  background:none;border:0;border-radius:6px;padding:8px 9px;cursor:pointer;
  font:inherit;color:var(--ink)}
.acopt>svg{width:14px;height:14px;flex:none;margin-top:2px;color:var(--ink-4)}
.acopt span{min-width:0;display:block}
.acopt b{display:block;font-size:13px;font-weight:600;line-height:1.35;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.acopt i{display:block;font-style:normal;font-size:11.5px;color:var(--ink-3);margin-top:1px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* .on is the keyboard cursor; hover paints the same so the two never disagree
   about which row is about to be chosen. */
.acopt:hover,.acopt.on{background:var(--brand-soft)}
.acopt:hover>svg,.acopt.on>svg,.acopt:hover b,.acopt.on b{color:var(--brand-ink)}

/* What Google actually returned, drawn the way a document draws it.
   The line before this one printed the same characters as the field above it
   with a different separator, which proved nothing and read as a duplicate.
   Breaking it where the invoice breaks it is what makes a city stuck inside
   line 1, or a missing ZIP, visible at a glance.

   Two states, keyed off data-mode: green while the field still matches what
   Google returned, amber the moment it does not — because that is the moment
   saveDrawer stops keeping the parts. In the amber state the two lines are
   hidden rather than restated: they are about to be untrue. */
.addrcard{display:flex;align-items:flex-start;gap:9px;margin-top:8px;padding:8px 11px;
  border:1px solid var(--green);background:var(--green-soft);border-radius:var(--r-sm)}
.addrcard>svg{width:14px;height:14px;flex:none;margin-top:2px;color:var(--green)}
.acbody{min-width:0}
.acdoc{display:block;margin-bottom:3px}
.acdoc b{display:block;font-size:12.5px;font-weight:600;line-height:1.45;color:var(--ink);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.acdoc b:empty{display:none}
.acnote{display:block;font-style:normal;font-size:11px;line-height:1.45;color:var(--green)}

.addrcard[data-mode=typed]{border-color:var(--amber);background:var(--amber-soft)}
.addrcard[data-mode=typed]>svg{color:var(--amber)}
.addrcard[data-mode=typed] .acdoc{display:none}
.addrcard[data-mode=typed] .acnote{color:var(--amber)}

/* A note under a field explaining why it offers what it offers — a filter
   nobody can see reads as a bug. */
.fldnote{margin-top:6px;font-size:11.5px;color:var(--ink-3);line-height:1.45}
.fldnote b{color:var(--ink-2);font-weight:600}

/* On the invoice or quote itself: it already went out, and to where. */
.sentnote{display:flex;gap:8px;align-items:center;margin:2px 0 14px;padding:9px 11px;
  border-radius:8px;background:var(--green-soft);color:var(--ink);font-size:12.5px}
.sentnote svg{width:15px;height:15px;flex:none;color:var(--green)}
/* Sent but not yet signed — amber, because it is waiting on somebody. */
.sentnote.amber{background:var(--amber-soft)}
.sentnote.amber svg{color:var(--amber)}
.sentnote b{font-weight:600}

/* ---------- the invoice document ----------
   There is NO stylesheet for it here any more. It is drawn once, by
   functions/invoiceDoc.js with pdfkit, and both the send preview and Print show
   that same file — so there is no second rendering to keep in step and no
   @media print rule to maintain. See the note above openInvoiceDoc in app.js.
   `.invdoc`, `.inv-*` and `#invprint` are all gone with it. */

/* ---------- conferences ----------
   Its own fixed-layout table with its own width block summing to 100 and its
   own tier ladder, like .moneytable. Wrapper is NOT a scroll container. */
/* The VSI user change log. A short list under the table, not a screen: it
   answers "did somebody just move this account", and it is the only place in
   the panel that can answer it. */
.chlog{margin-top:14px}
.chlog ul{list-style:none;margin:10px 0 0;padding:0;display:flex;flex-direction:column;gap:9px}
.chlog li{font-size:12.5px;color:var(--ink-2);line-height:1.5;padding-bottom:9px;border-bottom:1px solid var(--line)}
.chlog li:last-child{border-bottom:0;padding-bottom:0}
.chlog b{color:var(--ink);font-weight:600}
.chlog i{color:var(--ink-3);font-style:normal}
.chwho{font-weight:600;color:var(--ink)}
.chwhen{display:block;font-size:11px;color:var(--ink-3);margin-top:2px;font-variant-numeric:tabular-nums}

.conftable .k-name{width:22%}
.conftable .k-tier{width:10%}
.conftable .k-stat{width:11%}
/* Wide enough for the longest written-out date, "September 30th, 2026". */
.conftable .k-when{width:16%}
.conftable .k-where{width:15%}
.conftable .k-spend{width:11%}
.conftable .k-value{width:12%}
.conftable .k-go{width:3%}
.conftable td.k-value b.ok,.moneytable td b.ok{color:var(--green)}
/* The generic radius rule targets thead th.c-go; this table's last column is
   k-go, so without this the header's top-right corner stays square. */
.conftable thead th.k-go{border-top-right-radius:var(--r)}
.cfyr{font-weight:600;font-variant-numeric:tabular-nums;color:var(--ink-3);font-size:12px}
@media (max-width:1180px){ .conftable .k-where{display:none} }
@media (max-width:1040px){ .conftable .k-tier{display:none} }
@media (max-width:640px){
  .conftable .k-name,.conftable .k-tier,.conftable .k-stat,.conftable .k-when,
  .conftable .k-where,.conftable .k-spend,.conftable .k-value,.conftable .k-go{width:auto}
  .conftable td.k-name{grid-column:1/-1;order:1}
  .conftable td.k-tier{order:2;display:table-cell}
  .conftable td.k-stat{order:3}
  .conftable td.k-when{order:4}
  .conftable td.k-where{order:5;display:table-cell;grid-column:1/-1}
  .conftable td.k-spend{order:6}
  .conftable td.k-value{order:7}
  .conftable td.k-go{display:none}
  .conftable td.k-tier .mpill,.conftable td.k-stat .mpill{width:100%;text-align:center}
}

/* the itemized budget grid in the edition modal */
.costhead,.costrow{display:grid;grid-template-columns:minmax(0,1fr) 104px 104px;gap:8px;align-items:center}
.costhead{margin-bottom:5px}
.costhead span{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);text-align:right}
.costhead .cl{text-align:left}
.costrow{margin-bottom:6px}
.costrow .cl{font-size:12.5px;color:var(--ink-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.costrow input{width:100%;text-align:right;font-variant-numeric:tabular-nums}
.costrow .ca{background:var(--surface-2)}

/* "districts we met here" — a checkbox set, not a multi-select */
.metgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:6px;
  max-height:230px;overflow-y:auto;overscroll-behavior:contain;padding:2px;
  border:1px solid var(--line);border-radius:var(--r-sm);background:var(--surface-2)}
.metopt{display:flex;align-items:center;gap:7px;font-size:12.5px;padding:6px 8px;border-radius:6px;
  cursor:pointer;background:var(--surface);border:1px solid var(--line);min-width:0}
.metopt span,.metopt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.metopt input{flex:none;margin:0}
.metopt:hover{border-color:var(--line-2)}
.metopt.on{border-color:var(--green);background:var(--green-soft);color:var(--green);font-weight:600}
/* The trip modal puts these inside a .fld, where `.fld label` (0,1,1) beats
   `.metopt` (0,1,0) and would take away its display:flex and its font-size —
   the same collision that had the Type picker's icons sitting on a baseline.
   Everywhere else .metopt lives outside a .fld and needs none of this. */
.fld .metopt{display:flex;font-size:12.5px;margin:0}
.fld .metgrid{margin-bottom:8px}
/* `.fld input` sets width:100%, which is right for a text box and catastrophic
   for a checkbox: inside a .fld the box stretched to 198px, `.metopt input`
   pinned it there with flex:none, and the name beside it was squeezed to zero
   width — a row of green chips with no names on them. `.metopt input` only
   ever overrode flex and margin, so the width came straight through. */
.fld .metopt input[type=checkbox]{width:auto;height:auto;flex:none}
.fld .metopt span{flex:1 1 auto;min-width:0}

/* Inviting someone who is not on the access list. */
.attadd{display:flex;gap:8px;align-items:center}
.fld .attadd input{flex:1 1 auto;min-width:0}
.attadd .btn{flex:none}
.attchips{display:flex;flex-wrap:wrap;gap:6px}
.attchips:not(:empty){margin-top:8px}
.attchip{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;
  background:var(--surface-3);border:1px solid var(--line-2);border-radius:999px;
  padding:3px 4px 3px 10px;color:var(--ink-2);max-width:100%}
.attchip button{display:inline-flex;align-items:center;justify-content:center;
  width:18px;height:18px;padding:0;border:0;border-radius:50%;cursor:pointer;
  background:none;color:var(--ink-4)}
.attchip button:hover{background:var(--red-soft);color:var(--red)}
.attchip button svg{width:11px;height:11px}

/* contacts, repeated inside the conference drawer */
.ctrow{border:1px solid var(--line);border-radius:var(--r-sm);padding:11px 12px 8px;margin-bottom:9px;background:var(--surface-2)}
.ctdel{margin-top:2px}
.edrow .logdate .mpill{margin-left:6px}

@media (max-width:560px){
  .costhead,.costrow{grid-template-columns:minmax(0,1fr) 78px 78px;gap:6px}
  .metgrid{grid-template-columns:1fr}
}

/* ---------- contacts ----------
   Its own fixed-layout table, widths summing to 100, its own tier ladder.
   The 640px reset below matches these selectors' specificity exactly — a
   higher-specificity width rule beats the reset and collapses the phone cards,
   which is a bug this codebase has now shipped twice. */
.cttable .t-star{width:4%}
.cttable .t-name{width:22%}
.cttable .t-role{width:13%}
.cttable .t-org{width:20%}
.cttable .t-email{width:20%}
.cttable .t-phone{width:12%}
.cttable .t-state{width:6%}
.cttable .t-go{width:3%}
.cttable thead th.t-go{border-top-right-radius:var(--r)}
.cttable td.t-email a,.cttable td.t-phone a{color:var(--ink-2);font-size:12.5px}
.cttable td.t-email a{word-break:break-all}
.cttable td.t-phone{font-variant-numeric:tabular-nums}
.cttable td.t-state{font-weight:600;color:var(--ink-3);font-size:11.5px}
@media (max-width:1180px){ .cttable .t-org{display:none} }
@media (max-width:1040px){ .cttable .t-state{display:none} }
@media (max-width:640px){
  .cttable .t-star,.cttable .t-name,.cttable .t-role,.cttable .t-org,
  .cttable .t-email,.cttable .t-phone,.cttable .t-state,.cttable .t-go{width:auto}
  /* The star was `grid-column:1/-1` at order 0 — a full-width row holding one
     22px glyph, above the name it belongs to, on every card. Lifted out of the
     grid entirely and pinned to the card's top-right, which is both where a
     card's affordance is looked for and ~34px per card reclaimed. */
  .cttable tbody tr{position:relative;padding-right:46px}
  .cttable td.t-star{position:absolute;top:6px;right:6px;padding:0;width:auto}
  .cttable td.t-name{grid-column:1/-1;order:1}
  .cttable td.t-role{order:2}
  .cttable td.t-state{order:3;display:table-cell}
  .cttable td.t-org{order:4;grid-column:1/-1;display:table-cell}
  .cttable td.t-email{order:5;grid-column:1/-1}
  .cttable td.t-phone{order:6}
  .cttable td.t-go{display:none}
  .cttable td.t-role .mpill{width:100%;text-align:center}
  /* A contact with no org, email, phone or state rendered four labelled rows
     of "—": ~150px of a 375px screen spent saying nothing, and the conference
     and association records that carry only a name are exactly the ones where
     all four are blank. The desktop table keeps them — a column with a hole in
     it cannot be read — and the editor still shows every field.
     The marker is `nodata`, NOT `empty`: `.empty` is the global no-results
     panel (56px padding, centred, muted), and a bare `empty` on a <td> handed
     28 desktop cells centre-alignment and the wrong ink while every neighbour
     stayed left-aligned. Same trap for any table that copies this. */
  .cttable td.nodata{display:none}
}

/* the star: a one-click judgement, so it lives in the row */
.starbtn{background:none;border:0;padding:3px;cursor:pointer;color:var(--ink-5);
  display:grid;place-items:center;border-radius:6px;line-height:0}
.starbtn svg{width:16px;height:16px;fill:none}
.starbtn:hover{color:var(--amber);background:var(--amber-soft)}
.starbtn.on{color:var(--amber)}
.starbtn.on svg{fill:var(--amber)}
.ststar{display:inline-flex;color:var(--amber);margin-left:5px;vertical-align:-3px}
.ststar svg{width:13px;height:13px;fill:var(--amber)}
.segbar button .stx{width:13px;height:13px;fill:none;opacity:.8}
.segbar button[aria-pressed="true"] .stx{fill:var(--amber);color:var(--amber);opacity:1}
.ctrowline .logdate .mpill{margin-left:6px}
.ctwrap .logtop{margin-bottom:2px}

/* ---------- VSI users (cross-instance) ----------
   Own fixed-layout table, widths summing to 100, tier ladder, and a 640px
   reset at MATCHING specificity. */
.envrow{display:flex;align-items:center;gap:7px;flex-wrap:wrap}
/* The quiet counterpart to .mnote in the VSI user drawer: a fact worth reading
   before you save, not a warning. .mnote is amber and loud, and using it for a
   capital letter is how an operator learns to ignore amber. */
.dnote{margin:2px 0 4px;padding:9px 11px;border-radius:9px;background:var(--surface-2);
  border:1px solid var(--line);color:var(--ink-3);line-height:1.55}
.dnote code{font-size:11.5px;color:var(--ink-2)}


.usertable .u-who{width:30%}
.usertable .u-auth{width:16%}
.usertable .u-role{width:14%}
.usertable .u-county{width:22%}
.usertable .u-state{width:10%}
.usertable .u-go{width:8%}
.usertable thead th.u-go{border-top-right-radius:var(--r)}
.usertable td.u-state{font-weight:600;color:var(--ink-3);font-size:11.5px}
/* An off-enum value is shown verbatim, and looks like what it is: real data
   the product tolerates, not a rendering failure. */
.offenum{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;
  color:var(--red);background:var(--red-soft);border-radius:4px;padding:1px 6px}
.warnsub{color:var(--amber)!important;font-weight:600}
@media (max-width:1180px){ .usertable .u-state{display:none} }
@media (max-width:1040px){ .usertable .u-role{display:none} }
@media (max-width:640px){
  .usertable .u-who,.usertable .u-auth,.usertable .u-role,
  .usertable .u-county,.usertable .u-state,.usertable .u-go{width:auto}
  .usertable td.u-who{grid-column:1/-1;order:1}
  .usertable td.u-auth{order:2}
  .usertable td.u-role{order:3;display:table-cell}
  .usertable td.u-county{order:4;grid-column:1/-1}
  .usertable td.u-state{order:5;display:table-cell}
  .usertable td.u-go{display:none}
  .usertable td.u-auth .mpill{width:100%;text-align:center}
  .usertable td.nodata{display:none}
}
/* A state profile's empty county is the CORRECT configuration. It must not
   read like the missing-value dash next to it. */
.statewide{font-size:11.5px;font-weight:600;color:var(--teal);background:var(--teal-soft);
  border-radius:4px;padding:1px 7px;display:inline-block}
/* VSI Queue — the merged Uploads + Issues surface. Reuses the whole .up-*
   layer (Issues already did), and adds only the per-district kind mix. */
.q-mix{font-size:11px;color:var(--ink-4);margin-left:2px}
@media(max-width:640px){ .q-mix{display:none} }

/* ===================== Meetings ======================================== */
/* The dashboard. Ordinary cards — this is a list of meetings, and it should
   look like the rest of the app. The ROOM is the only part that goes dark. */
.mtwrap{display:flex;flex-direction:column;gap:14px}
.mtactive{border-color:var(--line-2)}
.mtnone{display:flex;flex-direction:column;align-items:flex-start;gap:4px;padding:6px 0 2px}
.mtnone b{font-size:13.5px}
.mtnone span{font-size:12.5px;color:var(--ink-3);margin-bottom:10px}

.mtlive{border:1px solid var(--brand);background:var(--brand-soft);border-radius:var(--r);padding:14px 16px}
.mtlive+.mtlive{margin-top:10px}
.mtlivetop{display:flex;align-items:center;gap:9px;margin-bottom:6px}
.mtlivetop b{font-size:14.5px;color:var(--ink)}
/* A live meeting is the one thing on this page that is happening RIGHT NOW,
   so it gets the only animated element in the app. Stilled for anyone who
   asked for less motion. */
.mtdot{width:9px;height:9px;border-radius:50%;background:var(--green);flex:none;
  box-shadow:0 0 0 0 color-mix(in srgb,var(--green) 70%,transparent);animation:mtpulse 2s infinite}
@keyframes mtpulse{70%{box-shadow:0 0 0 7px transparent}100%{box-shadow:0 0 0 0 transparent}}
@media(prefers-reduced-motion:reduce){.mtdot{animation:none}}
.mtlivetag{font-size:10px;font-weight:700;letter-spacing:.08em;color:var(--green);
  background:var(--green-soft);border-radius:5px;padding:2px 7px}
.mtlivemeta{display:flex;flex-wrap:wrap;gap:4px 14px;font-size:12px;color:var(--ink-2);margin-bottom:12px}
.mtliveacts{display:flex;gap:9px;align-items:center;flex-wrap:wrap}

.mtlist{display:flex;flex-direction:column}
.mtrow{display:grid;grid-template-columns:78px minmax(0,1fr) auto 16px;align-items:center;gap:12px;
  width:100%;padding:9px 8px;margin:0 -8px;border:0;border-radius:var(--r-sm);background:none;
  cursor:pointer;text-align:left;font:inherit;color:inherit}
.mtrow:hover{background:var(--surface-3)}
.mtrow+.mtrow{border-top:1px solid var(--line)}
.mtwhen{display:flex;flex-direction:column;gap:1px}
.mtwhen b{font-size:12.5px;font-weight:600;font-variant-numeric:tabular-nums}
.mtwhen em{font-style:normal;font-size:11px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.mtmid{min-width:0;display:flex;flex-direction:column;gap:1px}
.mtmid b{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mtmid em{font-style:normal;font-size:11.5px;color:var(--ink-3)}
.mtdur{font-size:11.5px;color:var(--ink-3);font-variant-numeric:tabular-nums;white-space:nowrap}
.mtgo{color:var(--ink-4);display:inline-flex}
.mtgo svg{width:15px;height:15px}
.nb.live{background:var(--green);color:#fff}

.mtfacts{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-bottom:6px}
.mtfacts>div{background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-sm);padding:9px 11px}
.mtfacts span{display:block;font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3)}
.mtfacts b{font-size:13.5px;font-weight:600}
.mtattlist{display:flex;flex-direction:column;gap:2px}
.mtattrow{display:flex;align-items:center;gap:10px;padding:6px 0}
.mtattrow+.mtattrow{border-top:1px solid var(--line)}
.mtattn{flex:1;min-width:0;display:flex;flex-direction:column}
.mtattn b{font-size:12.5px;font-weight:600}
.mtattn em{font-style:normal;font-size:10.5px;color:var(--ink-3)}
.mtattd{font-size:12px;color:var(--ink-2);font-variant-numeric:tabular-nums}

/* ---- the stage: lobby and room ---------------------------------------- */
/* Fixed, above everything, and its own dark ground regardless of theme — a
   light UI around video is glare, and every conferencing tool goes dark for
   the same reason. */
body.meeting{overflow:hidden}
.meetstage{position:fixed;inset:0;z-index:60;background:#0A0F16;color:#E9EEF4;
  display:flex;flex-direction:column;overscroll-behavior:none}

.mtlobby{margin:auto;width:min(560px,94vw);max-height:100%;overflow-y:auto;padding:24px 20px}
.mtkicker{font-size:10px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:#7DB4E8}
.mtlobbyhead h1{font-size:22px;letter-spacing:-.02em;margin:4px 0 16px}
.mtpreviewwrap{position:relative;aspect-ratio:16/9;border-radius:12px;overflow:hidden;
  background:#111A24;border:1px solid #1E2A37}
.mtpreview{width:100%;height:100%;object-fit:cover;transform:scaleX(-1)}
.mtpreviewoff{position:absolute;inset:0;display:grid;place-items:center;gap:8px;align-content:center;
  background:#111A24;color:#72808F;font-size:12px}
.mtpreviewoff em{font-style:normal}
.mtav{width:56px;height:56px;border-radius:50%;display:grid;place-items:center;
  background:#1E2A37;color:#9FADBE;font-size:18px;font-weight:700}
.mtav.sm{width:26px;height:26px;font-size:10px}
/* Proof the microphone works, before six people are waiting to find out. */
.mtlevel{position:absolute;left:10px;right:10px;bottom:10px;height:4px;border-radius:2px;background:rgba(255,255,255,.14)}
.mtlevel i{display:block;height:100%;width:0;border-radius:2px;background:#3DB88E;transition:width .1s linear}
.mterr{margin-top:12px;padding:11px 13px;border-radius:10px;background:#2C1615;border:1px solid #E4736F}
.mterr b{display:block;font-size:12.5px;color:#E4736F}
.mterr span{display:block;font-size:12px;color:#9FADBE;margin-top:3px;line-height:1.5}
.mtdevices{display:grid;gap:10px;margin-top:16px}
.mtdevices .fld{margin:0}
.mtdevices span{display:block;font-size:11px;color:#9FADBE;margin-bottom:4px}
.mtdevices select{width:100%;height:38px;font:inherit;font-size:13px;background:#111A24;color:#E9EEF4;
  border:1px solid #2A3846;border-radius:8px;padding:0 10px}
.mtlobbyacts{display:flex;align-items:center;gap:9px;margin-top:18px;flex-wrap:wrap}
.mtspacer{flex:1}
.mtwho{font-size:11.5px;color:#72808F;margin:12px 0 0}

.mtbtn{display:inline-flex;align-items:center;gap:7px;height:38px;padding:0 13px;border-radius:9px;
  border:1px solid #2A3846;background:#151F2A;color:#E9EEF4;font:inherit;font-size:12.5px;cursor:pointer}
.mtbtn:hover{border-color:#3d4d5e}
.mtbtn svg{width:16px;height:16px}
.mtbtn.off{background:#2C1615;border-color:#E4736F;color:#E4736F}
.mtbtn.on{background:#132738;border-color:#579BDD;color:#7DB4E8}

/* ---- room -------------------------------------------------------------- */
.mtroom{display:flex;flex-direction:column;height:100%;min-height:0}
.mtroomhead{display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:1px solid #16202B;flex:none}
.mtroomtitle{display:flex;align-items:baseline;gap:9px;min-width:0}
.mtroomtitle b{font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mtclock{font-size:11.5px;color:#72808F;font-variant-numeric:tabular-nums}
.mtlayoutpick{margin-left:auto;display:flex;gap:2px;background:#111A24;border:1px solid #1E2A37;border-radius:8px;padding:2px}
.mtlayoutpick button{border:0;background:none;color:#9FADBE;font:inherit;font-size:11.5px;
  padding:4px 10px;border-radius:6px;cursor:pointer}
.mtlayoutpick button.on{background:#1E2A37;color:#E9EEF4}
.mtbanner{padding:6px 16px;background:#2C2312;color:#D9A03F;font-size:12px;text-align:center;flex:none}
/* Full width and impossible to miss: until it is pressed, nobody in the call
   can be heard, and the person it is shown to cannot know that on their own. */
.mtsound{display:flex;align-items:center;justify-content:center;gap:8px;flex:none;width:100%;
  padding:9px 16px;border:0;background:#132738;color:#7DB4E8;font:inherit;font-size:13px;font-weight:600;cursor:pointer;
  position:relative;z-index:2}
.mtsound:hover{background:#17314a}
.mtsound svg{width:16px;height:16px}
.mtbody{flex:1;min-height:0;display:flex}
/* The video area can never cover the controls, whatever is inside it.
   - min-height:0 + overflow:auto keep tiles inside their own box: too many
     people scroll within the stage instead of spilling over the bar.
   - container-type:size lets a tile size itself to the space it actually has
     (cqw/cqh below), which is what "fit the screen" means for a 16:9 video.
   - the head and bar sit above it (z-index) as a last line of defence: a tile
     is position:relative, and a positioned box paints over non-positioned
     siblings, which is exactly how the Leave button disappeared. */
.mtstagearea{flex:1;min-width:0;min-height:0;display:flex;flex-wrap:wrap;gap:8px;padding:12px;
  align-content:center;justify-content:center;overflow:auto;container-type:size}
.mtroomhead,.mtbar,.mtbanner{position:relative;z-index:2;background:#0A0F16}
/* EVERY TILE HAS AN EXPLICIT WIDTH AND HEIGHT, computed from the stage alone,
   and the <video> is absolutely positioned inside it — so nothing about the
   video can change the size of the box.

   The previous rules set a width and relied on aspect-ratio for the height.
   Chrome honoured it; Safari let flex `stretch` win, so a two-person grid was
   672x749 (tall and skinny) and, after a screen share ended, 672x72 (a
   sliver) — both measured in Safari 26, both what people saw.

   Keeping the size independent of the video also means LiveKit's
   adaptiveStream, which picks a resolution from the element's size, can never
   be steered by the resolution it picked. That loop was checked for and NOT
   reproduced (real frames at changing resolutions did not move the old tiles
   either), so this is not the explanation for the reconnect storm — the call
   diary is how that gets answered. */
.mttile{position:relative;background:#111A24;border-radius:10px;overflow:hidden;
  flex:none;align-self:center;min-width:0;width:var(--tw);height:var(--th)}
.mttile video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
.mttile video.mirror{transform:scaleX(-1)}
.mttile.big video{object-fit:contain;background:#05090E}

/* Grid. Fit, not fill: the largest 16:9 box that fits BOTH the width and the
   height available. One person gets one big tile; two sit side by side; three
   or four make a 2x2. Five or more keep two columns and the stage scrolls. */
.mtstagearea.grid{align-items:center;align-content:center}
.mtstagearea.grid .mttile{--tw:calc((100cqw - 8px) / 2);--th:calc((100cqw - 8px) / 2 * 9 / 16)}
.mtstagearea.grid[data-n="1"] .mttile{
  --tw:min(100cqw, calc(100cqh * 16 / 9));--th:min(100cqh, calc(100cqw * 9 / 16))}
.mtstagearea.grid[data-n="2"] .mttile{
  --tw:min(calc((100cqw - 8px) / 2), calc(100cqh * 16 / 9));
  --th:min(calc((100cqw - 8px) / 2 * 9 / 16), 100cqh)}
.mtstagearea.grid[data-n="3"] .mttile,.mtstagearea.grid[data-n="4"] .mttile{
  --tw:min(calc((100cqw - 8px) / 2), calc((100cqh - 8px) / 2 * 16 / 9));
  --th:min(calc((100cqw - 8px) / 2 * 9 / 16), calc((100cqh - 8px) / 2))}

/* A shared screen, or speaker view. The focus takes a full row and everything
   but one strip of faces; the faces are fixed 160x90 thumbnails that wrap
   underneath. A row, not a column: stacked vertically, three faces would take
   300px away from the thing everyone is trying to read. */
.mtstagearea.sharing,.mtstagearea.speaker{flex-direction:row;flex-wrap:wrap;align-items:flex-start;
  align-content:flex-start;justify-content:center}
.mtstagearea.sharing .mttile.big,.mtstagearea.speaker .mttile.big{--tw:100cqw;--th:max(120px, calc(100cqh - 98px))}
.mtstagearea.sharing .mttile:not(.big),.mtstagearea.speaker .mttile:not(.big){--tw:160px;--th:90px}
.mtoff{position:absolute;inset:0;display:grid;place-items:center;background:#111A24}
.mtname{position:absolute;left:8px;bottom:6px;font-size:11px;color:#E9EEF4;
  background:rgba(5,9,14,.62);border-radius:5px;padding:2px 7px;max-width:calc(100% - 40px);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mttile.speaking{box-shadow:inset 0 0 0 2px #3DB88E}
.mtq{position:absolute;right:8px;top:8px;width:7px;height:7px;border-radius:50%;background:transparent}
.mtq.poor{background:#D9A03F}
.mtq.lost{background:#E4736F}

.mtpeople{flex:none;width:274px;border-left:1px solid #16202B;overflow-y:auto;padding:12px}
.mtpeoplehead{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
  color:#72808F;margin-bottom:8px}
.mtpeoplehead span{color:#E9EEF4}
.mtperson{display:flex;align-items:center;gap:9px;padding:6px 0}
.mtpn{flex:1;min-width:0;display:flex;flex-direction:column}
.mtpn b{font-size:12.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mtpn em{font-style:normal;font-size:10px;color:#7DB4E8}
.mtflag{font-size:9.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
  color:#3DB88E;background:#0C2721;border-radius:4px;padding:1px 5px}
.mticon{display:inline-flex;color:#9FADBE}
.mticon.off{color:#E4736F}
.mticon svg{width:14px;height:14px}
.mtpeoplenote{font-size:11px;color:#5A6775;line-height:1.5;margin:12px 0 0}

.mtbar{display:flex;align-items:center;gap:8px;padding:10px 16px;border-top:1px solid #16202B;
  flex:none;flex-wrap:wrap;padding-bottom:calc(10px + env(safe-area-inset-bottom,0px))}

/* ---- phones ------------------------------------------------------------ */
@media(max-width:760px){
  /* One or two tiles, big enough to read a face; the rest scroll. Controls
     lose their words and keep their icons. */
  .mtstagearea{padding:8px;gap:6px}
  /* A phone is portrait: one column. Two to four fit two to the height rather
     than two to the width, or a pair of tiles is a pair of postage stamps. */
  .mtstagearea.grid .mttile{--tw:100cqw;--th:calc(100cqw * 9 / 16)}
  .mtstagearea.grid[data-n="2"] .mttile,.mtstagearea.grid[data-n="3"] .mttile,
  .mtstagearea.grid[data-n="4"] .mttile{
    --tw:min(100cqw, calc((100cqh - 6px) / 2 * 16 / 9));
    --th:min(calc(100cqw * 9 / 16), calc((100cqh - 6px) / 2))}
  .mtbody{flex-direction:column}
  .mtpeople{width:auto;border-left:0;border-top:1px solid #16202B;max-height:44vh}
  .mtbtn span{display:none}
  .mtbtn{padding:0 11px}
  .mtroomhead{padding:8px 12px}
  .mtbar{padding:8px 12px;gap:6px;padding-bottom:calc(8px + env(safe-area-inset-bottom,0px))}
  .mtlobby{padding:16px 14px}
  .mtrow{grid-template-columns:64px minmax(0,1fr) auto}
  .mtrow .mtgo{display:none}
}
