/* ═══════════════════════════════════════════════════════════
   Quotation Suite — app stylesheet.
   Mobile first (390px), tap targets >=48px, RTL by [dir],
   plus a real A4 print sheet at the bottom.
   Theme colours come from JS as --primary / --accent.
   ═══════════════════════════════════════════════════════════ */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0}

:root{
  --primary:#1B3A57;
  --accent:#E0912F;
  --sidebar:#12212F;
  --ink:#131A22;
  --body:#3A4753;
  /* Darkened 2026-08-24 to clear WCAG AA. The old #7A8794 / #A3AEB9 scored
     3.37:1 and 2.07:1 against --bg — both under the 4.5 floor for normal
     text, and both carry real text here (.sect headings, row subtitles,
     .f label hints, catalogue unit labels, category counts). Measured, not
     eyeballed: --mute now 4.94:1 on --bg, --faint 4.65:1. The hierarchy is
     compressed rather than lost — this is the same fix already applied to
     the Renewals / Attendance / Building boards on 2026-08-22. */
  --mute:#5F6C79;
  --faint:#63707D;
  --line:#E9E5DE;
  --line-2:#E4E0D8;
  --bg:#F7F5F1;
  --surface:#FFFFFF;
  --good:#2E7D5B;
  --good-soft:#E4F0EA;
  --warn:#B0741F;
  --warn-soft:#FBF1DF;
  --bad:#C0432F;
  --bad-soft:#FBEBE8;
  /* Text-on-tint inks. The base hues above stay exactly as they are —
     they carry stripes, big numbers and icons at large sizes, where they
     already clear the 3:1 large-text floor and where the greyscale
     separation between the four statuses was tuned. But an 11px bold pill
     label is NORMAL text and needs 4.5:1, which --warn (3.50:1 on its own
     tint) and --good (4.27:1) did not reach. Measured after: 5.01 / 4.95 /
     5.12 / 4.67. Same split the Renewals and Attendance boards already use. */
  --good-ink:#2A7252;
  --warn-ink:#8F5D14;
  --bad-ink:#B03D2A;
  --mute-ink:#5A6672;
  --ui:"Barlow",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  --ar:"IBM Plex Sans Arabic","Barlow",system-ui,sans-serif;
  --num:"Space Grotesk",system-ui,sans-serif;
  --ease:cubic-bezier(.22,1,.36,1);
  --r:14px;
  --shadow:0 2px 10px rgba(19,26,34,.07);
  --shadow-l:0 -6px 22px rgba(19,26,34,.07);
}

body{
  font-family:var(--ui);background:var(--bg);color:var(--ink);
  line-height:1.5;-webkit-font-smoothing:antialiased;
  overscroll-behavior-y:none;
}
body[dir="rtl"]{font-family:var(--ar)}
.num{font-family:var(--num);font-variant-numeric:tabular-nums}
button{font:inherit;color:inherit;border:0;background:none;cursor:pointer}
input,select,textarea{font:inherit;color:inherit}
h1,h2,h3{margin:0;font-weight:700;letter-spacing:-.015em;line-height:1.2}

/* ── app shell ─────────────────────────────────────────── */
#app{max-width:520px;margin:0 auto;min-height:100vh;min-height:100dvh;
  display:flex;flex-direction:column;background:var(--bg);position:relative}
/* Tablets (iPad portrait etc, ~640–899px) were still boxed into the
   phone's 520px column — centred with large dead margins either side, the
   catalogue grid's 3rd column (see .grid below, same 640px point) cramped
   into a container that never grew to fit it. Below 900px the layout is
   still the phone one (single column, the drawer nav) — this only widens
   the column itself, not the layout, so mobile's own 520px, and the real
   desktop sidebar layout at 900px+ below, are both untouched. */
@media (min-width:640px){
  #app{max-width:720px}
}
@media (min-width:900px){
  #app{max-width:900px;box-shadow:0 0 60px rgba(19,26,34,.07)}
}
.scroll{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding-bottom:12px}

/* ── top bar ───────────────────────────────────────────── */
.top{display:flex;align-items:center;gap:12px;padding:14px 16px 10px;
  background:var(--surface);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:20}
.top .ic{width:44px;height:44px;border-radius:11px;border:1.5px solid var(--line-2);
  display:grid;place-items:center;flex:none;background:var(--surface)}
.top .ic:active{background:var(--bg)}
.top .mid{flex:1;min-width:0}
.top .eyebrow{font-size:12px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--mute)}
.top h1{font-size:17px}
[dir="rtl"] .top .ic svg{transform:scaleX(-1)}

/* height matches .top .ic (44px, the hamburger/help buttons either side)
   exactly — it was sized by its own text padding before, a few px shorter,
   so its top/bottom edges never lined up with the buttons flanking it
   even though every element was individually centered on the row. */
