/* Laundry9 platform — shared styles (marketing + portals). Light theme. */
@font-face{font-family:'Bricolage';src:url('/fonts/bricolage.woff2') format('woff2');font-weight:200 800;font-display:swap}
@font-face{font-family:'Manrope';src:url('/fonts/manrope.woff2') format('woff2');font-weight:200 800;font-display:swap}
:root{
  --c1:#0EA5E9;--c2:#0369A1;--ink:#0B1B26;--body:#3B4C59;--mut:#64798a;
  --bg:#F6FAFD;--card:#fff;--line:#E1EBF2;--ok:#0d9d58;--warn:#c77700;--bad:#c93434;
  --grad:linear-gradient(135deg,var(--c1),var(--c2));
  --sh:0 6px 24px rgba(3,105,161,.10);--r:14px;
  --font:'Manrope',system-ui,-apple-system,'Segoe UI',sans-serif;
  --disp:'Bricolage','Manrope',system-ui,sans-serif;
}
*{margin:0;padding:0;box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{font-family:var(--font);color:var(--body);background:var(--bg);line-height:1.6;font-size:16px}
h1,h2,h3,h4{font-family:var(--disp);color:var(--ink);line-height:1.15;font-weight:700}
a{color:var(--c2);text-decoration:none}
img{max-width:100%}
.wrap{max-width:1120px;margin:0 auto;padding:0 22px}

/* buttons */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:0;cursor:pointer;
  font:600 15px var(--font);border-radius:12px;padding:12px 22px;transition:.18s;text-align:center}