.langtog{display:flex;height:44px;border:1.5px solid var(--line-2);border-radius:9px;overflow:hidden;flex:none}
.langtog b,.langtog i{display:flex;align-items:center;padding:0 11px;font-size:13px;font-weight:700;font-style:normal;line-height:1}
.langtog b{background:var(--primary);color:#fff}
.langtog i{color:var(--mute);font-weight:600}

/* ── progress ──────────────────────────────────────────── */
.prog{display:flex;gap:6px;padding:12px 16px 14px;background:var(--surface)}
.prog i{height:5px;flex:1;border-radius:99px;background:var(--line-2);transition:background .3s}
.prog i.on{background:var(--primary)}

/* ── buttons ───────────────────────────────────────────── */
.btn{display:flex;align-items:center;justify-content:center;gap:9px;width:100%;
  min-height:56px;border-radius:var(--r);font-size:17px;font-weight:700;letter-spacing:-.01em;
  background:var(--primary);color:#fff;transition:transform .15s var(--ease),opacity .2s}
.btn:active{transform:scale(.985)}
.btn[disabled]{opacity:.4;pointer-events:none}
.btn.ghost{background:var(--surface);color:var(--ink);border:1.5px solid var(--line-2)}
.btn.amber{background:var(--accent);color:#1B1206}
.btn.danger{background:var(--bad-soft);color:var(--bad);border:1.5px solid #F1CFC9}
.btn.sm{min-height:44px;font-size:15px;border-radius:11px;width:auto;padding:0 16px}
.btnrow{display:flex;gap:8px}
.btnrow .btn{flex:1}
/* a plain-text action, deliberately quieter than every .btn around it — for
   something rare and one-way (deleting a record) that shouldn't visually
   compete with the actual send/save actions on the same screen. Still a
   real 44px tap target, just reads as "this is different," not "this is
   another option in the same list." */
.linkbtn{display:flex;align-items:center;justify-content:center;gap:7px;width:auto;
  min-height:44px;padding:0 14px;background:none;border:0;font-size:14.5px;font-weight:600;
  color:var(--mute)}
.linkbtn:active{opacity:.6}
.linkbtn.danger{color:var(--bad)}

/* ── cards & lists ─────────────────────────────────────── */
.pad{padding:16px}
.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);padding:16px;margin-bottom:10px}
.sect{font-size:12px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--mute);
  margin:20px 0 10px;padding:0 2px}
.muted{color:var(--mute);font-size:14px}
.empty{text-align:center;color:var(--mute);padding:48px 24px;font-size:15px;line-height:1.6}

/* ── fields ────────────────────────────────────────────── */
.f{margin-bottom:14px}
.f label{display:block;font-size:14px;font-weight:600;color:var(--body);margin-bottom:6px}
.f label span{font-weight:400;color:var(--faint)}
/* the "also add to your service list" opt-in on the custom-item dialog —
   unticked by default, so the item stays one-off for this quote unless
   asked otherwise. */
.checkrow{display:flex;align-items:center;gap:10px;padding:12px 14px;margin-top:2px;
  background:var(--bg);border-radius:11px;cursor:pointer}
.checkrow input[type="checkbox"]{width:20px;height:20px;flex:none;accent-color:var(--primary)}
.checkrow span{font-size:14px;font-weight:600;color:var(--body)}
/* A bare <input type="file"> renders the browser's own grey "Choose File"
   chip, which is the one control in the whole app that looked like it
   belonged to a different piece of software. Styling the button half to
   match .btn.ghost.sm keeps the native file picker (and its accessibility)
   while making it look like everything around it. */
.f input[type="file"]{padding:9px;font-size:14px;color:var(--mute);cursor:pointer}
.f input[type="file"]::file-selector-button{
  font:inherit;font-weight:700;font-size:14px;color:var(--ink);
  background:var(--surface);border:1.5px solid var(--line-2);border-radius:9px;
  min-height:36px;padding:0 14px;margin-inline-end:12px;cursor:pointer;
  transition:background .18s var(--ease),border-color .18s var(--ease)}
.f input[type="file"]::file-selector-button:hover{background:var(--bg);border-color:var(--mute)}
.f input,.f select,.f textarea{
  width:100%;min-height:52px;padding:13px 14px;border-radius:11px;
  border:1.5px solid var(--line-2);background:var(--surface);color:var(--ink);font-size:16px}
.f textarea{min-height:92px;resize:vertical;line-height:1.5}
/* Phone numbers, emails, IBANs and URLs are LTR data even inside an Arabic
   form. Left to inherit the page direction, bidi reorders them for display:
   "+971 6 555 0142" rendered as "0142 555 6 971+", which is not a number
   anyone can dial. dir="ltr" fixes the ORDER; text-align keeps the field
   aligned with the Arabic labels above it, so only the reordering changes. */
[dir="rtl"] .f .ltrnum{text-align:right}
/* a bare <select> is never actually the same height as its sibling
   <input> despite sharing every rule above — each browser draws its own
   native arrow and reserves its own space for it, which quietly overrides
   how the shared padding/min-height above gets applied (worst on iOS
   Safari). appearance:none hands that back to us completely: same
   chevron everywhere, and the box model finally matches the input next
   to it pixel for pixel instead of "close, on some devices." */
.f select{appearance:none;-webkit-appearance:none;-moz-appearance:none;
  padding-inline-end:38px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%237A8794' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-size:18px;background-position:right 12px center}
[dir="rtl"] .f select{background-position:left 12px center}
.f input:focus,.f select:focus,.f textarea:focus{outline:0;border-color:var(--primary);
  box-shadow:0 0 0 3px rgba(27,58,87,.12)}
/* required-field validation state (e.g. customer name on step 1) — a red
   border makes it obvious which field blocked "Next", not just a toast
   that's already gone by the time the eye lands on the form. */
.f input.error{border-color:var(--bad)}
.f input.error:focus{border-color:var(--bad);box-shadow:0 0 0 3px rgba(192,67,47,.12)}
.f .hint{font-size:13px;color:var(--mute);margin-top:5px}
.f2{display:grid;grid-template-columns:1fr 1fr;gap:10px}

.sw{display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:14px 0;border-bottom:1px solid var(--line)}
.sw:last-child{border-bottom:0}
.sw .lbl{font-size:15px;font-weight:600}
.sw .sub{font-size:13px;color:var(--mute);font-weight:400;margin-top:2px}
.toggle{width:52px;height:31px;border-radius:99px;background:var(--line-2);position:relative;flex:none;transition:background .2s}
.toggle::after{content:"";position:absolute;top:3px;left:3px;width:25px;height:25px;border-radius:50%;
  background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.25);transition:transform .2s var(--ease)}
.toggle.on{background:var(--good)}
.toggle.on::after{transform:translateX(21px)}
[dir="rtl"] .toggle::after{left:auto;right:3px}
[dir="rtl"] .toggle.on::after{transform:translateX(-21px)}

/* ── catalogue grid (the heart of the app) ─────────────── */
/* scroll-padding on the inline-end matters: the row scrolls horizontally,
   and without it the final chip sits flush against the viewport edge with
   nothing after it, which reads as "cut off" rather than "scrollable".
   The trailing gap is the affordance. */
.chips{display:flex;gap:8px;padding:14px 16px 10px;overflow-x:auto;scrollbar-width:none;
  scroll-padding-inline-end:16px}
.chips::after{content:"";flex:none;inline-size:4px}
.chips::-webkit-scrollbar{display:none}
.chip{display:flex;align-items:center;gap:7px;min-height:40px;padding:0 15px;border-radius:99px;
  font-size:15px;font-weight:600;white-space:nowrap;background:var(--surface);
  color:var(--body);border:1.5px solid var(--line-2);flex:none}
.chip.on{background:var(--primary);color:#fff;border-color:var(--primary)}
.chip u{width:8px;height:8px;border-radius:99px;text-decoration:none;flex:none}
/* the count is its own small pill, not bare text floating next to the
   label — a plain number there read as a stray digit, not a count. */
.chip i{font-style:normal;display:inline-flex;align-items:center;justify-content:center;
  min-width:19px;height:19px;padding:0 5px;border-radius:99px;background:var(--bg);
  color:var(--mute);font-size:11.5px;font-weight:700;font-family:var(--num);flex:none}
.chip.on i{background:rgba(255,255,255,.24);color:#fff}
/* the "+ Add new quote" chip at the end of the tabs row — solid so it
   reads as an action among filter toggles, pushed to the row's far end
   when there's room, part of the same horizontal scroll when there isn't. */
.newqchip{background:var(--primary);color:#fff;border-color:var(--primary);
  margin-inline-start:auto}
.newqchip:active{background:var(--accent);border-color:var(--accent)}

.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;padding:2px 16px 0}
@media (min-width:640px){.grid{grid-template-columns:repeat(3,minmax(0,1fr))}}
.item{cursor:pointer;-webkit-tap-highlight-color:transparent;padding:14px;border-radius:var(--r);background:var(--surface);border:1.5px solid var(--line);
  min-height:120px;display:flex;flex-direction:column;text-align:start;transition:border-color .18s,box-shadow .18s}
/* the qty stepper itself (a solid navy pill with the number in it) is
   already a clear, unambiguous "you added this" signal on its own — a
   thick 2px border stacked on top of it was a second loud signal doing
   the same job, and across a grid of cards that's what read as busy:
   selected cards going noticeably darker/heavier than their neighbours
   in two different ways at once. A quiet shadow lift is enough. */
.item.on{box-shadow:var(--shadow)}
.item:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
.item .hd{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}
.item .iw{width:44px;height:44px;border-radius:11px;display:grid;place-items:center;flex:none}
.item .nm{margin-top:12px;font-size:15px;font-weight:600;line-height:1.25}
.item .pr{margin-top:3px;font-size:14px;font-weight:600;color:var(--mute)}
.item .un{font-size:12px;color:var(--faint)}
/* a one-off item added on this quote only (not in the catalogue) — same
   card shape as a real one so it's obviously "selected", dashed border and
   a small tag so it reads as temporary, not a permanent service. */
.item.custom{border-style:dashed;border-color:var(--warn)}
.item.custom .iw{background:var(--warn-soft);color:var(--warn)}
.item.custom .customtag{display:inline-block;margin-inline-start:5px;padding:1px 7px;
  border-radius:99px;background:var(--warn-soft);color:var(--warn);
  font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;vertical-align:1px}

.qty{display:flex;align-items:center;gap:2px;background:var(--primary);border-radius:9px;padding:3px;flex:none}
.qty button{width:30px;height:30px;border-radius:7px;display:grid;place-items:center}
.qty b{min-width:20px;text-align:center;font-size:15px;font-weight:700;color:#fff;font-family:var(--num)}

.dashed{margin:10px 16px 0;display:flex;align-items:center;justify-content:center;gap:9px;
  min-height:56px;border:1.5px dashed #CFC9BE;border-radius:var(--r);color:var(--mute);
  font-size:15px;font-weight:600;width:calc(100% - 32px)}

/* ── pinned total bar ──────────────────────────────────── */
.footbar{background:var(--surface);border-top:1px solid var(--line);
  padding:12px 16px calc(14px + env(safe-area-inset-bottom));box-shadow:var(--shadow-l);
  position:sticky;bottom:0;z-index:15}
.footbar .r{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:11px}
.footbar .r span{font-size:14px;font-weight:600;color:var(--mute)}
.footbar .r b{font-size:28px;font-weight:700;letter-spacing:-.02em;font-family:var(--num)}
/* The one standard button-row treatment: Back at the true left edge, the
   primary action at the true right edge, every step, every width — not
   glued together in the middle the way .btnrow splits two buttons 50/50.
   When there's a money row above it (every step but the first), this is
   the second child of .footbar .in; when there isn't, it's the only one. */
.spread{display:flex;align-items:center;justify-content:space-between;gap:12px}
.spread .btn{width:auto}

/* ── money rows ────────────────────────────────────────── */
.rows{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
.rows .row{display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:14px 16px;border-bottom:1px solid var(--line);font-size:15px}
.rows .row:last-child{border-bottom:0}
.rows .row b{font-family:var(--num);font-weight:600}
.rows .row.big{background:var(--primary);color:#fff}
.rows .row.big span{font-size:13px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;opacity:.8}
.rows .row.big b{font-size:22px;font-weight:700}

.seg{display:flex;border:1.5px solid var(--line-2);border-radius:11px;overflow:hidden;background:var(--surface)}
.seg button{flex:1;min-height:48px;font-size:14px;font-weight:600;color:var(--body)}
.seg button.on{background:var(--primary);color:#fff}

/* ── customer picker (step 1) ──────────────────────────── */
.searchwrap{position:relative;display:flex;align-items:center}
.searchwrap svg{position:absolute;inset-inline-start:13px;color:var(--faint);pointer-events:none}
.searchwrap input{padding-inline-start:38px}
.custlbl{font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
  color:var(--mute);margin:12px 2px 6px}
.custrow{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;
  padding:11px 12px;border-radius:10px;text-align:start;border-top:1px solid var(--line)}
.custrow:first-child{border-top:0}
.custrow:active{background:var(--bg)}
.custrow .who b{display:block;font-size:14.5px;font-weight:600}
.custrow .who span{font-size:12.5px;color:var(--mute)}
.custrow .pill{background:var(--bg);color:var(--mute);flex:none}

/* anchored dropdown — see UI.dropdown(). Reuses .menu's own row styling
   inside it, just in a smaller floating card instead of the full sheet. */
.dd{position:fixed;z-index:90;background:var(--surface);border:1px solid var(--line);
  border-radius:14px;box-shadow:var(--shadow-l);min-width:220px;max-width:280px;overflow:hidden}
.dd .menu{border:0;border-radius:0}
.dd .menu button{min-height:52px;padding:13px 15px}

/* ── quote list ────────────────────────────────────────── */
.qrow{background:var(--surface);border:1px solid var(--line);border-radius:12px;
  margin-bottom:8px;overflow:hidden}
.qrow .qopen{display:flex;align-items:center;gap:12px;width:100%;padding:13px 14px 11px;text-align:start}
.qrow .who{flex:1;min-width:0}
.qrow .who b{display:block;font-size:15px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* nowrap+ellipsis, same treatment as .who b just above — without it, this
   line wraps to two lines once .veh's column also claims some of the row's
   width, and .veh (a single line, vertically centered) then renders
   visually sandwiched between the two wrapped lines instead of beside a
   clean two-line block. Found on a 390px screenshot, 2026-08-23. */
.qrow .who span{display:block;font-size:13px;color:var(--mute);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* the vehicle/reference column — secondary info, so it's muted and sized
   like .who's own subtitle line, capped and truncated rather than
   squeezing the customer name it sits next to. */
/* unicode-bidi:plaintext makes each cell take its direction from its own
   first strong character, not from the page. Without it a Latin vehicle
   name in an Arabic list truncated at its LOGICAL start — "Nissan Patrol
   2019" rendered as "…n Patrol 2019", dropping the make, which is the
   part that identifies the vehicle. An Arabic vehicle name still truncates
   at its own end, because the rule follows the content either way. */
.qrow .veh{flex:none;max-width:90px;text-align:end;font-size:12.5px;color:var(--mute);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;unicode-bidi:plaintext}
.qrow .amt{text-align:end;flex:none}
.qrow .amt b{display:block;font-family:var(--num);font-size:16px;font-weight:700}
/* The dashboard's follow-up rows are a bare <button class="qrow">, not the
   quotes list's <div class="qrow"> wrapping a <button class="qopen">. A
   button is neither full-width nor a flex container by default, so those
   rows were shrinking to their own text and the amount was colliding with
   the customer name — on every width, on the live site. The layout the
   .qopen rule above gives a list row has to be restated here because there
   is no .qopen element in this markup to carry it. */
.qrow.chaserow{display:flex;align-items:center;gap:12px;width:100%;
  padding:12px 14px;text-align:start}
/* one clear "Edit" (icon + word) plus a dropdown for everything else — see
   V.quoteMenu(). A single joined button anchored to the row's right edge,
   NOT a bar stretched across the width — sized to match the amount/status
   block it sits under, not the whole card. */
.qrow .qfoot{display:flex;justify-content:flex-end;padding:0 14px 12px}
.qrow .qeditgrp{display:flex;align-items:stretch;border:1.5px solid var(--line-2);
  border-radius:10px;overflow:hidden;flex:none}
/* 44px min-height, not 38 — this is the row's primary action on a phone
   and was the last sub-44px tap target in the app. */
.qrow .qedit{display:flex;align-items:center;gap:6px;padding:0 12px;min-height:44px;
  color:var(--primary);font-weight:700;font-size:13.5px;white-space:nowrap}
.qrow .qedit:active{background:var(--bg)}
/* the divider is a box-shadow, not a real border — a border on only one
   side eats into that side's content box under border-box sizing, which
   silently shifted place-items:center off true-centre by the border's own
   width (measured: 1px, small but real, and exactly the kind of thing
   that reads as "slightly wrong" without anyone being able to say why).
   A box-shadow paints the same line without touching the box model at
   all, so the full 44px stays available to centre the icon in. */
.qrow .qmore{width:44px;flex:none;display:grid;place-items:center;min-height:44px;
  color:var(--mute);box-shadow:inset 1.5px 0 0 var(--line-2)}
[dir="rtl"] .qrow .qmore{box-shadow:inset -1.5px 0 0 var(--line-2)}
.qrow .qmore:active{background:var(--bg);color:var(--primary)}
/* points up while its dropdown is open, so re-tapping it reads as "close
   this," not "open it again" — see UI.dropdown()/.closeDropdown(). */
.qrow .qmore svg{transition:transform .15s var(--ease)}
.qrow .qmore.dd-open{background:var(--bg);color:var(--primary)}
.qrow .qmore.dd-open svg{transform:rotate(180deg)}

/* Column labels above the list — see V.quotes()'s qhead markup. This
   reuses .qrow's OWN classes (qopen/who/veh/amt/qfoot) on the header row
   instead of a second copy of the same widths, so the labels are governed
   by the exact same flex math as a real row and can never drift out of
   alignment with it as that math changes above. Paint-only overrides
   below: a tinted label band instead of a card, in the same treatment the
   invoice's own line-item table header already uses (.doc thead th
   above) — reusing an existing convention rather than inventing one.
   The .qfoot inside is a real (invisible) copy of the row's Edit button —
   it exists purely to reserve the same trailing width a real Edit button
   takes, which is what makes AMOUNT land above the actual amount column
   instead of drifting toward Edit (the bug this replaced: a bare label
   row has no Edit button to stop at, so its own flush-right edge sat
   further right than the real column's). Hidden on narrower screens,
   where .qfoot already drops to its own line below .qopen and so needs
   no horizontal reservation at all. */
.qrow.qhead{background:#F1F4F7;border:0;box-shadow:none}
.qhead .who,.qhead .veh,.qhead .amt{
  font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--primary)}
.qhead .qfoot{display:none}

/* On a wide window #app drops its max-width entirely (see the min-width:900px
   rule above) and this list now fills that full width rather than being
   capped to its own narrower column — no dead grey space beside it. The
   row itself lays out as one line at this width: name growing on the
   left, amount+status as their own column, Edit anchored at the true
   right edge — instead of stacking amount/status/Edit on top of each
   other. The name is capped (not left to grow arbitrarily far on an
   ultra-wide window) so it can't drag amount/Edit into a huge, odd gap —
   the row still uses the full width as a card, it just doesn't stretch
   short text to fill it. */
@media (min-width:900px){
  .qrow{display:flex;align-items:center}
  .qrow .qopen{flex:1;min-width:0;padding:14px 6px 14px 14px}
  .qrow .who{flex:0 1 480px}
  .qrow .veh{max-width:160px}
  /* .who's fixed 480px basis (above) used to leave dead space before .amt,
     which sat wherever .who's box happened to end rather than at the
     row's true right edge — the "amount is far from Edit" gap. An auto
     start-margin absorbs all of that leftover space instead, snapping
     .amt flush against .qfoot regardless of how short the name/vehicle
     text actually is. */
  .qrow .amt{margin-inline-start:auto;margin-inline-end:14px}
  .qrow .qfoot{padding:0 14px 0 0}

  /* .qhead's who/veh/amt already inherit the sizing above for free (see
     .qrow.qhead) — the only thing left to switch on here is the ghost
     .qfoot itself, so AMOUNT has the same trailing width to snap
     flush-right against that a real row's Edit button provides. */
  .qhead .qfoot{display:flex;visibility:hidden;pointer-events:none}
}
.pill{display:inline-block;padding:2px 8px;border-radius:99px;font-size:11px;font-weight:700;
  letter-spacing:.06em;text-transform:uppercase;margin-top:3px}
.pill.draft{background:var(--line);color:var(--mute-ink)}
.pill.sent{background:var(--warn-soft);color:var(--warn-ink)}
.pill.accepted{background:var(--good-soft);color:var(--good-ink)}
.pill.rejected{background:var(--bad-soft);color:var(--bad-ink)}
.pill.invoice{background:#E8EEF4;color:var(--primary)}

/* ── settings menu ─────────────────────────────────────── */
.menu{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
.menu button{display:flex;align-items:center;gap:14px;width:100%;padding:16px;text-align:start;
  border-bottom:1px solid var(--line);min-height:60px}
.menu button:last-child{border-bottom:0}
/* The selected pane needs to look selected. It used to differ from its
   neighbours only by the icon tile turning navy, which is a 38px cue on a
   full-width row — on desktop, where this list is a TAB STRIP and not
   navigation, there was almost nothing saying which pane you were looking
   at. Now the row is tinted, the label goes ink-dark and bold, and a solid
   bar marks the selected edge — the same weight of cue the Service list's
   category rail already uses, so the two lists behave alike. */
.menu button:active{background:var(--bg)}
.menu button.on{background:var(--bg);position:relative}
.menu button.on::before{content:"";position:absolute;inset-inline-start:0;top:0;bottom:0;
  width:3px;background:var(--primary)}
.menu button.on .mt b{color:var(--ink);font-weight:700}
/* The chevron promises "this opens something else". On the row you are
   already on, it promises wrongly — so the selected row drops it. */
.menu button.on .chev{visibility:hidden}
.menu .mi{width:38px;height:38px;border-radius:10px;background:var(--bg);display:grid;place-items:center;flex:none}
.menu button.on .mi{background:var(--primary);color:#fff}
.menu .mt{flex:1;min-width:0}
.menu .mt b{display:block;font-size:15px;font-weight:600}
.menu .mt span{font-size:13px;color:var(--mute)}
.menu button.danger .mi{background:#FBEAE6;color:var(--bad)}
.menu button.danger .mt b{color:var(--bad)}
[dir="rtl"] .menu .chev{transform:scaleX(-1)}

/* ── price list / team rows ────────────────────────────── */
.lrow{display:flex;align-items:center;gap:12px;background:var(--surface);border:1px solid var(--line);
  border-radius:12px;padding:12px 14px;margin-bottom:8px}
.lrow.off{opacity:.55}
.lrow .info{flex:1;min-width:0}
/* the name still truncates with an ellipsis rather than wraps if it's
   genuinely long even with the extra room below — a safety net, not the
   main fix (see the narrow-phone rule further down). */
.lrow .info b{display:block;font-size:15px;font-weight:600;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
/* display:block is what makes the ellipsis work at all — overflow and
   text-overflow are both ignored on an inline box, so this line was
   running on under the row's action buttons instead of truncating
   ("Last quote 6 days a" with the eye icon over the "go"). Same bug as
   the Renewals Board's .lbl/.val/.sub row labels; third time in this repo. */
/* display:block is what makes the ellipsis work at all — overflow and
   text-overflow are both ignored on an inline box, so this line was
   running on under the row's action buttons instead of truncating
   ("Last quote 6 days a" with the eye icon over the "go"). Same bug as
   the Renewals Board's .lbl/.val/.sub row labels; third time in this repo.
   The child combinator is required, not tidiness: the team row nests a
   .rolechip <span> inside its <b>, and a descendant selector blocks that
   too — which stretches the OWNER/STAFF pill across the whole row. */
.lrow .info > span{display:block;font-size:13px;color:var(--mute);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lrow .price{font-family:var(--num);font-weight:700;font-size:15px;flex:none}
.lrow .acts{display:flex;gap:6px;flex:none}
/* On a narrow phone the tick, grab-arrows, price and eye/edit buttons
   alone leave so little width for the name column that even short names
   like "Oil Change" were being cut down to "Oil Cha…" — technically no
   longer wrapping (the fix above), but now unreadable, which is worse.
   Give the name its own full-width line instead of fighting everything
   else for space on one line; tick, grab, price and the action buttons
   move to a second line together. Tablets and up already had enough room
   on one line (see the earlier tablet screenshot) so this is phone-only.
   Scoped to .svcrow (price-list rows specifically, tagged in settings.js)
   — .lrow is shared with the team list, which has no tick/price to push
   aside and would just end up with three broken lines instead. */
@media (max-width:599px){
  .lrow.svcrow{flex-wrap:wrap}
  .lrow.svcrow .info{order:1;flex:1 1 100%}
  .lrow.svcrow .tick{order:2}
  .lrow.svcrow .price{order:3;margin-inline-start:auto}
  .lrow.svcrow .acts{order:4}
}
/* 44px, not 38 — these are the edit / show-hide controls on every service
   and team row, and were the last sub-44px targets on those two screens. */
.lrow .acts button{width:44px;height:44px;border-radius:11px;background:var(--bg);display:grid;place-items:center}

/* ── dashboard ─────────────────────────────────────────── */
.tiles{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
.tile{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);padding:15px}
.tile .k{font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--mute)}
.tile .v{font-family:var(--num);font-size:26px;font-weight:700;letter-spacing:-.02em;margin-top:5px}
.tile .s{font-size:13px;color:var(--mute)}
.bar{display:flex;align-items:center;gap:10px;margin-bottom:9px}
.bar .bn{flex:1;font-size:14px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bar .bt{height:10px;border-radius:99px;background:#2E6DA4;flex:none;min-width:6px}
.bar .bv{font-family:var(--num);font-size:13px;font-weight:600;color:var(--mute);flex:none}

/* ═══════════════ home hero ═══════════════
   The identity block, the greeting and the day's three numbers in one
   surface. Base gets it as a clean card; the Pro layer further down turns
   it into the deep navy panel — same markup, same measurements. */
.hero{background:var(--surface);border:1px solid var(--line);border-radius:18px;
  overflow:hidden;margin-bottom:14px}
.herotop{display:flex;align-items:center;gap:14px;padding:16px}
.hero .mk{width:52px;height:52px;border-radius:14px;background:var(--bg);flex:none;
  display:grid;place-items:center;font-weight:700;font-size:17px;overflow:hidden}
.hero .mk img{width:100%;height:100%;object-fit:contain}
.hgreet{flex:1;min-width:0}
.hgreet .hi{display:block;font-size:13px;font-weight:600;color:var(--mute);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hgreet b{display:block;font-size:17px;letter-spacing:-.015em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.hero .hout{flex:none}
/* three numbers, equal columns, hairline between. min-width:0 on the
   track is what stops a long money string widening its own column and
   squeezing the other two to nothing. */
.hstats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  border-top:1px solid var(--line);background:var(--bg)}
.hstat{padding:11px 10px;min-width:0;border-inline-start:1px solid var(--line)}
@media (min-width:560px){ .hstat{padding:12px 16px} }
.hstat:first-child{border-inline-start:0}
/* These wrap. Held on one line with an ellipsis they became "WON THIS
   MO…", "WAITING ON A…", "NEEDS ATTEN…" at 390px — three labels that no
   longer say what they measure. Two short lines beat one truncated one. */
.hstat span{display:block;font-size:11px;font-weight:700;letter-spacing:.04em;
  text-transform:uppercase;color:var(--mute);line-height:1.3;min-height:2.6em}
.hstat b{display:block;font-family:var(--num);font-size:16.5px;font-weight:700;
  letter-spacing:-.02em;margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media (min-width:560px){ .hstat b{font-size:19px} }
.hstat.warn b{color:var(--warn-ink)}
.hstat.good b{color:var(--good-ink)}
/* the primary action gets real presence — it is the reason the app exists */
.btn.hcta{min-height:64px;font-size:18px;margin-bottom:14px}

/* ═══════════ dashboard v2 — insight strip, sparklines, aging ═══════════
   Ordered the way the screen is: what needs you today, then the headline
   numbers, then the explanation. See dash.js for why that order. */

/* ── the insight strip ── */
.sect.first{margin-top:4px}
.insights{display:flex;flex-direction:column;gap:8px;margin-bottom:4px}
.ins{display:flex;align-items:flex-start;gap:11px;width:100%;text-align:start;
  padding:13px 14px;border-radius:12px;border:1px solid var(--line);
  background:var(--surface);min-height:44px}
.ins .insic{flex:none;display:grid;place-items:center;width:30px;height:30px;
  border-radius:9px;background:var(--bg);color:var(--body)}
.ins .instx{flex:1;min-width:0;font-size:14.5px;line-height:1.5;color:var(--ink)}
.ins .inschev{flex:none;color:var(--mute);align-self:center}
[dir="rtl"] .ins .inschev svg{transform:scaleX(-1)}
/* tone: the icon tile and the border carry it, never the sentence itself —
   body text stays at full ink so it is readable at every tone */
.ins.bad{border-color:#F0CCC5;background:var(--bad-soft)}
.ins.bad .insic{background:#F6DAD4;color:var(--bad-ink)}
.ins.warn{border-color:#EEDCB8;background:var(--warn-soft)}
.ins.warn .insic{background:#F6E6C8;color:var(--warn-ink)}
.ins.good{border-color:#C9E2D5;background:var(--good-soft)}
.ins.good .insic{background:#D6EADF;color:var(--good-ink)}
.ins.info .insic{background:#E4EDF5;color:#2E6DA4}
button.ins{cursor:pointer;transition:transform .18s var(--ease),box-shadow .18s var(--ease)}
button.ins:active{transform:scale(.99)}

/* ── tiles: sparkline + delta chip ── */
.tile{position:relative}
.tile .sparkwrap{margin-top:9px;height:26px;color:#2E6DA4;opacity:.85}
.tile .spark{display:block;width:100%;height:26px;overflow:visible}
/* time must still run forwards for an Arabic reader */
[dir="rtl"] .tile .spark{transform:scaleX(-1)}
.tile.warn .sparkwrap,.tile.warn .v{color:var(--warn-ink)}
.tile.good .v{color:var(--good-ink)}
.dlt{display:inline-flex;align-items:center;gap:2px;font-size:12px;font-weight:700;
  padding:1px 6px;border-radius:99px;margin-inline-start:4px;vertical-align:middle;
  background:var(--line);color:var(--mute-ink)}
.dlt.up{background:var(--good-soft);color:var(--good-ink)}
.dlt.down{background:var(--bad-soft);color:var(--bad-ink)}
.dlt svg{flex:none}
/* the footer fact on a tile that has no time series to draw */
.tile .footwrap{margin-top:9px;height:26px;display:flex;align-items:center}
.tfoot{display:inline-flex;align-items:center;gap:5px;font-size:12px;font-weight:600;color:var(--mute)}
.tfoot.warn{color:var(--warn-ink)}
.tfoot svg{flex:none}

/* a single plain fact under a block — the smallest unit of "insight" */
.factline{display:flex;align-items:flex-start;gap:8px;margin-top:10px;padding:0 2px;
  font-size:13.5px;line-height:1.5;color:var(--body)}
.factline svg{flex:none;margin-top:2px;color:var(--mute)}

/* ── aging ──
   Ordinal data, so the ramp runs calm → alarm rather than four unrelated
   hues. Validated with the dataviz validator (lightness, chroma, CVD and
   normal-vision separation all pass); every segment also carries a text
   label, which is what answers the validator's sub-3:1 contrast warning
   and keeps status off colour-alone. */
.agebar{display:flex;height:10px;border-radius:99px;overflow:hidden;background:var(--bg);gap:2px}
.ageseg{display:block;min-width:4px}
.ageseg.fresh,.agedot.fresh{background:#5E9BD1}
.ageseg.week,.agedot.week{background:#2E6DA4}
.ageseg.two,.agedot.two{background:#E0912F}
.ageseg.cold,.agedot.cold{background:#C0432F}
.agelist{margin-top:12px}
.agerow{display:flex;align-items:center;gap:10px;padding:7px 0;
  border-top:1px solid var(--line)}
.agerow:first-child{border-top:none}
.agerow.zero{opacity:.45}
.agekey{flex:1;min-width:0;display:flex;align-items:center;gap:8px;font-size:14px}
.agedot{width:9px;height:9px;border-radius:50%;flex:none}
.agen{flex:none;font-weight:700;font-size:15px;min-width:26px;text-align:end}
.agev{flex:none;font-size:13px;color:var(--mute);min-width:82px;text-align:end}
.subsect{font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
  color:var(--mute);margin:16px 0 8px}

/* ── validity block ── */
.vhd{display:flex;align-items:center;gap:7px;font-size:13px;font-weight:700;
  margin:2px 0 8px}
.vhd.bad{color:var(--bad-ink)} .vhd.warn{color:var(--warn-ink)}
.vhd svg{flex:none}

/* ── funnel ── */
.funnel{display:flex;flex-direction:column;gap:12px}
.fstage .fhd{display:flex;align-items:center;justify-content:space-between;margin-bottom:5px}
.fstage .fhd b{font-size:16px}
.ftrack{display:block;height:9px;border-radius:99px;background:var(--bg);overflow:hidden}
.ffill{display:block;height:100%;border-radius:99px;background:#2E6DA4}
.ffill.draft{background:var(--faint)}
.ffill.sent{background:#E0912F}
.ffill.accepted{background:var(--good)}
.ffill.rejected{background:var(--bad)}
.fstage .fv{font-size:12.5px;color:var(--mute);margin-top:4px}

/* ── month columns ──
   Six months is a time series; time reads along the x-axis, so this is a
   column chart, not the horizontal rows it used to be. */
.mchart{display:flex;align-items:flex-end;gap:6px;height:190px}
.mcol{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;gap:6px;height:100%}
.mcol .mv{font-family:var(--num);font-size:10.5px;font-weight:700;color:var(--mute);
  white-space:nowrap;height:14px;overflow:hidden;text-overflow:ellipsis;max-width:100%}
/* No track fill. A visible grey column behind each bar reads as a second
   data series ("what's the grey part?") — in a column chart the bar IS the
   value and the empty space above it is just empty space. A hairline
   baseline does the job of showing where zero is. */
.mcol .mtrack{flex:1;width:100%;display:flex;align-items:flex-end;
  border-bottom:1px solid var(--line);overflow:hidden}
.mcol .mfill{width:100%;background:#2E6DA4;border-radius:8px 8px 0 0;min-height:3px}
.mcol .ml{font-size:12px;color:var(--mute);font-weight:600}
.mcol.now .mfill{background:var(--primary)}
.mcol.now .ml{color:var(--ink);font-weight:700}

/* ── ranked bars gain a trailing meta tag (win rate / repeat count) ── */
.bar{flex-wrap:wrap}
.bar .bm{flex:none;font-size:11.5px;font-weight:700;margin-inline-start:auto}
.wr{font-style:normal;padding:1px 7px;border-radius:99px;background:var(--line);color:var(--mute-ink)}
.wr.good{background:var(--good-soft);color:var(--good-ink)}
.wr.bad{background:var(--bad-soft);color:var(--bad-ink)}
.wr.none{background:transparent;color:var(--mute);font-weight:600}

@media (min-width:900px){
  /* two insight cards side by side once there is room; the tiles row
     becomes four across so the headline reads in one scan line */
  .insights{flex-direction:row;flex-wrap:wrap}
  .insights .ins{flex:1 1 320px}
  .tiles{grid-template-columns:repeat(4,1fr)}
  .mchart{height:230px}
}

/* ── toast ─────────────────────────────────────────────── */
.toast{position:fixed;left:50%;bottom:26px;transform:translate(-50%,20px);z-index:90;
  background:var(--ink);color:#fff;padding:13px 20px;border-radius:99px;font-size:15px;font-weight:600;
  opacity:0;pointer-events:none;transition:opacity .25s,transform .25s var(--ease);max-width:88vw;text-align:center}
.toast.show{opacity:1;transform:translate(-50%,0)}

/* ── sheet / modal ─────────────────────────────────────── */
.veil{position:fixed;inset:0;background:rgba(10,16,22,.5);z-index:80;opacity:0;pointer-events:none;transition:opacity .25s}
.veil.show{opacity:1;pointer-events:auto}
.sheet{position:fixed;left:0;right:0;bottom:0;z-index:85;background:var(--surface);
  border-radius:20px 20px 0 0;padding:20px 16px calc(20px + env(safe-area-inset-bottom));
  transform:translateY(102%);transition:transform .3s var(--ease);max-height:88vh;overflow-y:auto;
  max-width:520px;margin:0 auto}
.sheet.show{transform:none}
.sheet h3{font-size:18px;margin-bottom:14px}
.sheet .grip{width:40px;height:4px;border-radius:99px;background:var(--line-2);margin:0 auto 16px}
@media (min-width:900px){
  /* On desktop this is a real centred dialog, not a sheet pinned to the
     bottom edge — a bottom sheet only makes sense where a thumb needs to
     reach it. Rises + fades in from the middle of the viewport instead. */
  .sheet{max-width:520px;left:50%;top:50%;right:auto;bottom:auto;
    transform:translate(-50%,-45%);opacity:0;pointer-events:none;
    border-radius:20px;transition:transform .25s var(--ease),opacity .2s var(--ease)}
  .sheet.show{transform:translate(-50%,-50%);opacity:1;pointer-events:auto}
  .sheet .grip{display:none}
}

/* ── login ─────────────────────────────────────────────── */
/* flex:1/width:100% is load-bearing, not tidying. .login is a direct child
   of #app, and #app switches to flex-direction:row at 900px+ for the
   sidebar layout — so without a width the sign-in screen was sized to its
   own content and rendered as a ~285px strip down the left edge of any
   laptop, the rest of the viewport left empty. It is the only child of
   #app while signed out, so it should simply take the whole row. */
.login{min-height:100vh;min-height:100dvh;display:flex;flex-direction:column;justify-content:center;
  flex:1;width:100%;min-width:0;
  padding:32px 24px;background:var(--sidebar);color:#fff;text-align:center}
.login .mark{width:72px;height:72px;border-radius:18px;background:rgba(255,255,255,.1);
  display:grid;place-items:center;margin:0 auto 20px;font-size:24px;font-weight:700;overflow:hidden}
.login .mark img{width:100%;height:100%;object-fit:contain}
.login h1{font-size:24px;margin-bottom:6px}
.login .sub{color:rgba(255,255,255,.6);margin:0 0 26px;font-size:15px}
.dots{display:flex;gap:12px;justify-content:center;margin-bottom:24px}
.dots i{width:16px;height:16px;border-radius:50%;border:2px solid rgba(255,255,255,.35)}
.dots i.on{background:var(--accent);border-color:var(--accent)}
.keys{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;max-width:300px;margin:0 auto;width:100%}
.keys button{min-height:62px;border-radius:14px;background:rgba(255,255,255,.09);color:#fff;
  font-size:23px;font-weight:600;font-family:var(--num)}
.keys button:active{background:rgba(255,255,255,.2)}
.who-list{display:flex;flex-direction:column;gap:8px;max-width:320px;margin:0 auto 22px;width:100%}
.who-list button{min-height:52px;border-radius:12px;background:rgba(255,255,255,.09);color:#fff;
  font-size:16px;font-weight:600;padding:0 16px;display:flex;align-items:center;justify-content:space-between}
.who-list button.on{background:var(--accent);color:#1B1206}
.who-list .rl{font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;opacity:.7}
.err{color:#FFC9BE;font-size:14px;min-height:22px;margin-top:14px}

/* ═══════════════ mobile nav drawer + desktop sidebar ═══════════════
   One <aside class="side"> markup (built by ui.js sidebar()) serves both:
   below 900px it's a fixed off-canvas panel, closed by default, that
   slides in over a dimming backdrop; at 900px+ the desktop override further
   down turns it into the permanent static column it always was. Open state
   lives as a class on #app (outside the innerHTML render() replaces), set
   by UI.openNav()/closeNav() in ui.js — never baked into this markup. */
.side{display:flex;flex-direction:column;position:fixed;inset:0 auto 0 0;
  width:82vw;max-width:300px;height:100%;height:100dvh;background:var(--sidebar);
  color:#fff;padding:20px 14px;gap:4px;z-index:95;overflow-y:auto;
  transform:translateX(-105%);transition:transform .28s var(--ease);
  box-shadow:0 0 40px rgba(0,0,0,.3)}
[dir="rtl"] .side{inset:0 0 0 auto;transform:translateX(105%)}
#app.nav-open .side{transform:translateX(0)}
.side .brand{display:flex;align-items:center;gap:11px;padding:6px 8px 20px}
.side .brand .mk{width:42px;height:42px;border-radius:11px;background:rgba(255,255,255,.12);
  display:grid;place-items:center;font-weight:700;font-size:15px;overflow:hidden;flex:none}
.side .brand .mk img{width:100%;height:100%;object-fit:contain}
.side .brand>div{flex:1;min-width:0}
.side .brand b{display:block;font-size:15px;line-height:1.2}
.side .brand span{font-size:12px;color:rgba(255,255,255,.55)}
/* only meaningful on the mobile drawer — hidden on the desktop column
   further down, where the sidebar never closes. */
/* 44px, not 36 — it is a real tap target on a phone (the only way to
   close the drawer other than tapping the backdrop). */
.side .navclose{width:44px;height:44px;border-radius:11px;background:rgba(255,255,255,.1);
  color:#fff;display:grid;place-items:center;flex:none}
.side .navclose:active{background:rgba(255,255,255,.18)}
.side nav{display:flex;flex-direction:column;gap:3px;flex:1}
.side nav button{display:flex;align-items:center;gap:12px;min-height:48px;padding:0 12px;
  border-radius:10px;color:rgba(255,255,255,.72);font-size:15px;font-weight:600;text-align:start}
.side nav button:hover,.side nav button:active{background:rgba(255,255,255,.1);color:#fff}
.side nav button.on{background:var(--accent);color:#1B1206}
.side .foot{border-top:1px solid rgba(255,255,255,.12);padding-top:12px;
  font-size:12px;color:rgba(255,255,255,.45)}
/* the drawer's tap-outside-to-close backdrop — a no-op at 900px+, hidden
   there since the sidebar is permanent and there's nothing to dim under. */
.navveil{position:fixed;inset:0;background:rgba(10,16,22,.55);z-index:90;
  opacity:0;pointer-events:none;transition:opacity .2s}
#app.nav-open .navveil{opacity:1;pointer-events:auto}
/* the top bar's hamburger — hidden at 900px+ (below), where it would just
   duplicate the always-visible sidebar. */
.top .ic.navtoggle{display:grid}

/* ═══════════════ DESKTOP — real layout, not a wide phone ═══════════════ */
@media (min-width:900px){
  #app{max-width:none;flex-direction:row;align-items:stretch;box-shadow:none}
  .side{position:sticky;top:0;width:250px;flex:none;height:100vh;height:100dvh;
    max-width:none;transform:none;transition:none;box-shadow:none;z-index:auto}
  /* Restating this for RTL is not redundant. The off-canvas drawer rule
     `[dir="rtl"] .side{inset:0 0 0 auto;transform:translateX(105%)}` scores
     (0,2,0) against this block's plain `.side` at (0,1,0), so specificity
     beat the media query and the Arabic desktop sidebar kept its closed-
     drawer transform — it sat ~1453px off the right edge of a 1440px
     viewport. The hamburger is display:none at this width too, so an
     Arabic user on a laptop had no navigation on screen at all. */
  [dir="rtl"] .side{transform:none;inset:0 auto auto auto}
  .side .navclose{display:none}
  .navveil{display:none}
  .top .ic.navtoggle{display:none}
  .main{flex:1;min-width:0;display:flex;flex-direction:column;height:100vh;height:100dvh}
  .inner{max-width:1080px;width:100%;margin:0 auto}
  /* 6px of bottom padding and no divider put the first card's edge flush
     against the bar with nothing between them — and once the Pro layer
     gave the bar a near-white translucent fill, a white card met a white
     bar and the two read as one merged block. The bar needs to end
     somewhere: real breathing room, plus a hairline. */
  .top{position:static;background:transparent;border-bottom:1px solid var(--line);
    padding:22px 28px 16px}
  .top h1{font-size:24px}
  .top .ic.homeonly{display:none}
  .scroll{padding:18px 28px 28px}
  .pad{padding:0}
  .grid{grid-template-columns:repeat(4,minmax(0,1fr));padding:2px 0 0}
  .chips{padding:14px 0 12px}
  .dashed{margin:12px 0 0;width:100%}
  .tiles{grid-template-columns:repeat(4,1fr)}
  .footbar{position:sticky;bottom:0;border-radius:14px;border:1px solid var(--line);
    margin:14px 0 0;padding:14px 18px;box-shadow:var(--shadow)}
  .footbar .in{display:flex;align-items:center;gap:20px;max-width:1080px;margin:0 auto}
  .footbar .r{flex:1;margin-bottom:0}
  .footbar .btn{width:auto;min-width:230px}
  /* A footbar holding ONLY a primary button (the quotes list) left it
     stranded against the left edge of a 1080px bar with a screen's worth
     of empty space beside it. Centre it when it is the only thing there;
     the steps, which pair it with a money row, are unaffected. */
  .footbar .in:not(.solo) > .btn:only-child{margin-inline:auto}
  /* no money row (step 1) — nothing else to give the row width, so the
     button pair itself grows to span it, same as at every other width. */
  .footbar .in.solo .spread{flex:1}
  /* two-pane: form beside the live document */
  .split{display:grid;grid-template-columns:minmax(0,1fr) 420px;gap:26px;align-items:start}
  .split .prev{position:sticky;top:14px}
  /* settings two-pane */
  .twopane{display:grid;grid-template-columns:280px minmax(0,1fr);gap:24px;align-items:start}
  .login .keys button:hover{background:rgba(255,255,255,.16)}
}
@media (max-width:899px){
  .split,.twopane{display:block}
  .split .prev{margin-top:16px}
  .desk-only{display:none!important}
}
@media (min-width:900px){ .mob-only{display:none!important} }

/* ═══════════════ THE DOCUMENT — A4, built to impress ═══════════════ */
.doc{background:#fff;color:#131A22;border:1px solid var(--line);border-radius:12px;
  overflow:hidden;font-size:13px;line-height:1.5;box-shadow:0 10px 40px -18px rgba(19,26,34,.28)}

/* masthead: a deep band, the mark, and the document identity */
.doc .mast{background:var(--primary);color:#fff;padding:22px 26px 20px;position:relative;overflow:hidden}
.doc .mast::after{content:"";position:absolute;right:-70px;top:-70px;width:230px;height:230px;
  border-radius:50%;background:rgba(255,255,255,.05)}
.doc .mast .rowa{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;position:relative;z-index:1}
.doc .mark{display:flex;align-items:center;gap:12px;min-width:0}
.doc .mark .m{width:48px;height:48px;border-radius:12px;background:#fff;color:var(--primary);
  display:grid;place-items:center;font-size:17px;font-weight:700;flex:none;overflow:hidden}
.doc .mark .m img{width:100%;height:100%;object-fit:contain}
.doc .mark b{display:block;font-size:18px;font-weight:700;line-height:1.15;letter-spacing:-.01em}
.doc .mark span{display:block;font-size:11.5px;color:rgba(255,255,255,.66);margin-top:3px}
.doc .kind{text-align:end;flex:none}
.doc .kind .t{font-size:11px;font-weight:700;letter-spacing:.2em;text-transform:uppercase;
  color:var(--accent)}
.doc .kind .v{font-size:19px;font-weight:700;font-family:var(--num);margin-top:4px;letter-spacing:-.01em}
.doc .metastrip{display:flex;gap:26px;flex-wrap:wrap;margin-top:18px;padding-top:14px;
  border-top:1px solid rgba(255,255,255,.16);position:relative;z-index:1}
.doc .metastrip div{min-width:0}
.doc .metastrip .k{font-size:9.5px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;
  color:rgba(255,255,255,.5)}
.doc .metastrip .v{font-size:12.5px;font-weight:600;margin-top:3px;color:#fff}

.doc .in{padding:22px 26px 26px}

/* who it is for */
.doc .parties{display:flex;justify-content:space-between;gap:24px;padding:0 0 18px}
.doc .parties .k{font-size:9.5px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;
  color:var(--mute);margin-bottom:5px}
.doc .parties .nm{font-size:15px;font-weight:700;line-height:1.25}
.doc .parties em{font-style:normal;font-weight:400;color:var(--mute);display:block;font-size:12px;margin-top:2px}

/* the line-item table */
.doc table{width:100%;border-collapse:collapse}
.doc thead th{font-size:9.5px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;
  color:var(--primary);text-align:start;padding:9px 8px;background:#F1F4F7;
  border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.doc thead th:first-child{border-start-start-radius:7px}
.doc thead th:last-child{border-start-end-radius:7px}
.doc th.r,.doc td.r{text-align:end}
.doc tbody td{padding:11px 8px;border-bottom:1px solid var(--line);font-size:12.5px;vertical-align:top}
.doc tbody tr:last-child td{border-bottom:1px solid var(--line-2)}
.doc td .ln{font-weight:600;display:block;font-size:13px}
.doc td em{font-style:normal;color:var(--mute);font-size:11.5px;display:block;margin-top:2px;line-height:1.4}
.doc td.r{font-family:var(--num);font-weight:600;white-space:nowrap}
.doc td.amt{font-weight:700}

/* totals */
.doc .tailrow{display:flex;justify-content:space-between;gap:28px;margin-top:18px;align-items:flex-start}
.doc .sum{width:min(268px,58%);margin-inline-start:auto;flex:none}
.doc .sum .row{display:flex;justify-content:space-between;padding:6px 0;font-size:12.5px;color:var(--mute)}
.doc .sum .row b{color:#131A22;font-weight:600;font-family:var(--num)}
.doc .sum .rule{height:1px;background:var(--line);margin:5px 0}
.doc .sum .tot{display:flex;justify-content:space-between;align-items:baseline;margin-top:9px;
  padding:13px 15px;border-radius:10px;background:var(--primary);color:#fff}
.doc .sum .tot span{font-size:10px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;
  color:var(--accent)}
.doc .sum .tot b{font-size:19px;font-weight:700;font-family:var(--num);letter-spacing:-.01em}
.doc .inwords{font-size:11px;color:var(--mute);margin-top:8px;text-align:end;font-style:italic}

/* payment schedule */
.doc .pay{flex:1;min-width:0;border:1px solid var(--line);border-radius:9px;overflow:hidden}
.doc .pay .ph{background:#F1F4F7;padding:8px 12px;font-size:9.5px;font-weight:700;
  letter-spacing:.14em;text-transform:uppercase;color:var(--primary)}
.doc .pay .r{display:flex;justify-content:space-between;padding:9px 12px;font-size:12.5px;
  border-top:1px solid var(--line)}
.doc .pay .r b{font-family:var(--num);font-weight:600}

/* footer blocks */
.doc .notes{margin-top:18px;padding:12px 14px;border-radius:9px;background:#FFFBF3;
  border:1px solid #F0E2C8;font-size:12px;line-height:1.55}
.doc .notes b{display:block;font-size:9.5px;font-weight:700;letter-spacing:.14em;
  text-transform:uppercase;color:var(--warn);margin-bottom:4px}
.doc .ft{margin-top:20px;padding-top:16px;border-top:1px solid var(--line);
  display:flex;gap:26px;justify-content:space-between;flex-wrap:wrap}
.doc .ft div{font-size:11px;color:var(--mute);line-height:1.6;white-space:pre-line;min-width:0}
.doc .ft b{display:block;color:var(--primary);font-size:9.5px;font-weight:700;
  letter-spacing:.14em;text-transform:uppercase;margin-bottom:4px}
.doc .sign{margin-top:26px;display:flex;gap:40px;justify-content:space-between}
.doc .sign div{flex:1}
.doc .sign .line{border-top:1px solid #B9C2CB;margin-bottom:5px;padding-top:0}
.doc .sign span{font-size:10px;color:var(--mute);letter-spacing:.06em;text-transform:uppercase;font-weight:600}
.doc .thanks{margin-top:22px;text-align:center;font-size:12px;color:var(--mute);
  padding-top:14px;border-top:1px solid var(--line-2)}
.doc .thanks b{color:var(--primary);font-weight:700}

/* The Team screen at its login cap. Not package-scoped — Pro is the level
   that hits it, but the rule that produces it (S.canAddLogin) is general. */
.capnote{display:flex;gap:11px;align-items:flex-start;padding:14px;margin-bottom:12px;
  background:var(--warn-soft);border:1px solid #EFDCBA;border-radius:12px;color:var(--warn)}
.capnote svg{flex:none;margin-top:1px}
.capnote span{font-size:14px;line-height:1.5;color:var(--body)}
.capnote b{display:block;color:var(--ink);font-weight:700;margin-bottom:2px}

/* ═══════════════════════════════════════════════════════════════════════
   PRO PRESENTATION — scoped to [data-package="2"] (set by applyTheme()).

   Purely additive polish on the shared design: depth, gradient, and
   motion. It never moves an element, changes a tap target, or restyles a
   status colour — so the measured accessibility baseline (44px minimum
   targets, WCAG AA text, no status carried by colour alone) still holds
   without re-auditing, and Base/Enterprise are untouched by every rule
   here. Most of the lift comes from re-declaring three shared tokens, so
   components pick it up without being named individually.
   ═══════════════════════════════════════════════════════════════════════ */
[data-package="2"]{
  --line:#E7E3DC;
  --shadow:0 1px 2px rgba(19,26,34,.05), 0 6px 18px -6px rgba(19,26,34,.14);
  --shadow-l:0 -8px 30px -8px rgba(19,26,34,.16);
  --lift:0 2px 4px rgba(19,26,34,.05), 0 14px 34px -10px rgba(27,58,87,.24);
  --glass:rgba(255,255,255,.82);
}

/* a warm ground with a faint brand-tinted wash, so white cards read as
   sitting ON something rather than as holes in a flat field */
[data-package="2"] body,
[data-package="2"] #app{
  background:
    radial-gradient(1100px 520px at 78% -8%, rgba(224,145,47,.07), transparent 60%),
    radial-gradient(900px 480px at 4% 0%, rgba(27,58,87,.06), transparent 62%),
    var(--bg);
}

/* ── surfaces ─────────────────────────────────────────── */
[data-package="2"] .card,
[data-package="2"] .tile{
  transition:box-shadow .28s var(--ease), transform .28s var(--ease);
}
/* a 1px inner highlight along the top edge — the detail that separates a
   "box with a border" from a surface with a material to it. Done with an
   inset shadow, NOT a background-image layer: .qrow.qhead sets its own
   background colour, and an opaque gradient layer here would silently
   paint over it (the quotes-list table header would lose its grey band). */
[data-package="2"] .card,
[data-package="2"] .tile,
[data-package="2"] .qrow{
  box-shadow:inset 0 1px 0 rgba(255,255,255,.9), var(--shadow);
}
/* the header row is a label band, not a card — it never had a shadow */
[data-package="2"] .qrow.qhead{box-shadow:none}

/* ── top bar ──────────────────────────────────────────── */
[data-package="2"] .top{
  background:var(--glass);
  -webkit-backdrop-filter:saturate(180%) blur(14px);
  backdrop-filter:saturate(180%) blur(14px);
  border-bottom-color:rgba(19,26,34,.07);
}
/* Frosting needs a real fallback — without backdrop-filter the bar would
   render as flat 82%-opaque white with the page scrolling visibly through
   the text behind it. */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  [data-package="2"] .top{background:var(--surface)}
}
[data-package="2"] .top h1{letter-spacing:-.02em}
@media (min-width:900px){
  /* A softer line than the shared one, plus a 1px light edge under it, so
     the frosted bar ends on a lip rather than a drawn rule. */
  [data-package="2"] .top{border-bottom-color:rgba(19,26,34,.08);
    box-shadow:0 1px 0 rgba(255,255,255,.7)}
}

/* ── buttons ──────────────────────────────────────────── */
[data-package="2"] .btn{
  background-image:linear-gradient(180deg,rgba(255,255,255,.14),rgba(255,255,255,0));
  box-shadow:0 1px 2px rgba(19,26,34,.12), 0 8px 20px -8px rgba(27,58,87,.45);
  transition:transform .18s var(--ease), box-shadow .28s var(--ease), opacity .2s;
}
[data-package="2"] .btn:active{
  transform:translateY(1px) scale(.99);
  box-shadow:0 1px 2px rgba(19,26,34,.14);
}
[data-package="2"] .btn.ghost{
  background-image:none;
  box-shadow:0 1px 2px rgba(19,26,34,.04), 0 4px 12px -6px rgba(19,26,34,.14);
}
[data-package="2"] .btn.amber{
  box-shadow:0 1px 2px rgba(19,26,34,.12), 0 8px 20px -8px rgba(224,145,47,.55);
}

/* ── progress ─────────────────────────────────────────── */
[data-package="2"] .prog{background:transparent}
[data-package="2"] .prog i{transition:background .45s var(--ease), box-shadow .45s var(--ease)}
[data-package="2"] .prog i.on{
  background-image:linear-gradient(90deg,var(--primary),#2E6DA4);
  box-shadow:0 1px 6px -1px rgba(46,109,164,.6);
}

/* ── sidebar / drawer ─────────────────────────────────── */
[data-package="2"] .side{
  background-image:
    radial-gradient(520px 300px at 50% -12%, rgba(224,145,47,.16), transparent 68%),
    linear-gradient(180deg,rgba(255,255,255,.05),rgba(0,0,0,.18));
}
[data-package="2"] .side nav button{
  position:relative;
  transition:background .22s var(--ease), color .22s var(--ease);
}
/* The active item keeps its amber fill — that exact pairing is what the
   contrast audit measured — and gains only depth: a top-light gradient and
   a glow. An edge rail was tried here and removed: the amber pill already
   says "you are here" unambiguously, and a second amber mark on the same
   element is the same double-signalling this project already stripped out
   of the step-2 catalogue cards. One signal, made better. */
[data-package="2"] .side nav button.on{
  background-image:linear-gradient(180deg,rgba(255,255,255,.22),rgba(255,255,255,0));
  box-shadow:0 6px 18px -8px rgba(224,145,47,.75);
}
[data-package="2"] .side .brand .mk{
  background-image:linear-gradient(160deg,rgba(255,255,255,.22),rgba(255,255,255,.04));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.22);
}

/* ── home hero: the first thing a prospect sees in a demo ──
   Same markup and the same measurements as the shared card above; only
   the surface changes. Deep navy with the sidebar's own gradient so home
   and the nav read as one piece of software, a gold hairline where the
   numbers begin, and the figures in white on navy where they carry real
   weight. Text colours are set explicitly rather than inherited so
   nothing here depends on a token tuned for a light surface. */
[data-package="2"] .hero{
  border-color:transparent;
  background:
    radial-gradient(420px 220px at 88% -30%, rgba(224,145,47,.28), transparent 70%),
    linear-gradient(160deg,#1E4262 0%, var(--sidebar) 78%);
  box-shadow:0 2px 4px rgba(19,26,34,.10), 0 18px 40px -18px rgba(18,33,47,.6);
}
/* background-color, not just background-image. The base .hero .mk sets an
   opaque light --bg; overriding only the image left white initials sitting
   on that light tile at 1.09:1 — invisible. Caught by the contrast audit
   only after it was taught to read gradients, which is the whole reason
   that fix was worth making. */
[data-package="2"] .hero .mk{
  background-color:rgba(255,255,255,.14);
  background-image:linear-gradient(160deg,rgba(255,255,255,.20),rgba(255,255,255,.04));
  color:#fff;box-shadow:inset 0 1px 0 rgba(255,255,255,.22)}
[data-package="2"] .hgreet .hi{color:rgba(255,255,255,.66)}
[data-package="2"] .hgreet b{color:#fff}
[data-package="2"] .hero .hout{
  background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.22);color:#fff}
[data-package="2"] .hstats{
  background:rgba(0,0,0,.16);border-top:1px solid rgba(224,145,47,.55)}
[data-package="2"] .hstat{border-inline-start-color:rgba(255,255,255,.14)}
[data-package="2"] .hstat span{color:rgba(255,255,255,.6)}
[data-package="2"] .hstat b{color:#fff;font-size:18px}
/* the two states that mean something keep meaning it, lightened for navy */
[data-package="2"] .hstat.warn b{color:#F0B460}
[data-package="2"] .hstat.good b{color:#7FD3AA}

/* ── dashboard ────────────────────────────────────────── */
/* the four headline tiles are the reason Pro exists — give them the
   strongest treatment on the screen, and let the number carry it */
[data-package="2"] .tile{position:relative;overflow:hidden;padding-top:17px}
[data-package="2"] .tile::before{
  content:"";position:absolute;inset-inline-start:0;top:0;height:3px;width:100%;
  background:linear-gradient(90deg,var(--primary),#2E6DA4 70%,transparent);
  opacity:.9;
}
[data-package="2"] .tile.good::before{background:linear-gradient(90deg,var(--good),#5BB98C 70%,transparent)}
[data-package="2"] .tile .v{font-size:28px;letter-spacing:-.03em}
[data-package="2"] .tile .k{color:var(--body);opacity:.75}
[data-package="2"] .bar .bt{
  background-image:linear-gradient(90deg,#2E6DA4,#4E93CC);
  box-shadow:0 1px 4px -1px rgba(46,109,164,.5);
  transition:width .5s var(--ease);
}

/* ── login — the first screen a Pro client ever sees ──── */
[data-package="2"] .login{
  background-image:
    radial-gradient(760px 460px at 50% -6%, rgba(224,145,47,.20), transparent 66%),
    radial-gradient(620px 520px at 12% 108%, rgba(46,109,164,.26), transparent 62%);
}
[data-package="2"] .login .mark{
  background-image:linear-gradient(160deg,rgba(255,255,255,.24),rgba(255,255,255,.05));
  box-shadow:0 18px 40px -14px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.28);
}
[data-package="2"] .login h1{letter-spacing:-.025em}
[data-package="2"] .keys button,
[data-package="2"] .who-list button{
  background-image:linear-gradient(180deg,rgba(255,255,255,.10),rgba(255,255,255,.02));
  box-shadow:inset 0 1px 0 rgba(255,255,255,.14);
  transition:background .18s var(--ease), transform .12s var(--ease);
}
[data-package="2"] .keys button:active{transform:scale(.95)}
[data-package="2"] .who-list button.on{
  background-image:linear-gradient(180deg,rgba(255,255,255,.28),rgba(255,255,255,0));
  box-shadow:0 10px 26px -10px rgba(224,145,47,.8);
}
[data-package="2"] .dots i{transition:background .2s var(--ease), box-shadow .2s var(--ease)}
[data-package="2"] .dots i.on{box-shadow:0 0 0 4px rgba(224,145,47,.22)}

/* ── sheets & rows ────────────────────────────────────── */
[data-package="2"] .sheet{box-shadow:0 -20px 60px -20px rgba(10,16,22,.5)}
[data-package="2"] .qrow{transition:box-shadow .24s var(--ease), transform .24s var(--ease)}

/* Hover lift is desktop-only on purpose: on a touch screen :hover sticks
   after a tap, so a card stays visibly raised long after the finger left. */
@media (hover:hover) and (min-width:900px){
  [data-package="2"] .card:hover,
  [data-package="2"] .qrow:hover{transform:translateY(-2px);box-shadow:var(--lift)}
  [data-package="2"] .tile:hover{transform:translateY(-2px);box-shadow:var(--lift)}
  [data-package="2"] .btn:hover{box-shadow:0 2px 4px rgba(19,26,34,.12), 0 14px 30px -10px rgba(27,58,87,.5)}
  [data-package="2"] .side nav button:not(.on):hover{background:rgba(255,255,255,.13)}
}

@media (prefers-reduced-motion:reduce){
  [data-package="2"] *,
  [data-package="2"] *::before,
  [data-package="2"] *::after{transition-duration:.01ms!important}
  [data-package="2"] .card:hover,
  [data-package="2"] .qrow:hover,
  [data-package="2"] .tile:hover{transform:none}
}

/* PRINT — real A4. Also what the browser's own "Save as PDF" uses. */
@page{size:A4;margin:11mm}
@media print{
  html,body{background:#fff!important}
  body{font-size:11pt}
  .top,.footbar,.chips,.side,.noprint,.toast,.veil,.sheet,.prog{display:none!important}
  #app,.main,.scroll,.inner,.pad{display:block!important;max-width:none!important;height:auto!important;
    overflow:visible!important;padding:0!important;margin:0!important;box-shadow:none!important;
    background:#fff!important}
  .split{display:block!important}
  .split .form{display:none!important}
  .split .prev{position:static!important;margin:0!important}
  .doc{border:0!important;border-radius:0!important;box-shadow:none!important;font-size:10.5pt}
  .doc .mast{-webkit-print-color-adjust:exact;print-color-adjust:exact}
  .doc thead th,.doc .sum .tot,.doc .pay .ph,.doc .notes{
    -webkit-print-color-adjust:exact;print-color-adjust:exact}
  .doc .in{padding:18px 0 0!important}
  .doc table{page-break-inside:auto}
  .doc tr{page-break-inside:avoid}
  .doc thead{display:table-header-group}
  .doc .sum,.doc .pay,.doc .ft,.doc .notes,.doc .sign,.doc .tailrow{page-break-inside:avoid}
}

/* ══ v2 additions ══════════════════════════════════════ */

/* quantity control — white buttons so the − and + are obvious */
.qty{background:var(--primary);border-radius:10px;padding:3px;gap:3px}
.qty button{width:30px;height:30px;border-radius:8px;background:#fff;color:var(--primary)}
.qty button:active{background:var(--accent);color:#fff}
.qty b{min-width:22px;color:#fff;font-size:15px}

/* an explicit ADD badge so nobody wonders how to choose a service */
.addbadge{display:inline-flex;align-items:center;gap:4px;padding:5px 10px;border-radius:99px;
  background:var(--bg);border:1.5px solid var(--line-2);color:var(--mute);
  font-size:12px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;flex:none}
.item.on .addbadge{display:none}
.item:active .addbadge{background:var(--primary);border-color:var(--primary);color:#fff}

/* The "tap a card" line is plain text now, deliberately NOT styled like a
   button — it used to sit inside a full-width coloured pill, and people
   were tapping the pill itself expecting something to happen (it did
   nothing; only the small square inside it was live). Only the "Add
   service" button on the right should read as tappable now. It's also
   reachable here without scrolling to the bottom of the grid (the other
   place it lives, .dashed). */
/* The hint has this row to itself now — the "Add service" button that used
   to sit at its right end was a duplicate of the dashed button under the
   grid (see V.step2) and has gone. With nothing to share the row with, the
   hint no longer needs to be squeezed onto one line and truncated with an
   ellipsis, which is what it was doing on a phone. It wraps instead. */
.tapbar{display:flex;align-items:center;gap:10px;margin:0 16px 10px}
.tapbar .hint{min-width:0;font-size:12.5px;font-weight:600;color:var(--mute);line-height:1.45}
.tapbar .hint i{font-style:normal}
@media (min-width:900px){ .tapbar{margin-inline:0} }

/* help "?" button in the top bar */
.top .ic[data-help]{border-color:var(--line-2);color:var(--mute)}
.top .ic[data-help]:active{background:var(--bg);color:var(--primary)}
/* the "+" here is the same icon as the big "Make a new quote" button
   already visible lower on this same screen, and solid-filled (unlike the
   plain outlined help circle) so it reads as an action, not information. */
.top .ic.newq{background:var(--primary);border-color:var(--primary);color:#fff}
.top .ic.newq:active{background:var(--accent);border-color:var(--accent)}

/* price list: category rail (the services left-bar) */
.rail{background:var(--surface);border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
.rail .railhd{padding:11px 14px;font-size:11px;font-weight:700;letter-spacing:.12em;
  text-transform:uppercase;color:var(--mute);background:var(--bg);border-bottom:1px solid var(--line)}
.rail button{display:flex;align-items:center;gap:10px;width:100%;padding:12px 14px;text-align:start;
  border-bottom:1px solid var(--line);min-height:48px;font-size:14.5px;font-weight:600;color:var(--body)}
.rail button:last-child{border-bottom:0}
.rail button.on{background:var(--primary);color:#fff}
.rail button u{width:9px;height:9px;border-radius:9px;text-decoration:none;flex:none}
.rail button span{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rail button .cnt{font-style:normal;font-size:12.5px;color:var(--faint);font-weight:700}
.rail button.on .cnt{color:rgba(255,255,255,.7)}

/* select-to-raise */
/* The box stays 26px because a 44px checkbox beside a 15px service name
   reads as the loudest thing on the row, which it is not. The TARGET is
   44px instead, added as a transparent ::before overlay — visual size and
   hit size are separate concerns, and only the second one is the
   accessibility requirement. */
.lrow .tick{position:relative;width:26px;height:26px;border-radius:7px;border:1.8px solid var(--line-2);
  background:var(--surface);display:grid;place-items:center;flex:none;color:#fff}
.lrow .tick::before{content:"";position:absolute;top:50%;left:50%;
  transform:translate(-50%,-50%);width:44px;height:44px}
.lrow .tick.on{background:var(--primary);border-color:var(--primary)}
.selbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:11px 13px;margin-bottom:10px;
  border-radius:11px;background:#E8EEF4;border:1px solid #CFDDE9}
.selbar span{flex:1;min-width:90px;font-size:14px;font-weight:700;color:var(--primary)}

/* two-pane also applies to the price list on desktop, where the rail is a
   full vertical list sitting in its own sticky 280px column (see .twopane
   above 900px) — plenty of room to spare there. Stacked below 900px it
   used to render the exact same way inline, ahead of the search box and
   the actual price rows: 5 full-width category buttons is 300+px a person
   had to scroll past before reaching a single item they could act on.
   Every category is still shown — nothing hidden behind a scroll or a
   swipe, unlike a horizontal strip, which matters for someone who might
   not think to try swiping a row that doesn't visibly overflow — just
   wrapped into a compact chip cluster (same shape as the step-2 category
   chips elsewhere in this app) instead of one full-width row each. */
@media (max-width:899px){
  .rail{display:flex;flex-wrap:wrap;gap:8px;background:none;border:0;padding:0;margin-bottom:12px}
  .rail .railhd{display:none}
  .rail button{width:auto;flex:0 0 auto;min-height:40px;border-radius:999px;
    border:1.5px solid var(--line-2);background:var(--surface);padding:0 14px}
  .rail button.on{border-color:var(--primary)}
  .rail button span{overflow:visible;text-overflow:clip}
}

/* ══ team ══════════════════════════════════════════════ */
.avatar{width:40px;height:40px;border-radius:11px;background:var(--primary);color:#fff;
  display:grid;place-items:center;font-weight:700;font-size:16px;flex:none}
.lrow.off .avatar{background:var(--faint)}
.rolechip{display:inline-block;padding:1px 7px;border-radius:99px;font-size:10.5px;font-weight:700;
  letter-spacing:.06em;text-transform:uppercase;vertical-align:middle;margin-inline-start:4px}
.rolechip.owner{background:var(--warn-soft);color:var(--warn-ink)}
.rolechip.staff{background:#E8EEF4;color:var(--primary)}

/* ══ dashboard ═════════════════════════════════════════ */
/* `.tile .s .up` / `.down` used to live here, from the old dashboard's
   bare <span class="up"> delta markup. The delta is now a .dlt chip that
   sets its own AA-checked ink — but those rules still MATCHED it (the chip
   is a .up inside .tile .s) and, at (0,3,0) against .dlt.up's (0,2,0), won
   on specificity and silently reverted the colour. That dropped the chip
   to 4.27:1 and 4.44:1 against its own tint, under the 4.5 floor, with
   nothing in the markup or the .dlt rule to suggest why. Deleted rather
   than re-scoped: the markup they were written for no longer exists.
   .tile.good .v is kept but re-pointed at the darker ink for the same
   reason — it is the tile's big number on a white surface. */
.tile.good .v{color:var(--good-ink)}
.chasehd{font-size:13px;color:var(--body);padding-bottom:10px;margin-bottom:10px;
  border-bottom:1px solid var(--line)}
.chasehd b{color:var(--primary);font-weight:700}
.pipe{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
@media (min-width:640px){ .pipe{grid-template-columns:repeat(4,1fr)} }
.pstage{padding:12px;border-radius:11px;background:var(--bg);border:1px solid var(--line)}
.pstage .pill{margin:0 0 7px}
.pstage b{display:block;font-size:22px;font-weight:700;letter-spacing:-.02em}
.pstage .pv{display:block;font-size:12.5px;color:var(--mute);margin-top:2px}
.wonline{display:flex;justify-content:space-between;align-items:center;padding:11px 0;
  border-top:1px solid var(--line);margin-top:12px;font-size:14px;color:var(--mute)}
.wonline:first-child{margin-top:0;border-top:none}
.tile .v .cu{font-size:.62em;font-weight:600;color:var(--mute);margin:0 .22em}  /* symmetric: bidi decides which side the symbol lands on in AR */
.wonline b{color:var(--ink);font-weight:700;font-size:15px}
.wonline b.warn{color:var(--accent)}
.bar{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.bar .bn{width:34%;font-size:13.5px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bar .btrack{flex:1;height:10px;border-radius:99px;background:var(--bg);overflow:hidden;min-width:0}
.bar .bt{display:block;height:100%;border-radius:99px;background:#2E6DA4}
.bar .bv{font-size:12.5px;font-weight:600;color:var(--mute);flex:none;font-family:var(--num);
  min-width:78px;text-align:end}

/* ═══════════════════════════════════════════════════════════
   CAPACITY — Compass & Crate's own screen. Two trucks, a week at a
   time. Reuses .card/.lrow/.pill/.empty/.sect from the shared design
   system throughout; only the grid itself and its cells are new.
   ═══════════════════════════════════════════════════════════ */
.pill.bad{background:var(--bad-soft);color:var(--bad-ink)}
.pill.sm{font-size:9.5px;padding:1px 6px;margin-top:3px;display:block;width:fit-content}

.wkbtn{width:36px;height:36px;border-radius:10px;border:1.5px solid var(--line-2);
  background:var(--surface);color:var(--body);display:grid;place-items:center;flex:none}
.wkbtn:active{background:var(--bg)}
/* same mirroring [dir="rtl"] .top .ic svg already gets — a directional
   chevron has to point the way it means, not just sit on the flipped side */
[dir="rtl"] .wkbtn svg{transform:scaleX(-1)}

.cal{display:grid;grid-template-columns:64px repeat(7,minmax(84px,1fr));gap:6px;margin-top:10px}
.cal .hd{font-size:10.5px;font-weight:700;letter-spacing:.02em;color:var(--mute);
  text-align:center;padding:4px 2px}
.cal .hd.today-col{color:var(--primary)}
.cal .lane-lbl{display:flex;align-items:center;font-size:12px;font-weight:700;color:var(--body);
  padding:4px 2px}
.cal .slot{background:var(--surface);border:1px solid var(--line);border-radius:9px;
  min-height:56px;padding:5px;display:flex;flex-direction:column;gap:4px}
.cal .slot.open{align-items:center;justify-content:center}
.cal .slot.open span{font-size:11px;color:var(--faint)}
.cal .job{display:flex;flex-direction:column;align-items:flex-start;gap:1px;
  border-radius:7px;padding:5px 7px;background:var(--good-soft);border:1px solid transparent;
  width:100%;text-align:start}
.cal .job .id{font-family:var(--num);font-size:9px;color:var(--good-ink);opacity:.85}
.cal .job b{font-size:11.5px;color:var(--ink);font-weight:600;line-height:1.25}

@media (max-width:760px){
  /* Horizontal scroll only — NOT grid-auto-flow:column. This grid's
     cells are all implicitly placed (no explicit grid-row/-column on
     any of them), so switching the auto-placement axis to "column"
     re-flows every cell into the wrong slot instead of just narrowing
     them — the header row survives (it's genuinely first) but every
     truck row underneath silently disappears, because auto-flow packs
     down the first two explicit columns before ever reaching a third.
     Fixed pixel columns + overflow-x is the only change mobile needs;
     row-major placement (the default) must stay untouched. */
  .cal{overflow-x:auto;padding-bottom:6px;grid-template-columns:56px repeat(7,84px)}
}