.btn-p{background:var(--grad);color:#fff;box-shadow:0 6px 18px rgba(14,165,233,.35)}
.btn-p:hover{transform:translateY(-1px);box-shadow:0 10px 22px rgba(14,165,233,.45)}
.btn-o{background:#fff;color:var(--c2);border:1.5px solid var(--c1)}
.btn-o:hover{background:#f0f9ff}
.btn-g{background:#eef4f8;color:var(--ink)}
.btn-d{background:#fdecec;color:var(--bad)}
.btn-sm{padding:7px 14px;font-size:13px;border-radius:9px}
.btn[disabled]{opacity:.5;cursor:not-allowed;transform:none}
.btn-block{width:100%}
/* The driver shell alone (body.drv, driver.html) gets a 44px touch floor: every
   press there happens one-handed at a doorstep, and the compact .btn-sm above --
   7px pad on 13px type, roughly 30px tall -- also sits in dense staff tables
   where 44px rows would be wrong under a mouse, so the floor cannot be blanket.
   Buttons get the height floor, .btn-sm the width too (the stop card's photo,
   missed-stop and issue controls and the day-nav arrows are glyph-sized), and
   the day-nav date box the same height. */
.drv .btn{min-height:44px}
.drv .btn-sm{min-width:44px}
.drv input[type=date]{min-height:44px}

/* forms */
/* .f is a field caption. On a <label> it carries a `for` and names one control;
   on a <div> it captions a GROUP (a window grid, a set of choice tiles) or a
   region whose control varies, where a <label> would be a label for nothing.
   Both render identically, so the markup can be honest without a visual diff. */
.f{display:block;font:600 13px var(--font);color:var(--ink);margin:14px 0 5px}
label.f{cursor:pointer}
input,select,textarea{width:100%;padding:11px 13px;border:1.5px solid var(--line);border-radius:10px;
  font:500 15px var(--font);color:var(--ink);background:#fff;outline:none;transition:border .15s}
input:focus,select:focus,textarea:focus{border-color:var(--c1)}
/* A box whose value is already committed. The intake station locks its weigh-in
   half the moment that write lands and goes on being about the photo, and a box
   that still looks editable is a promise that typing in it changes something.
   Kept readable rather than greyed out to .5 the way .btn[disabled] is — the
   figures are the thing staff are checking — and still in the tab order, which is
   why it is readonly and not disabled. --body over #f4f6f8 is 8:1. */
input[readonly],textarea[readonly]{background:#f4f6f8;color:var(--body);cursor:default}
input[readonly]:focus,textarea[readonly]:focus{border-color:var(--line)}
/* The one box a refusal named — after :focus on purpose, so the box we put the
   cursor in stays the red one and the ring keeps it findable in a twelve-line
   manifest once the cursor moves on. */
input.bad,select.bad,textarea.bad{border-color:var(--bad);box-shadow:0 0 0 3px rgba(201,52,52,.15)}
textarea{resize:vertical;min-height:76px}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.row3{display:grid;grid-template-columns:2fr 1fr 1fr;gap:12px}
@media(max-width:560px){.row2,.row3{grid-template-columns:1fr}}
.err{background:#fdecec;color:var(--bad);border-radius:10px;padding:10px 14px;font-size:14px;margin:12px 0;display:none}
.err.show{display:block}
.hint{font-size:13px;color:var(--mut)}
/* The per-order tracking link (portal.js orderDetailView). A rule of its own so
   the box keeps its air whichever of its two states is drawn — the empty one is a
   button and a sentence, the live one is a read-only field and three buttons. */
.share-box{margin-top:14px;padding-top:12px;border-top:1px solid var(--line)}

/* cards, pills, tables */
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--sh);padding:22px}
.pill{display:inline-block;padding:3px 11px;border-radius:99px;font:700 12px var(--font);white-space:nowrap}
.pill.blue{background:#e0f2fe;color:#075985}.pill.green{background:#dcf5e7;color:#106b3f}
.pill.amber{background:#fdf1dc;color:#8a5800}.pill.red{background:#fdecec;color:#a32626}
.pill.gray{background:#edf2f6;color:#51636f}.pill.violet{background:#ede9fe;color:#5b21b6}
/* The CRM tag list under a customer's name, on the Customers list and the
   dispatch board (public/admin.js tagChips). A row of .pill.gray with the 🏷 in
   front of it: inline-flex so a customer with six tags wraps under their name
   instead of stretching the column, and the gap is what keeps two adjacent
   one-word tags from reading as one. .tagchip is the same pill with a click on
   it — it filters the list to that tag exactly — so it says so on hover and
   carries a visible focus ring, since it is reached by Tab inside a row that is
   itself clickable. */
.tagrow{display:inline-flex;flex-wrap:wrap;gap:4px;align-items:center;margin-top:3px;font-size:12px}
.tagchip{cursor:pointer}
.tagchip:hover{background:#dbe7f0;color:#22303a}
.tagchip:focus-visible{outline:2px solid var(--c1);outline-offset:2px}
table.tbl{width:100%;border-collapse:collapse;font-size:14px}
.tbl th{font:700 12px var(--font);text-transform:uppercase;letter-spacing:.04em;color:var(--mut);
  text-align:left;padding:10px 12px;border-bottom:2px solid var(--line)}
.tbl td{padding:11px 12px;border-bottom:1px solid var(--line);color:var(--ink);vertical-align:middle}
.tbl tr:hover td{background:#f4f9fc}
.tbl .click{cursor:pointer}
/* A column header that re-orders the list. Inherits the header's own type so it
   reads as the heading it replaced, not as a link dropped into it; the active one
   takes the brand ink because the arrow alone is easy to miss at 12px. */
.sortth{cursor:pointer;display:inline-block;white-space:nowrap;-webkit-user-select:none;user-select:none}
.sortth:hover,.sortth.on{color:var(--c2)}
.sortth.sub{font-size:11px;letter-spacing:0;text-transform:none}
.sortth:focus-visible{outline:2px solid var(--c1);outline-offset:2px;border-radius:4px}
.tblwrap{overflow-x:auto}

/* ============ MARKETING ============ */
.mk-head{position:sticky;top:0;z-index:50;background:rgba(255,255,255,.92);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
.mk-head .wrap{display:flex;align-items:center;justify-content:space-between;height:66px}
.logo{display:flex;align-items:center;gap:10px;font:800 21px var(--disp);color:var(--ink)}
.logo img{width:34px;height:34px;border-radius:9px}
.logo b{background:var(--grad);-webkit-background-clip:text;background-clip:text;color:transparent}
.mk-nav{display:flex;gap:26px;align-items:center}
.mk-nav a{font:600 14.5px var(--font);color:var(--body)}
.mk-nav a:hover{color:var(--c2)}
@media(max-width:820px){.mk-nav .lnk{display:none}}
.hero{position:relative;background:var(--grad);color:#fff;overflow:hidden}
.hero::before{content:"";position:absolute;inset:0;background:
  radial-gradient(700px 340px at 85% -10%,rgba(255,255,255,.22),transparent 60%),
  radial-gradient(500px 300px at -5% 110%,rgba(255,255,255,.14),transparent 55%)}
.hero .wrap{position:relative;display:grid;grid-template-columns:1.15fr .85fr;gap:44px;padding:72px 22px 80px;align-items:center}
@media(max-width:880px){.hero .wrap{grid-template-columns:1fr;padding:52px 22px}}
.hero h1{color:#fff;font-size:clamp(34px,5.2vw,56px);letter-spacing:-.02em}
.hero p.sub{font-size:19px;margin:16px 0 26px;color:#e6f5ff;max-width:34em}
.hero .btn-w{background:#fff;color:var(--c2)}
.badges{display:flex;gap:10px;flex-wrap:wrap;margin-top:26px}
.badge{background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.3);border-radius:99px;
  padding:6px 14px;font:600 13px var(--font)}
/* THE COVERAGE CHECK IN THE HERO. On the gradient, so the field is a translucent
   white the way .badge is rather than the page's default input, and the answer band
   below it borrows the same treatment so a "yes", a "not yet" and a typo all read as
   one component instead of three. .zipanswer is empty until the script writes into
   it and collapses to nothing when it is — no reserved gap under the form. */
.zipcheck{margin-top:26px;max-width:30em}
.zipcheck .f{display:block;font:700 13.5px var(--font);color:#e6f5ff;margin-bottom:8px}
.zipcheck-row{display:flex;gap:10px;flex-wrap:wrap}
.zipcheck-row input{flex:1 1 11em;min-width:9em;background:rgba(255,255,255,.16);color:#fff;
  border:1px solid rgba(255,255,255,.4);border-radius:11px;padding:12px 14px;font:600 15px var(--font)}
.zipcheck-row input::placeholder{color:rgba(255,255,255,.72)}
.zipcheck-row input:focus{outline:2px solid #fff;outline-offset:1px}
.zipanswer:empty{display:none}
.zipanswer{margin-top:14px;max-width:34em;background:rgba(255,255,255,.16);
  border:1px solid rgba(255,255,255,.3);border-radius:14px;padding:14px 16px;font:500 14.5px var(--font);color:#fff}
.zipanswer b{color:#fff}
.zipanswer a{color:#fff;text-decoration:underline}
.zipanswer .zipform{display:flex;gap:10px;flex-wrap:wrap;margin-top:10px}
.zipanswer .zipform input{flex:1 1 14em;min-width:11em;background:rgba(255,255,255,.16);color:#fff;
  border:1px solid rgba(255,255,255,.4);border-radius:11px;padding:11px 13px;font:500 14.5px var(--font)}
.zipanswer .zipform input::placeholder{color:rgba(255,255,255,.72)}
.hero-card{background:#fff;border-radius:18px;box-shadow:0 24px 60px rgba(2,44,66,.35);padding:26px;color:var(--body)}
.hero-card h3{font-size:20px;margin-bottom:4px}
.steps{counter-reset:s;display:grid;gap:0}
.step{position:relative;padding:14px 0 14px 52px;border-bottom:1px dashed var(--line)}
.step:last-child{border-bottom:0}
.step::before{counter-increment:s;content:counter(s);position:absolute;left:0;top:14px;width:36px;height:36px;
  border-radius:11px;background:var(--grad);color:#fff;font:800 16px var(--disp);display:grid;place-items:center}
.step b{display:block;color:var(--ink);font-size:15px}
.step span{font-size:13.5px;color:var(--mut)}
section.sec{padding:72px 0}
section.sec.alt{background:#fff;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.k{font:800 12px var(--font);letter-spacing:.14em;text-transform:uppercase;color:var(--c1)}
.sec h2{font-size:clamp(26px,3.6vw,38px);margin:8px 0 12px}
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:30px}
@media(max-width:880px){.grid3{grid-template-columns:1fr 1fr}}
@media(max-width:580px){.grid3{grid-template-columns:1fr}}
.svc{background:#fff;border:1px solid var(--line);border-radius:var(--r);padding:22px;box-shadow:var(--sh)}
.svc .ic{width:44px;height:44px;border-radius:12px;background:linear-gradient(135deg,#e0f2fe,#bae6fd);
  display:grid;place-items:center;font-size:22px;margin-bottom:12px}
.svc h3{font-size:17px;margin-bottom:5px}
.svc p{font-size:14.5px;color:var(--mut)}
.price-tag{font:800 26px var(--disp);color:var(--c2)}
.price-tag small{font:600 13px var(--font);color:var(--mut)}
.faq details{background:#fff;border:1px solid var(--line);border-radius:12px;padding:16px 20px;margin-bottom:10px}
.faq summary{font:700 15.5px var(--font);color:var(--ink);cursor:pointer}
.faq p{margin-top:8px;font-size:14.5px}
.cta-band{background:var(--grad);color:#fff;text-align:center;padding:64px 22px}
.cta-band h2{color:#fff;font-size:clamp(26px,3.6vw,38px)}
.cta-band p{color:#dff2ff;margin:10px 0 24px}
footer.mk{background:#082433;color:#9fc2d4;padding:44px 0;font-size:14px}
footer.mk a{color:#cde8f5}
footer.mk .wrap{display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap}

/* ============ PORTAL SHELL ============ */
.shell{display:grid;grid-template-columns:230px 1fr;min-height:100vh}
@media(max-width:840px){.shell{grid-template-columns:1fr}}
.side{background:#07222f;color:#bcd9e8;padding:20px 14px;display:flex;flex-direction:column;gap:4px}
@media(max-width:840px){.side{flex-direction:row;align-items:center;overflow-x:auto;padding:10px 12px;position:sticky;top:0;z-index:40}}
.side .logo{color:#fff;font-size:18px;margin:4px 10px 18px}
@media(max-width:840px){.side .logo{margin:0 12px 0 0}.side .logo span{display:none}}
.side a.nv{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:10px;
  font:600 14px var(--font);color:#bcd9e8;white-space:nowrap}
.side a.nv:hover{background:rgba(255,255,255,.08);color:#fff}
.side a.nv.on{background:var(--grad);color:#fff}
.side .sp{flex:1}
@media(max-width:840px){.side .sp{display:none}}
.main{padding:26px;max-width:1180px;width:100%}
@media(max-width:560px){.main{padding:16px}}
.topbar{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:22px;flex-wrap:wrap}
.topbar h1{font-size:24px}
.stats{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:14px;margin-bottom:22px}
/* A STRIP MAY ONLY TAKE A COLUMN COUNT THAT DIVIDES IT, which is why the two
   admin pages below state their own rungs instead of riding the shared rule.
   The shared auto-fill ladder above hands out floor((W+14)/174) columns over a
   content box of min(1180, viewport - 230) - 52 — .main's max-width and padding,
   beside the 230px .side — so it reaches 6, 5, 4, 3, 2 and 1 and nothing else. A
   strip whose count does not divide the rung it lands on strands tiles on a
   short row, and no single minmax() can fix that for a given count: reaching 6
   columns at 1128px of content needs a minimum of 176px or less, and skipping 5
   at 998px needs one above 188px, and those contradict.
   THERE WAS A .stats-12 HERE, and it is gone rather than lost. The Analytics
   strip was one grid of twelve, and this is where it was kept off the 5 columns
   the shared ladder reaches from 1138px to 1311px (which wrapped it 5/5/2). That
   strip is now TWO labelled strips of six — see public/admin.js vReports — and
   six is a size the rule below already serves, so the twelve-tile rungs became
   dead weight the day the split shipped. A future strip of twelve wants them
   back, derived again from the constants above, not remembered from here.
   AND THE DASHBOARD'S TWENTY-FOUR, WHICH ARE FOUR LABELLED STRIPS.
   WHY 24 AND NOT 21, first, because it is still the reason this page has rules of
   its own. The Dashboard carried 21 tiles, and 21 is 3 x 7: the shared ladder
   reaches 6, 5, 4, 3, 2 and 1, so 21 was ragged at every rung except 3 — 6/6/6/3
   on every 1440 laptop, 5/5/5/5/1 on a 1280 one, 4x5+1 below that. A page-scoped
   ladder could not rescue it the way it rescued twelve, because 21's own divisors
   are the only counts it may take: 7 columns needs 7x160 + 6x14 = 1204px and the
   content box tops out at 1128px, and 3 columns is a 367px .stat, more than twice
   the 160px floor for a tile that holds one figure over one label. So the COUNT
   moved, and three tiles were added to get there, each closing a hole the strip
   genuinely had (today's bookings, unassigned DELIVERIES, and the finished work
   waiting to go out); see public/admin.js vDash and docs/backlog.md.
   SQUARING THE GRID DID NOT MAKE IT READABLE. 24 tiles under no heading at all is
   one undifferentiated wall, and a manager opening this page every morning has to
   re-find the four figures they came for. So the strip is now four strips — 8, 4,
   6 and 6, in the order the tiles already stood in — each under an <h3> naming
   what it is: today's board and the shop floor, what needs somebody, money to
   chase, and the business.
   THE SIZES ARE NOT FREE. A strip of G tiles is ragged at a rung of c columns
   unless c divides G, so the sub-strips could not be chosen semantically and then
   made to fit; 8 + 4 + 6 + 6 is the partition that is both honest about meaning
   and clean at every width. Each states the rungs its own count divides — and the
   shared ladder is NOT monotonic in the viewport, because the 230px .side leaves
   at 840px: 734-840px gets 4 columns and 841-963px only 3, which is why the pair
   below steps down and back up again rather than reading as one staircase.
     · 8 and 4 share a rule: below 5 columns their divisor sets agree, and neither
       may ever take 3, 5 or 6 (a stranded 2, 3 and 2 respectively). They take 4
       wherever four tracks fit and 2 below that.
     · 6 takes the 6 the shared rule already reaches at 1312px, and 3 below it —
       never 4 or 5, which strand 2 and 1. FOUR STRIPS WEAR THIS RULE, not two:
       the Analytics page's own split is 6 + 6, so a change here moves both pages
       and both suites will say so.
   No rung puts a tile under the 160px floor, and the widest is a 334px .stat in
   the 841-963px band, a width this page already hands out on a tablet.
   The number in each class name is a CLAIM about the strip wearing it, pinned by
   scripts/tests/dashboard-strip-columns.test.js and its Analytics sibling against
   a real render: they count the tiles inside every strip, re-derive these
   breakpoints from the four lengths the model stands on, and walk 320-2560px one
   pixel at a time. Source order is load-bearing here — equal specificity, so the
   LAST matching @media wins, and these must stay in ascending min-width order per
   class. */
@media(min-width:540px){.stats.stats-4,.stats.stats-8{grid-template-columns:repeat(2,1fr)}}
@media(min-width:734px){.stats.stats-4,.stats.stats-8{grid-template-columns:repeat(4,1fr)}}
@media(min-width:841px){.stats.stats-4,.stats.stats-8{grid-template-columns:repeat(2,1fr)}}
@media(min-width:964px){.stats.stats-4,.stats.stats-8{grid-template-columns:repeat(4,1fr)}}
@media(min-width:540px){.stats.stats-6{grid-template-columns:repeat(3,1fr)}}
@media(min-width:1312px){.stats.stats-6{grid-template-columns:repeat(6,1fr)}}
/* The label over a sub-strip. h1-h4 above already set the display family, the ink
   and the weight, so this is only a size and a spacing — deliberately the same
   15px the dashboard's own cards give their <h3>, so the strip headings and the
   card headings under them read as one type scale rather than two. Bound tighter
   to the strip below it (10px) than to the strip above it (.stats' own 22px), or
   a heading floats between two walls instead of naming one. */
.strip-h{font-size:15px;margin-bottom:10px}
.stat{background:#fff;border:1px solid var(--line);border-radius:var(--r);padding:16px 18px;box-shadow:var(--sh)}
.stat .v{font:800 26px var(--disp);color:var(--ink)}
.stat .l{font:600 12.5px var(--font);color:var(--mut)}
.stat.hot .v{color:var(--c2)}
/* The "of which" line: a footnote to the tile's own number (the share of Revenue
   that is cancellation fees), so it is quieter than the label above it and must
   never compete with the figure. Only tiles that pass `sub` grow one. */
.stat .s{font:500 11.5px var(--font);color:var(--mut);opacity:.8;margin-top:4px}
/* A tile holding money OUT reads as money out. Renderers set this from
   isMoneyOut() (common.js), never from the formatted string; last of the .v
   rules so it wins the specificity tie with .stat.hot .v. */
.stat .v.bad{color:var(--bad)}
/* A tile whose number has somewhere to be acted on renders as a real anchor, so
   keyboard, middle-click and the status bar all come free. It lifts on hover and
   carries an arrow at rest, so a clickable tile is never mistaken for a static
   one — tiles with no destination (revenue, in-facility counts) stay divs. */
a.stat{display:block;color:inherit;transition:transform .12s,box-shadow .12s,border-color .12s}
a.stat:hover{transform:translateY(-1px);border-color:var(--c1);box-shadow:0 8px 18px rgba(14,165,233,.18)}
a.stat:focus-visible{outline:2px solid var(--c1);outline-offset:2px}
a.stat .l::after{content:" →";color:var(--c1)}
.tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}
.tab{padding:8px 15px;border-radius:99px;font:700 13px var(--font);cursor:pointer;background:#fff;
  border:1.5px solid var(--line);color:var(--body)}
.tab.on{background:var(--grad);border-color:transparent;color:#fff}
.empty{text-align:center;color:var(--mut);padding:44px 16px;font-size:15px}
.empty .big{font-size:38px;display:block;margin-bottom:8px}
/* THE SAME SLAB, WAITING. `.empty` is what a screen with nothing on it says and
   what common.js drawRefusal() draws when a read is refused; this is the third
   member of that set — the read is still out. Same box, same muted colour, so a
   page that goes loading -> loaded (or loading -> refused) does not jump; the
   glyph borrows the `.pulse` animation below, which is the only motion this
   stylesheet has, rather than inventing a spinner. common.js loadingSlab(). */
.empty.load .big{animation:pl 1.4s infinite}

/* timeline */
.tline{list-style:none;position:relative;padding-left:26px}
.tline::before{content:"";position:absolute;left:8px;top:6px;bottom:6px;width:2px;background:var(--line)}
.tline li{position:relative;padding:0 0 16px}
.tline li::before{content:"";position:absolute;left:-24px;top:5px;width:12px;height:12px;border-radius:50%;
  background:#fff;border:3px solid var(--c1)}
.tline li.done::before{background:var(--c1)}
/* A trip that ended at a closed door (admin order modal, "Came back undelivered").
   Same timeline furniture, a filled RED bead: these rows are read straight after
   the status timeline above them and a blue dot would say a step completed. */
.tline li.miss::before{background:var(--bad);border-color:var(--bad)}
.tline b{display:block;color:var(--ink);font-size:14px}
.tline span{font-size:12.5px;color:var(--mut)}

/* progress tracker (client order view) */
.trk{display:flex;gap:4px;margin:16px 0}
.trk i{flex:1;height:7px;border-radius:5px;background:var(--line)}
.trk i.on{background:var(--grad)}

/* modal */
.mback{position:fixed;inset:0;background:rgba(5,25,38,.5);display:none;z-index:100;overflow-y:auto;padding:30px 14px}
.mback.show{display:block}
.modal{background:#fff;border-radius:16px;max-width:680px;margin:0 auto;padding:26px;position:relative;box-shadow:0 30px 80px rgba(0,0,0,.3)}
.modal.wide{max-width:860px}
.mx{position:absolute;top:14px;right:16px;background:#eef4f8;border:0;width:34px;height:34px;border-radius:9px;
  font-size:16px;cursor:pointer;color:var(--ink)}
.modal h2{font-size:20px;margin-bottom:14px;padding-right:40px}

/* order stepper */
.osteps{display:flex;gap:6px;margin-bottom:22px}
.osteps span{flex:1;text-align:center;font:700 12px var(--font);color:var(--mut);padding-top:10px;position:relative}
.osteps span::before{content:"";display:block;height:6px;border-radius:4px;background:var(--line);margin-bottom:7px}
.osteps span.on{color:var(--c2)}
.osteps span.on::before{background:var(--grad)}
.chooser{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:8px 0}
@media(max-width:560px){.chooser{grid-template-columns:1fr}}
.choice{border:2px solid var(--line);border-radius:13px;padding:15px;cursor:pointer;background:#fff;transition:.15s}
.choice:hover{border-color:#a8d8f0}
.choice.on{border-color:var(--c1);background:#f0f9ff}
.choice.off{opacity:.45;cursor:not-allowed;background:var(--bg)}
.choice.off:hover{border-color:var(--line)}
.choice b{display:block;color:var(--ink);font-size:15px}
.choice small{color:var(--mut);font-size:13px}
/* These tiles are real controls — radio/checkbox roles and a tabindex, wired by
   portal.js radioTiles/toggleTiles — so the keyboard has to be able to see where
   it is. Nothing here drew a focus ring before, which cost a mouse user the
   feedback too once a click moved focus onto the tile. */
.choice:focus-visible,.win:focus-visible,.addr-card:focus-visible{outline:2px solid var(--c1);outline-offset:2px}
.qty{display:flex;align-items:center;gap:10px}
.qty button{width:32px;height:32px;border-radius:9px;border:1.5px solid var(--line);background:#fff;
  font:700 16px var(--font);cursor:pointer;color:var(--c2)}
.qty b{min-width:22px;text-align:center;font-size:16px;color:var(--ink)}
.itemrow{display:flex;justify-content:space-between;align-items:center;padding:11px 0;border-bottom:1px dashed var(--line)}
.itemrow:last-child{border-bottom:0}
.itemrow .nm{font:600 14.5px var(--font);color:var(--ink)}
.itemrow .pr{font-size:13px;color:var(--mut)}
.wingrid{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:8px}
@media(max-width:480px){.wingrid{grid-template-columns:1fr}}
.win{border:2px solid var(--line);border-radius:11px;padding:11px;cursor:pointer;text-align:center;
  font:600 13.5px var(--font);color:var(--ink);background:#fff}
.win.on{border-color:var(--c1);background:#f0f9ff;color:var(--c2)}
.win.full{opacity:.42;cursor:not-allowed;text-decoration:line-through}
.win small{display:block;font-size:11px;color:var(--mut)}
/* THE SHOP IS TAKING NO BOOKINGS — portal.js windowGrid() puts the existing amber
   .hold-band inside the window grid at a window capacity of 0. No new component
   and no new colour: it is the same "something is stopping this" band the Account
   page and the statement already use. Only the geometry changes, because a grid
   child would otherwise sit in one 2-column cell beside a window tile. */
.wingrid .hold-band{grid-column:1/-1;margin:0}
/* AND THE GRID'S OTHER TWO EMPTY MOUTHS, same geometry for the same reason: the
   red .fail-band is what windowGrid() draws when the availability read itself
   fails (with a Try again button inside it), and .grid-note is the one-line
   loading state that replaces the previous day's tiles while the next day's are
   being fetched — both span the grid or they'd sit in one cell beside nothing. */
.wingrid .fail-band{grid-column:1/-1;margin:0}
.wingrid .grid-note{grid-column:1/-1;margin:2px 0;color:var(--mut);font-size:14px}
/* WHERE ELSE THERE IS ROOM — portal.js dayStrip(), one chip per day under the
   window grid. It scrolls sideways rather than wrapping: seven chips wrapping to
   two ragged rows on a phone stop reading as a run of days, which is the only
   thing this control is. The states are the grid's own — .full is the same
   opacity + line-through .win.full wears, so "you cannot have this" looks the
   same either side of the picker. */
.daystrip{display:flex;gap:7px;overflow-x:auto;padding-bottom:3px;margin-top:6px;-webkit-overflow-scrolling:touch}
.pday{flex:0 0 auto;min-width:74px;background:#fff;border:2px solid var(--line);border-radius:11px;
  padding:7px 8px;cursor:pointer;text-align:center;color:var(--ink);font:600 13px var(--font)}
.pday:hover{border-color:var(--c1)}
.pday .wd{display:block;font:800 11px var(--font);letter-spacing:.04em;text-transform:uppercase}
.pday .dm{display:block;font:600 10.5px var(--font);margin-top:1px;color:var(--mut)}
.pday .s{display:block;font-size:10.5px;margin-top:4px;color:var(--mut)}
.pday.on,.pday.on:hover{border-color:var(--c1);background:#f0f9ff;color:var(--c2)}
.pday.on .dm,.pday.on .s{color:var(--c2)}
.pday.full{opacity:.42;cursor:not-allowed;text-decoration:line-through}
.pday.full:hover{border-color:var(--line)}
.pday:focus-visible{outline:2px solid var(--c1);outline-offset:2px}
.sumline{display:flex;justify-content:space-between;padding:7px 0;font-size:14.5px}
.sumline.tot{border-top:2px solid var(--line);margin-top:8px;padding-top:12px;font:800 18px var(--disp);color:var(--ink)}
/* The next-pickup estimate on a recurring-service card: a one-line figure that
   opens into the same .sumline breakdown the Review step shows. */
.nextq{margin-top:9px;max-width:420px}
.nextq>summary{font-size:14px;color:var(--ink);cursor:pointer;list-style:none}
.nextq>summary::-webkit-details-marker{display:none}
.nextq>summary::before{content:"▸ ";color:var(--mut)}
.nextq[open]>summary::before{content:"▾ "}
.nextq[open]{background:#f8fafc;border:1px solid var(--line);border-radius:12px;padding:10px 14px}
/* A <button> since 2026-08-29, so it needs the two things a button does not
   inherit — the page's font, and a focus ring. The ring is the same one the
   booking tiles carry (.choice/.win/.addr-card above): the bell is now a tab stop
   in the topbar, and a tab stop nothing outlines is a keyboard user pressing Enter
   on a control they cannot see they are on. */
.bell{position:relative;cursor:pointer;background:#fff;border:1.5px solid var(--line);border-radius:10px;
  width:40px;height:40px;display:grid;place-items:center;font-size:17px;font-family:var(--font);color:var(--ink)}
.bell:focus-visible{outline:2px solid var(--c1);outline-offset:2px}
.bell .dot{position:absolute;top:-4px;right:-4px;background:var(--bad);color:#fff;font:700 10.5px var(--font);
  min-width:17px;height:17px;border-radius:99px;display:grid;place-items:center;padding:0 4px}
.notiflist{position:absolute;right:0;top:48px;width:330px;max-height:420px;overflow-y:auto;background:#fff;
  border:1px solid var(--line);border-radius:14px;box-shadow:0 20px 50px rgba(0,0,0,.18);z-index:60;display:none}
.notiflist.show{display:block}
.notif{padding:12px 16px;border-bottom:1px solid var(--line);font-size:13.5px}
.notif b{display:block;color:var(--ink)}
.notif.unread{background:#f0f9ff}
.rel{position:relative}

/* auth screens */
.auth{min-height:100vh;display:grid;place-items:center;padding:24px;background:
  radial-gradient(800px 400px at 80% -10%,#dbeffc,transparent 60%),var(--bg)}
.auth .card{width:100%;max-width:420px;padding:30px}
.auth .logo{justify-content:center;margin-bottom:18px}
/* WHAT IS ON SCREEN BEFORE ANY SCRIPT HAS PARSED — public/portal.html,
   admin.html and driver.html each ship one of these inside their #app, and it is
   the whole reason those rules are HERE and not in a portal script: a portal used
   to be a white page until common.js, its own script, the auth probe and the first
   read had all landed. It rides .auth for the centring (same gradient, same
   place-items) because that IS the next screen for a visitor who is signed out,
   and a card would have promised more than a wait. Replaced by whatever comes
   first: the auth screen, the shell, or drawRefusal's slab. */
.boot{text-align:center;max-width:420px}
.boot .ld{color:var(--mut);font-size:15px;animation:pl 1.4s infinite}
.boot .ns{color:var(--body);font-size:14.5px;background:var(--card);border:1px solid var(--line);
  border-radius:var(--r);padding:16px 18px;text-align:left}
/* A SHEET BEING REPLACED BY A READ SOMEBODY ASKED FOR — the driver's day pager
   (driver.js pageTo). Not a placeholder: the stops still on screen are the previous
   day's and stay readable, which is why they are dimmed rather than erased. The
   class is set on the <main> the next write of #app discards, so it clears itself. */
main.reading{opacity:.45;transition:opacity .12s}
/* THE SAME .reading, SCOPED TO A REGION instead of the whole sheet — the driver's
   earnings window chips (driver.js earnTo). Tapping "This week" replaces the money
   strip and nothing else: the stops under it are the same one day either way, so
   dimming them with main.reading above would say the route is being replaced when it
   is not. data-reread is which nodes belong to the strip's answer — everything
   earningsStrip() draws EXCEPT the two chips, because opacity on a parent cannot be
   undone by a child, so the chip that has to stay lit has to be left outside the
   dimmed group rather than turned back up inside it. Membership is in the markup and
   the dim is the class, so a strip drawn while nothing is pending is never dim.
   Self-clearing on the same rule as above: every tagged node is inside the #app that
   the next write — draw()'s or drawRefusal's — throws away. */
[data-reread].reading{opacity:.45;transition:opacity .12s}
/* Motion is decoration in all three of these — the words say the same thing
   standing still — so it comes off for anyone who asked for less of it. */
@media(prefers-reduced-motion:reduce){.boot .ld,.empty.load .big{animation:none}}
.switch{text-align:center;margin-top:16px;font-size:14px}

/* misc */
.flex{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.right{margin-left:auto}
.mt{margin-top:16px}.mb{margin-bottom:16px}
.muted{color:var(--mut);font-size:13.5px}
.addr-card{border:2px solid var(--line);border-radius:13px;padding:14px;cursor:pointer;background:#fff}
.addr-card.on{border-color:var(--c1);background:#f0f9ff}
.addr-card b{color:var(--ink)}
.bar{height:8px;border-radius:5px;background:var(--grad)}
.barrow{display:grid;grid-template-columns:60px 1fr 100px;gap:10px;align-items:center;font-size:13px;padding:5px 0}
.barrow .num{display:flex;flex-direction:column;align-items:flex-end;line-height:1.35;text-align:right}
.barrow .num .muted{font-size:12px}
/* width:max-content is load-bearing, not decoration. This box is position:fixed
   with left:50% and right:auto, so its containing block is the viewport and the
   shrink-to-fit width available to it is 100vw - 50vw = HALF THE SCREEN — a
   max-width alone cannot raise that ceiling. max-content sizes it to the text
   instead, and the max-width then clamps it: a two-word confirmation stays a
   small centred pill, a 300-character refusal wraps inside 420px rather than
   climbing the phone as a one-word-per-line ribbon. The 1.45 line-height is
   what makes those wrapped lines readable (the font shorthand would otherwise
   reset it to normal). See toast() in common.js for the matching dwell time. */
.toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);width:max-content;
  max-width:min(420px,calc(100vw - 32px));background:var(--ink);color:#fff;
  padding:12px 22px;border-radius:12px;font:600 14px/1.45 var(--font);z-index:200;
  box-shadow:0 10px 30px rgba(0,0,0,.3);opacity:0;transition:.25s;pointer-events:none}
.toast.show{opacity:1}

/* ============ V2 ============ */
.map{height:340px;border-radius:14px;border:1px solid var(--line);z-index:1}
.map-sm{height:240px}
.stars{display:inline-flex;gap:4px;font-size:26px;cursor:pointer;user-select:none}
.stars span{color:#d5e2ec;transition:.1s}
.stars span.on{color:#f5a623}
.stars.ro{cursor:default;font-size:18px}
.stars span:focus-visible{outline:2px solid var(--c1);outline-offset:2px;border-radius:3px}
.eta-chip{display:inline-flex;align-items:center;gap:8px;background:var(--grad);color:#fff;
  border-radius:99px;padding:8px 18px;font:700 14px var(--font);box-shadow:0 6px 16px rgba(14,165,233,.35)}
.pulse{width:9px;height:9px;border-radius:50%;background:#fff;animation:pl 1.4s infinite}
@keyframes pl{0%,100%{opacity:1}50%{opacity:.35}}
.feed{max-height:380px;overflow-y:auto}
.feed .fi{padding:9px 0;border-bottom:1px dashed var(--line);font-size:13.5px;color:var(--ink)}
.feed .fi .t{color:var(--mut);font-size:12px}
.feed .fi.click{cursor:pointer;padding-left:6px;margin-left:-6px;border-radius:8px}
.feed .fi.click:hover{background:var(--bg)}
.feed .fi.click:focus-visible{outline:2px solid var(--c1);outline-offset:1px}
/* WINDOW LOAD — the strip above the dispatch board (admin.js capacityStrip):
   one cell per configured window, bookings against window_capacity. The bar is
   MAGNITUDE, so it is one hue at one weight; the only colour that carries meaning
   is amber for a window at its cap and red for one past it, the two states a
   manager acts on. Every cell prints its own numbers and names its state in
   words, so none of this is readable by colour alone. Red repeats the .dcol.late
   treatment exactly — same hue, same 3px inset edge — because "look here first"
   should look the same twice on one screen. */
.caprow{display:grid;grid-template-columns:repeat(auto-fit,minmax(136px,1fr));gap:8px}
.capcell{border:1.5px solid var(--line);border-radius:11px;padding:8px 10px;background:#fff;font-size:12px}
.capcell .w{display:block;font:700 11.5px var(--font);color:var(--ink);line-height:1.35}
.capcell .n{display:block;margin:4px 0 5px;color:var(--body)}
.capcell .n b{font:800 17px var(--disp);color:var(--ink)}
.capbar{display:block;height:8px;border-radius:5px;background:var(--bg);overflow:hidden}
.capbar span{display:block;height:100%;border-radius:5px;background:var(--c2)}
.capcell .s{display:block;margin-top:5px;color:var(--mut)}
.capcell.full{border-color:var(--warn)}
.capcell.full .capbar span{background:var(--warn)}
.capcell.full .s{color:var(--warn);font-weight:700}
.capcell.over{border-color:var(--bad);box-shadow:inset 3px 0 0 var(--bad)}
.capcell.over .capbar span{background:var(--bad)}
.capcell.over .s{color:var(--bad);font-weight:700}
/* A shop taking no bookings: dashed, so "there is no capacity here" is a
   different shape and not just a paler colour. */
.capcell.shut{border-style:dashed}
.capcell.shut .capbar span{background:var(--mut)}
.capcell.past{opacity:.58}
/* "THIS IS THE ONE YOU WERE SENT TO" — the cell the dashboard's overbooked band
   named, marked on ARRIVAL by vDispatch and gone on the next redraw. An OUTLINE
   and never a border or a colour, deliberately: the border and the inset bar are
   already spoken for by full/over/shut above, and a cell that changed state to
   say "you are here" would be lying about its load. Outside the box, so it also
   survives on a cell that is dimmed for being past. */
.capcell.jump{outline:2.5px solid var(--c1);outline-offset:3px}
/* THE SAME "YOU WERE SENT HERE" MARK ON A TABLE ROW — the complaint the Analytics
   card's worst answer time belongs to (admin.js vIssues, arriving on `ijump`). An
   outline for the reason above: the row already says what it is with pills, and a
   background change would read as a state this complaint is in. Inset, because a
   row spans the table and an outset outline would sit under the row beside it. */
.tbl tr.jump{outline:2.5px solid var(--c1);outline-offset:-2px}
/* Focused by the same arrival so a screen reader lands with the eye. The row is a
   destination and not a control, so it gets no second ring of its own. */
.tbl tr[tabindex]:focus{outline:none}
.tbl tr[tabindex]:focus-visible{outline:2.5px solid var(--c1);outline-offset:-2px}
/* PEOPLE THE SHOP TURNED AWAY FROM THIS WINDOW (admin.js waitMark). Deliberately
   NOT one of the state colours above: full and over are the load, and this is
   demand standing outside it — a window can be full with nobody waiting, and one
   waiting person is worth reading on a window that is merely busy. So it is its
   own accent chip under the state line, and it only ever exists on a cell that
   has somebody waiting on it. */
.capwait{display:block;margin-top:5px;font:800 10.5px var(--font);color:var(--c2);
  background:var(--bg);border-radius:6px;padding:2px 5px;line-height:1.4}
.capweek .capwait{margin-top:3px;font-size:10px;padding:1px 3px}
/* THE WEEK AHEAD — the same cells, seven days wide (admin.js weekGrid). Only the
   METRICS are set here: the four state rules above are reused exactly as the day
   strip draws them, because a second opinion about what "full" looks like is the
   thing a second view of one number must not have. A real <table> with <th scope>
   on both edges, so a cell announces "8:00 AM – 10:00 AM, Thu Aug 14, 3/8" rather
   than a bare number seventy times over; .tbl's styling is scoped to that class,
   so this table restyles nothing else. It scrolls sideways rather than crushing
   seven columns onto a phone. */
.capweekwrap{overflow-x:auto}
.capweek{width:100%;min-width:620px;border-collapse:separate;border-spacing:5px;table-layout:fixed}
.capweek caption{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.capweek th{padding:0;vertical-align:middle}
.capweek th[scope=row]{width:104px;text-align:left;font:700 10.5px var(--font);color:var(--ink);line-height:1.3;padding-right:4px}
.capweek td{padding:0}
.capweek .capcell{padding:5px 6px;border-radius:8px;font-size:11px;text-align:center}
.capweek .capcell .n{margin:0}
.capweek .capcell .n b{font-size:14px}
.capweek .capcell .s{margin-top:4px;font-size:10px;line-height:1.25}
/* A day that has no such window at all — not a window with nothing in it, which
   is a 0/8 cell. Dashed and empty, so the difference is a shape. */
.capweek .capgap{border:1.5px dashed var(--line);border-radius:8px;height:100%}
/* PER-DAY CAPACITY, THE EDITOR (admin.js vSettings → Scheduling). The same shape
   as .capweek above and deliberately so — rows are windows, columns are weekdays
   — because the owner reads the load on that grid and changes the number on this
   one, and two different geometries for one pair of coordinates is how a Thursday
   gets edited into a Wednesday. A real <table> with <th scope> on both edges; the
   caption is the visually-hidden sentence a screen reader gets before the boxes,
   the same trick .capweek's caption plays. It scrolls sideways rather than
   crushing eight columns onto a phone. */
.capsetwrap{overflow-x:auto;margin:2px 0 6px}
.capset{width:100%;min-width:460px;border-collapse:collapse;table-layout:fixed}
.capset caption{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.capset th{font:700 10.5px var(--font);color:var(--mut);text-transform:uppercase;letter-spacing:.04em;
  padding:3px 2px;text-align:center}
.capset th[scope=row]{width:118px;text-align:left;text-transform:none;letter-spacing:0;
  font:700 11px var(--font);color:var(--ink);line-height:1.3;padding-right:6px}
.capset th[scope=row] .muted{font-size:10px;line-height:1.3}
.capset td{padding:2px}
/* Small, centred, and the placeholder reads as a suggestion rather than a value —
   a blank cell means "the usual capacity", and admin.js capSync() keeps that
   figure honest as the owner types. If it looked like ink the grid would appear
   to hold forty-nine overrides nobody wrote. */
.capset input{padding:7px 2px;text-align:center;font:600 13px var(--font);border-radius:8px}
.capset input::placeholder{color:var(--mut);opacity:.5;font-weight:500}
.capday{display:block;width:100%;background:none;border:0;border-radius:8px;padding:5px 2px;
  cursor:pointer;font-family:var(--font);color:var(--mut);text-align:center}
.capday:hover{background:var(--bg);color:var(--ink)}
.capday .wd{display:block;font:800 11px var(--font);letter-spacing:.04em;text-transform:uppercase}
.capday .dm{display:block;font:600 10.5px var(--font);margin-top:1px}
.capday.on,.capday.on:hover{background:var(--c2);color:#fff}
.capday:focus-visible{outline:2px solid var(--c2);outline-offset:1px}
/* WHETHER ANYBODY IS DRIVING THAT DAY (admin.js coverMark) — the crew, not the
   load, and deliberately NOT one of the four cell-state colours above: those are
   room, and a day can be wide open and uncovered, which is the exact case this
   grid could not draw. It rides on the column HEAD because it is a fact about the
   day; repeating it down seven window cells would read as a claim about the 8–10
   AM run. Amber while somebody is still on (a rota question) and the shop's red
   when nobody is (a phone call) — the same two severities the cells use for full
   and over, so one scale runs down the column. The underline is on the head
   rather than a fill because .capday.on already owns the background. */
.capday .capoff{display:block;margin-top:3px;font:800 9.5px var(--font);color:var(--warn);line-height:1.35}
.capday .capoff.none{color:#fff;background:var(--bad);border-radius:5px;padding:1px 3px}
.capday.nocover{box-shadow:inset 0 -2.5px 0 var(--bad)}
.capday.someoff{box-shadow:inset 0 -2.5px 0 var(--warn)}
.capday.on .capoff{color:#fff}
/* AND WHAT AN UNCOVERED DAY IS CARRYING (admin.js coverMark) — the load, under the
   mark that says nobody is driving it. It is the figure a manager compares columns
   on: the dashboard band names the heaviest uncovered day and links to the SOONEST,
   so this is where the two are told apart. Ink rather than a second badge, because
   the red pill above it is already the alarm and two filled chips on a 90px head
   read as two problems. A bare day with nothing on it says so in the muted weight —
   it is a rota to fix, not a morning to rescue. */
.capday .capload{display:block;margin-top:2px;font:800 9.5px var(--font);color:var(--bad);line-height:1.3}
.capday .capload.quiet{color:var(--mut);font-weight:600}
.capday.on .capload,.capday.on .capload.quiet{color:#fff;opacity:.9}
.dispatch{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px;align-items:start}
.dcol{background:#fff;border:1px solid var(--line);border-radius:var(--r);padding:14px;min-height:120px}
.dcol h4{font-size:14px;display:flex;justify-content:space-between;margin-bottom:10px}
/* The ⏰ Overdue column: stops carried forward from a day that has passed. Same
   red the late pill uses, on the column edge — no new colour, and it reads as
   "look here first" without shouting over the day's own work. */
.dcol.late{border-color:var(--bad);box-shadow:inset 3px 0 0 var(--bad)}
.dcol.late h4{color:var(--bad)}
.stopcard{border:1.5px solid var(--line);border-radius:11px;padding:9px 11px;margin-bottom:8px;font-size:13px;background:#fff}
.stopcard b{color:var(--ink)}
/* The order code at the head of the card is a <button> since 2026-09-01 — the way
   in to the staff order modal from the one screen that had none — so it needs the
   two things a button does not inherit (the card's own type and no chrome of its
   own) and the ring every keyboard-reachable control on this sheet draws, the same
   2px var(--c1) as .bell and .thumb-lnk. The underline arrives on hover/focus only:
   at rest the code should read exactly as it always has, a bold code on a card,
   not a column of twenty links shouting over the pills beside them. */
.stopcard .codebtn{background:none;border:0;padding:0;margin:0;cursor:pointer;font:inherit;border-radius:4px}
.stopcard .codebtn:hover b,.stopcard .codebtn:focus-visible b{text-decoration:underline}
.stopcard .codebtn:focus-visible{outline:2px solid var(--c1);outline-offset:2px}
/* The stop standing in a window that is over its capacity — the same border and
   inset bar as .capcell.over above, so the red cell in the strip and the red card
   in the column read as one fact. Not .dcol.late's red: a card can be both, and
   these nest rather than compete (the late pill and the ⚠️ pill sit side by side). */
.stopcard.over{border-color:var(--bad);box-shadow:inset 3px 0 0 var(--bad)}
.seq{display:inline-grid;place-items:center;min-width:22px;height:22px;border-radius:7px;background:var(--grad);
  color:#fff;font:800 12px var(--disp);margin-right:6px}
.seq.pk{background:linear-gradient(135deg,#8b5cf6,#6d28d9)}
.thumb{width:74px;height:74px;object-fit:cover;border-radius:10px;border:1px solid var(--line);cursor:pointer}
/* THE CUSTOMER'S DELIVERY PHOTO IS A LINK now rather than an image with a click
   handler (public/portal.js deliveryProofBlock), so it needs the focus ring every
   other keyboard-reachable control on this sheet draws — the same 2px var(--c1) as
   .bell, .pday and the choice tiles. No colour and no underline of its own: what a
   reader is looking at is the photograph. The width bound and the ordinary
   line-height are for the case where the bytes never arrive and the img's own alt
   sentence takes the frame — that text is the error state, and a 74px box with its
   line-height zeroed would clip it to one line. */
.thumb-lnk{display:inline-block;border-radius:10px;max-width:190px;color:var(--mut);font-size:12px;text-decoration:none}
.thumb-lnk:focus-visible{outline:2px solid var(--c1);outline-offset:2px}
.chk{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px dashed var(--line);font-size:14px;color:var(--ink)}
.chk .tgl{margin-left:auto;display:flex;gap:6px}
.chk .tgl button{border:1.5px solid var(--line);background:#fff;border-radius:8px;padding:4px 12px;cursor:pointer;font:700 12px var(--font)}
.chk .tgl button.pass.on{background:#dcf5e7;border-color:#0d9d58;color:#106b3f}
.chk .tgl button.fail.on{background:#fdecec;border-color:#c93434;color:#a32626}
.copybox{display:flex;gap:8px;align-items:center;background:#f0f9ff;border:1.5px dashed var(--c1);border-radius:11px;padding:11px 14px}
.copybox b{font:800 18px var(--disp);letter-spacing:.08em;color:var(--c2)}
.walletbig{font:800 34px var(--disp);color:var(--c2)}
/* A store-credit balance below zero, in the same red as the .stat .v.bad tiles
   and the LTV pill — 34px of brand blue is how a windfall is typeset, not a
   balance the customer cannot spend. portal.js creditPanel decides, off
   isMoneyOut(); money-format.test.js pins both halves against each other. */
.walletbig.bad{color:var(--bad)}
.hold-band{background:#fdf1dc;border:1.5px solid #e5b568;border-radius:11px;padding:10px 14px;color:#8a5800;font-weight:600;font-size:14px}
/* The hold band's shape in the .pill.red palette, for the admin order modal's
   failed-card notice (admin.js orderModal). Same geometry on purpose: a manager
   reads these two bands in the same place for the same reason — something is
   stopping this order — and only the colour says which kind of stop it is. */
.fail-band{background:#fdecec;border:1.5px solid #e9a3a3;border-radius:11px;padding:10px 14px;color:#a32626;font-weight:600;font-size:14px}
/* WHERE IN THE ROUND — the hold band's geometry in the brand's blue, on the
   customer's order page (portal.js setRound). Same shape as the two bands above
   because it is read in the same place for the same reason — something about the
   TIMING of this order that the tracker cannot say — and only the colour says
   which kind of news it is. A round running past the promised window borrows the
   hold band's amber outright rather than approximating it: a customer should not
   have to learn that two slightly different yellows mean the same thing. */
.round-band{background:#eef2ff;border:1.5px solid #b9c3f5;border-radius:11px;padding:10px 14px;color:#33409a;font-weight:600;font-size:14px}
.round-band.late{background:#fdf1dc;border-color:#e5b568;color:#8a5800}
.itemrow2{display:grid;grid-template-columns:2fr 70px 1.6fr auto;gap:8px;margin-bottom:8px}
.leaflet-container{font-family:var(--font)}
.drvdot{background:var(--grad);width:100%;height:100%;border-radius:50%;border:3px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.4)}
/* THE FLEET PIN THAT POINTS. A bare .drvdot is a circle, and a circle cannot be
   rotated visibly — so the dispatch map's driver (admin.js driverIcon) wraps one
   in .drvpin and hangs a separate arrow off it. The customer's tracking map and
   the driver's own "You" pin keep using .drvdot on its own through common.js
   divIcon(); nesting is what turns these rules on, so those two are untouched.

   MIS-ANCHORING IS THE FAILURE MODE, so the geometry is arranged to make it
   impossible rather than to look right at one zoom. Both children are pinned at
   left/top 50% of the 44px box and pulled back by exactly half their own
   border-box in margin — no percentage translate — which puts each element's
   UNTRANSFORMED centre on the pin's centre, and therefore puts the default
   transform-origin there too. The arrow's rotate() then turns about the pin's
   centre, and the translateY that follows it in the same declaration rides in
   the rotated frame: 15px "up" becomes 15px along the bearing. Base at 10px from
   the centre so it overlaps the 13px dot and reads as attached, tip at 20px —
   inside the 22px half-box driverIcon() asks for, with clearance on every
   bearing rather than flush with the edge on the four cardinals.

   --hdg is degrees clockwise from north, written inline by driverIcon(). CSS
   rotate() is clockwise too and 0deg leaves the triangle pointing up, which is
   north — the two conventions already agree, so nothing here converts. The 0deg
   fallback only exists so a broken custom property degrades to an unrotated
   arrow rather than to an invalid transform; driverIcon never omits the value,
   and a NULL heading gets no .drvhdg element at all rather than a north default. */
.drvpin{position:relative;width:100%;height:100%}
.drvpin .drvdot{position:absolute;left:50%;top:50%;width:26px;height:26px;margin:-13px 0 0 -13px}
.drvhdg{position:absolute;left:50%;top:50%;width:0;height:0;margin:-5px 0 0 -6px;
  border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:10px solid var(--c2);
  transform:rotate(var(--hdg,0deg)) translateY(-15px);
  filter:drop-shadow(0 0 1.5px #fff) drop-shadow(0 1px 2px rgba(0,0,0,.35))}
.stopdot{background:#8b5cf6;width:100%;height:100%;border-radius:50%;border:2px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,.35)}
.stopdot.dl{background:#0d9d58}
/* A carried-forward stop keeps its pickup/delivery colour — the driver still
   needs to know which it is — and gains a red ring so the map agrees with the
   ⏰ Overdue column about which pins blew their date. */
.stopdot.late{border-color:var(--bad);box-shadow:0 0 0 2px var(--bad),0 1px 5px rgba(0,0,0,.35)}
.homedot{background:#0B1B26;width:100%;height:100%;border-radius:50%;border:3px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.4)}
