/* apps/web/shared.css — base design tokens + component styles shared by index.html (the
 * tenant app) and platform-admin.html (the Platform Admin / SaaS Management console).
 * Deliberately CSS-only sharing, not JS: the two pages are separate surfaces on purpose
 * (platform:* vs tenant module:action authz — see packages/platform-authz/platform.ts),
 * so their logic stays independently duplicated rather than risk a shared-module bug
 * crossing that boundary. Visual consistency (same brand/tokens) is fine to share since
 * it carries no security weight. */
:root{
  --font-body:"Be Vietnam Pro",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  --font-heading:"Rubik",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
  --brand:#6c53e5; --brand-hover:#5a3fd1; --brand-subtle:#f1edfe; --accent:#f59e0b;
  --bg-canvas:#f4f7f8; --bg-surface:#fff; --bg-subtle:#eef2f4; --bg-hover:#eef2f4;
  --border:#e2e8f0; --border-strong:#cbd5e1;
  --text:#0f172a; --text2:#475569; --muted:#57667a; --on-brand:#fff;
  --ok:#16a34a; --ok-bg:#f0fdf4; --warn:#d97706; --warn-bg:#fffbeb; --danger:#dc2626; --danger-bg:#fef2f2; --info:#2563eb; --info-bg:#eff6ff;
  --e1:0 1px 2px rgb(2 6 23/.06),0 0 0 1px rgb(2 6 23/.04); --e2:0 4px 14px rgb(2 6 23/.08),0 0 0 1px rgb(2 6 23/.04);
  --r:12px; --r2:8px; --r3:10px;
  /* Type scale. Added 2026-08-03 during the Platform Admin premium pass, which measured 17
     distinct font-sizes across this file — including half-pixel one-offs (11.5/12.5/13.5/14.5)
     that are the clearest signal of a scale guessed rather than designed.
     These tokens are ADDITIVE: nothing already in this file was changed, so the tenant app
     renders pixel-identical. platform-admin.html adopts them; the tenant app is a separate,
     larger migration that should be done deliberately rather than as a side effect. */
  --fs-display:26px;  /* KPI figures, the one number per card that matters */
  --fs-lg:20px;       /* section headings */
  --fs-md:15px;       /* card titles */
  --fs-base:13px;     /* body, table cells, buttons */
  --fs-sm:12px;       /* secondary text, meta, labels */
}
:root[data-theme="dark"]{
  --brand:#a78bfa; --brand-hover:#c4b5fd; --brand-subtle:#241c42; --accent:#fbbf24;
  --bg-canvas:#020617; --bg-surface:#0f172a; --bg-subtle:#1e293b; --bg-hover:#1e293b;
  --border:#1e293b; --border-strong:#334155;
  --text:#f1f5f9; --text2:#94a3b8; --muted:#7c8ca2; --on-brand:#1e1533;
  --ok:#4ade80; --ok-bg:#0c2417; --warn:#fbbf24; --warn-bg:#2a1e08; --danger:#f87171; --danger-bg:#2a1113; --info:#60a5fa; --info-bg:#0c1e3a;
  --e1:0 1px 2px rgb(0 0 0/.4),0 0 0 1px rgb(255 255 255/.04); --e2:0 6px 18px rgb(0 0 0/.5),0 0 0 1px rgb(255 255 255/.05);
}
*{box-sizing:border-box}
body{margin:0;font-family:var(--font-body);background:var(--bg-canvas);color:var(--text);font-size:15px;-webkit-font-smoothing:antialiased;min-height:100vh;display:flex;flex-direction:column}
h1,.bname,.ptitle,.btn,.tabs button,.coln,.amt,.kpi .v,.lt{font-family:var(--font-heading)}
/* Sakkal Majalla ships with Windows/Office, not macOS/Linux/Android — referenced by name
   only (no @font-face/hosted file) so this never redistributes a licensed font we don't
   have rights to serve. Browsers resolve a bare font-family name against fonts already
   installed locally; if it's not there, they silently fall through to the next name in
   the list. Practical effect: Windows users with Office get Sakkal Majalla, everyone else
   gets the exact same rendering as before this change. */
html[lang="ar"] body{font-family:"Sakkal Majalla","SF Arabic","Segoe UI","Geeza Pro",Tahoma,sans-serif}
html[lang="ar"] h1,html[lang="ar"] .bname,html[lang="ar"] .ptitle,html[lang="ar"] .btn,html[lang="ar"] .tabs button,html[lang="ar"] .coln,html[lang="ar"] .amt,html[lang="ar"] .kpi .v,html[lang="ar"] .lt{font-family:"Sakkal Majalla","SF Arabic","Segoe UI","Geeza Pro",Tahoma,sans-serif}
/* Arabic KPI values ("19.6 مليون ر.س.") run visibly longer than the English equivalent
   ("SAR 19.6M") at the same character count — confirmed overflowing (and silently
   clipped by .kpi's overflow:hidden) at the narrow per-tile widths a wide viewport with
   many KPI tiles in one row produces. Smaller size + explicit wrap, not just relying on
   the default, so a tile can never silently eat a real number. */
html[lang="ar"] .kpi .v{font-size:19px;letter-spacing:0}
.kpi .v{overflow-wrap:break-word;word-break:normal}
/* The browser's own `[hidden]{display:none}` lives in the UA stylesheet, so ANY author rule
   that sets `display` beats it — measured live: a `hidden` element still computed to flex on
   .fld/.who and to grid on .ib/.alerts-badge. That silently defeated el.hidden = true in four
   places (the platform-admin shield link for non-admins, the zero-count alerts badge, the
   console's avatar chip on its login screen, the announcement audience picker on "all"). The
   !important is the point: `hidden` must outrank layout classes, never lose to them. */
[hidden]{display:none!important}
:focus-visible{outline:2px solid var(--brand);outline-offset:2px;border-radius:6px}
.tnum{font-variant-numeric:tabular-nums}
button{font:inherit;cursor:pointer}
/* topbar */
/* .top is the full-bleed chrome (background/border span the whole viewport, as a header
   bar should); .top-inner carries the same max-width+centering as .wrap below it, so the
   logo/tabs/actions line up with the widget grid instead of hugging the true viewport
   edges while the content column stays centered narrower on wide screens. */
.top{background:var(--bg-surface);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:20}
.top-inner{max-width:1180px;margin:0 auto;padding:10px 18px}
.top-row{height:40px;display:flex;align-items:center;gap:14px}
.brand{display:flex;align-items:center;gap:10px;flex-shrink:0}
.mark{width:32px;height:32px;display:grid;place-items:center;flex-shrink:0}
.mark img{width:100%;height:100%;object-fit:contain}
.bname{font-weight:700;font-size:15px;white-space:nowrap}.bsub{font-size:11px;color:var(--muted);margin-top:-2px;white-space:nowrap}
/* Tabs are their own fixed, always-visible row (not a shared row with brand/actions) that
   WRAPS onto additional lines rather than scrolling — a 10-module-wide bar (pipeline_approvals
   included) doesn't reliably fit one line at every width/locale (Arabic labels run longer),
   and a horizontally-sliding strip read as broken/unstable rather than "more below the fold." */
.tabs{display:flex;flex-wrap:wrap;justify-content:space-between;background:var(--bg-subtle);border-radius:var(--r3);padding:3px;gap:2px;margin-top:8px}
.tabs button{border:0;background:transparent;color:var(--text2);font-size:13px;font-weight:600;padding:7px 14px;border-radius:var(--r2);flex-shrink:0}
.tabs button.on{background:var(--bg-surface);color:var(--brand);box-shadow:var(--e1)}
.sp{flex:1}
.ib{width:36px;height:36px;border-radius:var(--r2);border:1px solid var(--border);background:var(--bg-surface);color:var(--text2);display:grid;place-items:center}
.ib:hover{background:var(--bg-hover);color:var(--text)}
/* The operator identity block. The tenant app dropped it — its header carries the company, not
   the person — but the platform console keeps it: knowing WHICH operator you are signed in as
   is the point when you are about to suspend somebody's tenant. */
.who{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text2);cursor:pointer}
.ava{width:34px;height:34px;border-radius:50%;background:linear-gradient(135deg,#5a3fd1,#2e2170);color:#fff;display:grid;place-items:center;font-size:13px;font-weight:600}
.alerts-wrap{position:relative}
.alerts-badge{position:absolute;top:-4px;inset-inline-end:-4px;min-width:16px;height:16px;padding:0 3px;border-radius:8px;background:var(--danger);color:#fff;font-size:10px;font-weight:700;display:grid;place-items:center;line-height:1}
.alerts-panel{position:absolute;top:calc(100% + 8px);inset-inline-end:0;width:340px;max-height:420px;overflow-y:auto;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--r2);box-shadow:var(--e2);z-index:30}
.alerts-panel .ph2{padding:10px 14px;font-weight:700;font-size:13px;border-bottom:1px solid var(--border)}
.alerts-group-label{padding:8px 14px 2px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}
.alert-row{display:flex;align-items:baseline;gap:8px;padding:7px 14px;cursor:pointer;font-size:13px}
.alert-row:hover{background:var(--bg-hover)}
.alert-row .at{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.alert-row .ad{font-size:11px;color:var(--muted);flex-shrink:0}
.alerts-empty{padding:20px 14px;text-align:center;color:var(--muted);font-size:13px}
/* layout */
.wrap{max-width:1180px;margin:0 auto;padding:24px 22px 60px;width:100%;box-sizing:border-box;flex:1;display:flex;flex-direction:column}
.app-footer{margin-top:auto;padding-top:14px;border-top:1px solid var(--border);text-align:center;font-size:11.5px;color:var(--muted)}
.auth-wrap .app-footer{margin-top:0;padding-top:0;border-top:0}
.head{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:18px}
h1{font-size:22px;margin:0}.sub{color:var(--muted);font-size:13px;margin-top:3px}
.head-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.date-range{display:flex;align-items:center;gap:6px}
.date-range select,.date-range input[type=date]{border:1px solid var(--border-strong);background:var(--bg-surface);color:var(--text);border-radius:var(--r2);padding:7px 10px;font:inherit;font-size:12.5px;height:34px}
.date-range .dr-sep{color:var(--muted);font-size:12.5px}
.card{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--r);box-shadow:var(--e1)}
/* kpis */
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:18px}
/* container-type makes the tile a query container, so the value can be sized against the TILE
   rather than the viewport — which is what "fit the widget dimensions" actually requires.
   flex-column + margin-top:auto on the caption pins every caption to the bottom, so a value
   that takes two lines in one tile no longer shoves that tile's caption out of line with its
   neighbours'. */
.kpi{padding:15px 16px;container-type:inline-size;display:flex;flex-direction:column;min-height:118px}
.kpi .l{font-size:12.5px;color:var(--text2)}
/* 25px used to be the base size, but .kpirow's auto-fit(minmax(150px,1fr)) squeezes every
   tile down to right around that 150px floor once 6-7 KPIs share one row at the .wrap
   max-width (1180px) — "SAR 19.6M" at 25px bold didn't fit, and .v's own overflow-wrap
   fallback (below) broke it mid-token ("SAR 19.6" / "M") rather than after "SAR". Sized
   down so real compact-currency values clear the tile with margin instead of relying on
   that fallback to save it. */
/* Arabic money is far longer than English — "20.9 مليون ر.س." is 17 characters where
   "SAR 20.9M" is 9 — so a fixed 20px wrapped onto two lines and broke the row's alignment.
   clamp() against cqi scales the number to whatever width the tile actually has, with a floor
   that stays readable and a ceiling that stops it ballooning in a wide tile. */
.kpi .v{font-size:clamp(15px,8.5cqi,21px);font-weight:700;letter-spacing:-.3px;margin-top:6px;line-height:1.2}
/* The currency unit must not be orphaned onto its own line — "20.9 مليون" / "ر.س." reads as
   two different facts. */
.kpi .v{text-wrap:balance}
.kpi .n{margin-top:auto}
.kpi .n{font-size:11.5px;color:var(--muted);padding-top:6px}
.kpirow{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:13px;margin-bottom:16px}
.kpi{position:relative;overflow:hidden}
.kpi::before{content:"";position:absolute;inset-block-start:0;inset-inline-start:0;width:100%;height:3px;background:var(--brand);opacity:.9}
.kpi.k-amber::before{background:var(--accent)} .kpi.k-ok::before{background:var(--ok)} .kpi.k-info::before{background:var(--info)}
.cardgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(380px,1fr));gap:16px}
.stack{display:flex;flex-direction:column;gap:16px}
.two{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.widget-hidden{opacity:.4}
.widget-toggle{border:1px solid var(--border);background:var(--bg-surface);color:var(--text2);width:24px;height:24px;border-radius:var(--r2);font-size:12px;line-height:1;cursor:pointer;flex:none}
.kpi .widget-toggle{position:absolute;top:8px;inset-inline-end:8px}
[draggable="true"]{cursor:grab}
.card.dragging{opacity:.4}
/* funnel */
.funnel{display:flex;flex-direction:column;gap:9px}
.frow{display:flex;align-items:center;gap:12px}
.fname{width:118px;flex:0 0 auto;font-size:12.5px;color:var(--text2);font-weight:500}
.ftrack{position:relative;flex:1;height:28px;background:var(--bg-subtle);border-radius:var(--r2);overflow:hidden}
.fbar{height:100%;border-radius:var(--r2);background:linear-gradient(90deg,var(--brand),#8b7cf6);display:flex;align-items:center;padding-inline:9px;color:#fff;font-size:11.5px;font-weight:700;white-space:nowrap;transition:width .5s cubic-bezier(.2,0,0,1)}
.fbar.win{background:linear-gradient(90deg,var(--ok),#22c55e)}
.fbar.lost{background:linear-gradient(90deg,var(--danger),#f87171)}
/* An empty stage draws NO bar. The old min-width:24px painted a coloured stub for a stage
   holding nothing, which reads as "a little" when the honest answer is "none". The count
   moves onto the track instead, so a zero row is still labelled. */
.fzero{position:absolute;inset-block:0;inset-inline-start:9px;display:flex;align-items:center;font-size:11.5px;font-weight:700;color:var(--muted)}
:root[data-theme="dark"] .fbar{color:#1e1533}
[dir="rtl"] .fbar{background:linear-gradient(270deg,var(--brand),#8b7cf6)} [dir="rtl"] .fbar.win{background:linear-gradient(270deg,var(--ok),#22c55e)}
[dir="rtl"] .fbar.lost{background:linear-gradient(270deg,var(--danger),#f87171)}
.fval{width:96px;flex:0 0 auto;text-align:end;font-size:12.5px;font-weight:600}
/* Stage-to-stage drop-off. A funnel that does not show where deals LEAVE is just a bar
   chart with a funnel's name.
   Laid out as a funnel row rather than nudged with a margin: an offset hardcoded to the label
   column's width silently drifts the moment that column changes, and it did not survive RTL. */
.fdrop{display:flex;align-items:center;gap:12px;font-size:11px;color:var(--muted);line-height:1.35;min-width:0}
.fdrop .fdspacer{width:118px;flex:0 0 auto}
.fdrop .fdline{flex:0 0 14px;height:1px;background:var(--border-strong)}
/* source bars */
.srcrow{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.srcname{width:96px;flex:0 0 auto;font-size:12.5px;color:var(--text2)}
.srctrack{flex:1;height:20px;background:var(--bg-subtle);border-radius:6px;overflow:hidden}
.srcfill{height:100%;background:linear-gradient(90deg,var(--brand),#8b7cf6);border-radius:6px;transition:width .5s cubic-bezier(.2,0,0,1)}
[dir="rtl"] .srcfill{background:linear-gradient(270deg,var(--brand),#8b7cf6)}
.srcmeta{width:138px;flex:0 0 auto;text-align:end;font-size:12px;color:var(--text2);display:flex;gap:6px;align-items:center;justify-content:flex-end}
/* Share of total. The raw count alone cannot answer "is this channel actually carrying us?" */
.srcpct{font-size:11px;color:var(--muted);min-width:34px;text-align:end}
/* task health */
.sbar{display:flex;height:30px;border-radius:var(--r2);overflow:hidden;background:var(--bg-subtle)}
.sbar>div{display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#fff;white-space:nowrap;transition:width .5s cubic-bezier(.2,0,0,1)}
/* `to do` was painted var(--border-strong) — 1.32:1 against the track it sits on, measured.
   A board that was 100% to-do therefore rendered as a completely empty grey bar: the data
   was there and the widget looked broken. Every segment now clears 3:1 against the track. */
.seg-todo{background:#64748b} .seg-prog{background:var(--brand)}
/* Not var(--ok): that green measures 2.93:1 against this track, under the 3:1 floor WCAG 1.4.11
   sets for a graphical object carrying meaning. One shade darker clears it at 3.40:1 — scoped
   to the segment rather than moving --ok, which is correct everywhere else it is used. */
.seg-done{background:#149644}
:root[data-theme="dark"] .seg-todo{background:#94a3b8;color:#0f172a}
.shead{display:flex;align-items:baseline;gap:8px;margin-bottom:10px}
.shead .sbig{font-size:22px;font-weight:800;font-family:var(--font-heading)}
.shead .ssub{font-size:12px;color:var(--muted)}
.legend{display:flex;gap:16px;margin-top:12px;font-size:12.5px;color:var(--text2);flex-wrap:wrap}
.legend .li{display:flex;align-items:center;gap:6px} .legend b{color:var(--text)}
.ldot{width:10px;height:10px;border-radius:3px}
/* ai panel */
.ai-panel{background:linear-gradient(180deg,var(--brand-subtle),var(--bg-surface));border-color:color-mix(in srgb,var(--brand) 22%, var(--border))}
.ai-badge{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;font-weight:700;letter-spacing:.04em;color:var(--brand);background:var(--bg-surface);border:1px solid color-mix(in srgb,var(--brand) 30%, transparent);border-radius:20px;padding:3px 9px;text-transform:uppercase}
.ins{border:1px solid var(--border);background:var(--bg-surface);border-radius:var(--r3);padding:11px 12px;margin-top:10px;font-size:13px;line-height:1.55}
/* ── multi-select option grid ──────────────────────────────────────────────
   Every cell has a real minimum width, which is the whole point: the previous inline-flex
   labels let a name wrap mid-name whenever the remaining line width ran short. auto-fill
   rather than auto-fit so a short list keeps its column width instead of stretching two
   options across the full card. */
.optgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:6px}
.opt{display:flex;align-items:flex-start;gap:9px;padding:9px 11px;border:1px solid var(--border);
  border-radius:var(--r2);background:var(--bg-surface);cursor:pointer;font-size:13px;
  min-height:44px;transition:border-color .12s,background .12s}
.opt:hover{border-color:var(--border-strong);background:var(--bg-subtle)}
/* :has() lets the whole cell reflect the checkbox, so the selected set is legible at a glance
   rather than by scanning for ticks. */
.opt:has(input:checked){border-color:var(--brand);background:var(--brand-subtle)}
.opt:focus-within{outline:2px solid var(--brand);outline-offset:1px}
.opt input{margin-top:2px;flex:none;accent-color:var(--brand)}
.opt-body{display:flex;flex-direction:column;gap:1px;min-width:0}
.opt-label{font-weight:600;line-height:1.35}
.opt-hint{font-size:11px;color:var(--muted);line-height:1.3}
.optgroups{display:flex;flex-direction:column;gap:14px}
.optgroup-h{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
  color:var(--muted);margin-bottom:7px}

/* ── org chart ─────────────────────────────────────────────────────────────
   An indented list says "these are nested"; a chart says "this person reports to that one".
   The difference is the connectors, so they are drawn rather than implied by margin. */
.org-tree,.org-children{list-style:none;margin:0;padding:0}
.org-children{margin-inline-start:20px;padding-inline-start:20px;position:relative}
/* The vertical spine, stopped at the last child so it does not trail past the final node. */
.org-children::before{content:"";position:absolute;inset-block:0;inset-inline-start:0;width:1px;background:var(--border-strong)}
.org-children>.org-node:last-child::after{content:"";position:absolute;inset-inline-start:-20px;
  top:calc(var(--org-elbow) + 1px);bottom:0;width:1px;background:var(--bg-surface)}
.org-node{position:relative;--org-elbow:26px}
/* The elbow into each card. */
.org-children>.org-node::before{content:"";position:absolute;inset-inline-start:-20px;top:var(--org-elbow);
  width:20px;height:1px;background:var(--border-strong)}
.org-card{display:flex;align-items:center;gap:11px;padding:9px 12px;margin:5px 0;
  border:1px solid var(--border);border-radius:var(--r3);background:var(--bg-surface)}
.org-card:hover{border-color:var(--border-strong)}
.org-ava{flex:none;width:34px;height:34px;border-radius:50%;display:grid;place-items:center;
  background:var(--brand-subtle);color:var(--brand);font-size:12px;font-weight:700;font-family:var(--font-heading)}
.org-who{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
.org-name{font-size:13.5px;font-weight:700;font-family:var(--font-heading)}
.org-title{font-size:11.5px;color:var(--text2)}
.org-meta{display:flex;align-items:center;gap:7px;margin-top:3px;flex-wrap:wrap}
.org-dept{font-size:11px;color:var(--muted)}
/* Direct-report count — the one number an org chart is actually asked for. */
.org-count{flex:none;min-width:24px;height:24px;padding:0 7px;border-radius:20px;display:grid;place-items:center;
  background:var(--bg-subtle);color:var(--text2);font-size:11.5px;font-weight:700}
/* Editing in place. Deliberately quiet — it is a correction tool, not the point of the card,
   so it stays out of the way until the row is hovered or the control is focused. */
.org-edit{flex:none;display:flex;align-items:center;gap:7px;opacity:.35;transition:opacity .12s}
.org-card:hover .org-edit,.org-edit:focus-within{opacity:1}
.org-edit-l{font-size:10.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
.org-edit select{max-width:190px}
@media(max-width:760px){
  .org-card{flex-wrap:wrap}
  /* Always visible on touch, where there is no hover to reveal it. */
  .org-edit{opacity:1;width:100%;margin-top:8px;padding-top:8px;border-top:1px solid var(--border)}
  .org-edit select{flex:1;max-width:none}
}
.org-orphans{margin-top:18px;padding-top:14px;border-top:1px solid var(--border)}
@media(max-width:560px){
  .org-children{margin-inline-start:10px;padding-inline-start:14px}
  .org-children>.org-node::before{inset-inline-start:-14px;width:14px}
  .org-children>.org-node:last-child::after{inset-inline-start:-14px}
}
/* Language switch on the login card — top corner, quiet, but reachable before sign-in. */
/* Brand and language switch on one baseline. `space-between` and not a float, so it mirrors
   for Arabic without a single directional value. */
.auth-top{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:22px}
.auth-top .auth-brand{margin-bottom:0}
/* Pinned height, because .btn.sm is sized by its text: English rendered 26px and Arabic 29px
   from the same rule, so the one control changed size when it was used. */
.auth-top .auth-lang{flex:none;display:inline-flex;align-items:center;min-height:32px}
.auth-restricted a{color:var(--brand);font-weight:600}
/* The riyal symbol sits in the text baseline like a glyph would: sized in em so it tracks the
   surrounding font-size, and currentColor so it inherits the text colour in both themes. */
.money{display:inline-flex;align-items:center;gap:.28em;white-space:nowrap}
.riyal{width:.72em;height:.72em;flex:none;fill:currentColor;position:relative;top:.045em}
/* report builder — a filter is one readable sentence per row, so the controls sit on a line
   rather than in a grid that would break "field / operator / value" apart. */
.rb-filter{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:8px}
.rb-filter select,.rb-filter input{border:1px solid var(--border-strong);background:var(--bg-surface);
  color:var(--text);border-radius:var(--r2);padding:7px 9px;font:inherit;font-size:13px;min-width:0}
.rb-filter select{max-width:210px}
.rb-filter input{max-width:190px}
.rb-total td{border-top:2px solid var(--border-strong);font-weight:700}
.rb-foot{margin-top:10px;font-size:12px;color:var(--muted)}
/* roster checklist — picking someone the system already knows, instead of retyping them */
.ckl{display:flex;flex-direction:column;gap:2px;max-height:220px;overflow-y:auto;border:1px solid var(--border);border-radius:var(--r2);padding:4px}
.ckl-row{display:flex;align-items:flex-start;gap:9px;padding:8px 8px;border-radius:var(--r2);cursor:pointer;font-size:13px}
.ckl-row:hover{background:var(--bg-subtle)}
.ckl-row input{margin-top:2px;flex:none}
/* expected close on a deal card — quiet by default, loud only when the date has passed */
.dealdate{display:flex;align-items:center;gap:5px;margin-top:6px;font-size:11.5px;color:var(--muted)}
.dealdate.overdue{color:var(--danger);font-weight:700}
/* project financials — three numbers whose GAPS are the point, so they are stacked and
   aligned rather than spread across cards where the eye has to travel to compare them. */
.finrow{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:8px 2px;border-top:1px solid var(--border);font-size:13px;color:var(--text2)}
.finrow:first-of-type{border-top:0}
.finrow b{font-size:14px;color:var(--text)}
.finrow.bad b{color:var(--danger)} .finrow.good b{color:var(--ok)}
.finsplit{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:12px;padding-top:12px;border-top:1px solid var(--border)}
.finsplit .fl{font-size:11.5px;color:var(--muted)}
.finsplit .fv{font-size:14px;font-weight:700;margin-top:2px}
.finsplit .fv.bad{color:var(--danger)}
.burnwrap{margin-top:12px}
.burnlabel{display:flex;justify-content:space-between;font-size:12px;color:var(--text2);margin-bottom:5px}
.schednote{margin-top:10px;font-size:12px;color:var(--muted)}
.schednote.warn{color:var(--warn);font-weight:600}
.schednote.bad{color:var(--danger);font-weight:700}
/* list rows */
/* A ranked money list where every row looks identical makes the reader compare numerals.
   A proportional tint behind the row turns "which of these is big" into a glance. */
.lrow{position:relative;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 2px;border-top:1px solid var(--border);isolation:isolate}
.lrow:first-child{border-top:0}
.lrow .lshare{position:absolute;inset-block:4px;inset-inline-start:-6px;z-index:-1;border-radius:var(--r2);background:color-mix(in srgb,var(--brand) 9%, transparent);pointer-events:none}
.lrow.win .lshare{background:color-mix(in srgb,var(--ok) 11%, transparent)}
.lrow .lt{font-size:13.5px;font-weight:600}.lrow .ld{font-size:11.5px;color:var(--muted);margin-top:1px}
.lrow .lv{font-size:13.5px;font-weight:700;white-space:nowrap}
@media(max-width:1100px){.kpirow{grid-template-columns:repeat(3,1fr)}}
@media(max-width:680px){
  .kpirow{grid-template-columns:repeat(2,1fr)}
  /* minmax(0,1fr), not 1fr. `1fr` is shorthand for minmax(AUTO,1fr), and that automatic
     minimum means the track can never be narrower than its content's min-content width — so
     one stubborn descendant silently widens the column past the screen. On the project detail
     that produced a 640px track inside a 347px grid and 280px of horizontal page scroll.
     minmax(0,1fr) lets the track shrink and makes the content do the adapting. */
  .two{grid-template-columns:minmax(0,1fr)}
  .cardgrid{grid-template-columns:minmax(0,1fr)}
  .rolesel{max-width:130px}
  /* Touch targets: WCAG 2.5.5 wants >=44px. Desktop stays dense (mouse input
     doesn't need it); mobile is where imprecise touch actually matters. */
  .tabs button{min-height:44px;display:flex;align-items:center;justify-content:center}
  .btn{min-height:44px}
  .ib{width:44px;height:44px}
  /* The console's operator name, hidden on a narrow screen so the avatar alone carries it. */
  .who span{display:none}
  .fld input,.fld select{min-height:44px}
  .date-range select,.date-range input[type=date]{min-height:44px}
}
/* buttons */
.btn{border:1px solid var(--border);background:var(--bg-surface);color:var(--text);padding:8px 14px;border-radius:var(--r2);font-size:13px;font-weight:600}
.btn:hover{background:var(--bg-hover)}
.btn.primary{background:var(--brand);border-color:var(--brand);color:var(--on-brand)}
.btn.primary:hover{background:var(--brand-hover)}
.btn.danger{color:var(--danger)}
.btn.danger:hover{background:var(--danger-bg,#fef2f2)}
.btn.sm{padding:5px 10px;font-size:12px}
/* table */
.panel{padding:16px 18px}
.ph{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.ptitle{font-weight:700;font-size:14.5px}
.tbl-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:13.5px;line-height:1.5}
th,td{text-align:start;padding:11px 12px;border-top:1px solid var(--border);vertical-align:middle}
th{font-size:11px;font-weight:650;text-transform:uppercase;letter-spacing:.07em;
  color:var(--muted);border-top:0;white-space:nowrap;padding-bottom:9px;line-height:1.3}
tbody tr:hover{background:var(--bg-hover)}
.pill{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:700;padding:2px 9px;border-radius:20px;background:var(--bg-subtle);color:var(--text2)}
.pill.new{background:var(--info-bg);color:var(--info)} .pill.qualified{background:var(--brand-subtle);color:var(--brand)}
.pill.converted{background:var(--ok-bg);color:var(--ok)} .pill.won{background:var(--ok-bg);color:var(--ok)}
.score{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:700;padding:2px 9px;border-radius:20px;background:var(--brand-subtle);color:var(--brand)}
.score.lo{background:var(--warn-bg);color:var(--warn)}
.ai{font-size:9px;opacity:.85}
.pill.pr-high{background:var(--danger-bg,#fef2f2);color:var(--danger)} .pill.pr-medium{background:var(--warn-bg);color:var(--warn)} .pill.pr-low{background:var(--info-bg);color:var(--info)}
.pill.lost{background:var(--danger-bg,#fef2f2);color:var(--danger)}
/* A close date the deal has already sailed past — the one state worth colouring, since it is
   the reason to record the date at all. Shares the danger tokens rather than a new colour. */
.pill.overdue{background:var(--danger-bg,#fef2f2);color:var(--danger)}
.rolesel{border:1px solid var(--border);background:var(--bg-surface);color:var(--text);border-radius:var(--r2);padding:4px 8px;font:inherit;font-size:12.5px}
/* ── data-table density ──────────────────────────────────────────────────────────────────
   `.empty` is an EMPTY-STATE style: muted, small, and with its own 8px/2px padding. It is
   also used on table CELLS purely to borrow the muted colour, and it drags that 2px
   horizontal padding along — which cramped Email and Mobile to almost no gutter and let the
   neighbouring columns take the slack. On a cell the table's own padding wins; the colour
   and size still apply. */
td.empty{padding:10px 10px}

/* A person's name is the column a reader scans, so it gets a floor. Without one the browser
   handed Name 115px and Manager 212px, and "Faisal Al-Mutairi" wrapped onto two lines while
   his job title took three — which is what pushed rows to 131-146px tall. */
.tbl-people td:first-child,.tbl-people th:first-child{min-width:176px}
.tbl-people td:first-child b{font-size:13px;line-height:1.35}
.tbl-people .jobtitle{font-size:11.5px;line-height:1.3;margin-top:1px}
/* The selects sized to their longest option, so Manager alone claimed 212px. Capping them is
   only half the job: a cap with width:100% lets the COLUMN collapse and the select truncates
   its own value ("Faisal Al-Mu…"). So the columns that hold a select get a floor too, and the
   value is what decides the width — a control you cannot read is worse than a wide one. */
/* The type scale for this table is a size down from the default: at 13.5/12.5 the eight
   columns could not all show their longest value inside 1098px, and a select that clips its
   own value ("Mohammed Al-Zahra…") is worse than one set a point smaller. */
.tbl-people{font-size:13px}
.tbl-people select.rolesel{max-width:100%;width:100%;font-size:12px;padding:4px 6px}
.tbl-people td:nth-child(4),.tbl-people th:nth-child(4){min-width:134px}   /* Role */
/* 164, not 148: the longest real manager name ("Mohammed Al-Zahrani") needs it, and the width
   comes out of Email — which was holding 167px for addresses like mohammed@motm.sa. */
/* Sized so the SELECTED value fits — a select need only show its current value; the native
   dropdown reveals the full list. Sizing for the longest option in the roster
   ("Abdulrahman Al-Qahtani") would need a 202px column and starve every other one. */
.tbl-people td:nth-child(6),.tbl-people th:nth-child(6){min-width:176px}   /* Manager */
.tbl-people td:nth-child(2),.tbl-people th:nth-child(2){max-width:152px}   /* Email   */
.tbl-people td:nth-child(7),.tbl-people th:nth-child(7){min-width:126px}   /* Sees    */
/* A phone number is one token; wrapping it mid-number makes it unreadable and adds a line. */
.tbl-people td:nth-child(3){white-space:nowrap;min-width:116px}
/* Give the squeeze somewhere to come from rather than taking it out of the columns above. */
.tbl-people td:nth-child(5),.tbl-people th:nth-child(5){max-width:120px}   /* Department */
.tbl-people td,.tbl-people th{padding:9px 10px}
.tbl-people td .btn.sm{padding:3px 9px;font-size:11.5px}

.permgrid-table{width:auto;font-size:12.5px}
.permgrid-table td,.permgrid-table th{padding:5px 10px}
/* form */
/* minmax(0,…) not 1fr: a bare `1fr` track floors at its content's min-content width, so one
   input with a long placeholder pushes the whole row past the card and clips the last field.
   Paired with min-width:0 on the controls, which have their own intrinsic `size` width. */
.form{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:10px;align-items:end}
.fld{display:flex;flex-direction:column;gap:4px;min-width:0}
.fld label{font-size:11.5px;color:var(--text2);font-weight:600}
/* :not() on the checkable types is load-bearing, not tidiness. The width:100% here exists to
   stop text inputs blowing out the form grid — applied to a checkbox it stretches a 13px box
   to the full column and shoves its own label out of the cell, which is exactly what happened
   to the automation owner picker. A checkbox never wants a stretched width. */
.fld input:not([type="checkbox"]):not([type="radio"]),.fld select{min-width:0;width:100%;border:1px solid var(--border-strong);background:var(--bg-surface);color:var(--text);border-radius:var(--r2);padding:8px 10px;font:inherit}
.acct-combo{position:relative}
.combo-list:not(:empty){position:absolute;top:100%;left:0;right:0;margin-top:4px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--r2);box-shadow:var(--e2);max-height:220px;overflow-y:auto;z-index:20}
.combo-opt{padding:8px 10px;font-size:13.5px;cursor:pointer}
.combo-opt:hover,.combo-opt.sel{background:var(--bg-hover)}
.combo-empty{padding:8px 10px;font-size:13px;color:var(--muted)}
/* sales offers */
.li-row{display:grid;grid-template-columns:1fr 90px 130px 130px 32px;gap:8px;align-items:center;padding:8px;border:1px solid var(--border);border-radius:var(--r2);background:var(--bg-subtle);margin-bottom:6px}
.li-row:focus-within{border-color:var(--border-strong)}
.li-row input{background:var(--bg-surface)}
.offer-total{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:10px;padding:12px 16px;background:var(--brand-subtle);border-radius:var(--r2)}
.offer-total .l{font-size:12.5px;font-weight:600;color:var(--text2)}
.offer-total .v{font-size:19px;font-weight:700;color:var(--brand)}
/* pipeline */
.board{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px;padding-bottom:8px}
.col{background:var(--bg-subtle);border-radius:var(--r);padding:11px;min-height:120px;transition:background .12s,outline-color .12s}
.col.dragover{background:var(--brand-subtle);outline:2px dashed var(--brand);outline-offset:-2px}
.deal[data-opp]{cursor:pointer}
.colh{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;padding-inline:2px}
.coln{font-size:13px;font-weight:700;display:flex;gap:7px;align-items:center}
.cc{font-size:11px;color:var(--muted);background:var(--bg-surface);border-radius:20px;padding:1px 8px;font-weight:600}
.dot{width:9px;height:9px;border-radius:50%}
.deal{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--r3);padding:11px;margin-bottom:9px;box-shadow:var(--e1);transition:opacity .12s}
.deal[draggable="true"]{cursor:grab}
.deal.dragging{opacity:.4;cursor:grabbing}
.deal .n{font-size:13px;font-weight:600}.deal .a{font-size:11.5px;color:var(--muted);margin-top:2px}
.deal .m{display:flex;justify-content:space-between;align-items:center;margin-top:9px}
.amt{font-weight:700;font-size:13.5px}
.empty{color:var(--muted);font-size:12.5px;padding:8px 2px}
/* toast */
#toast{position:fixed;inset-block-end:20px;inset-inline:0;display:flex;justify-content:center;pointer-events:none;z-index:50}
#toast>div{background:var(--text);color:var(--bg-surface);padding:11px 18px;border-radius:var(--r3);font-size:13.5px;box-shadow:var(--e2);opacity:0;transform:translateY(8px);transition:.2s}
#toast>div.show{opacity:1;transform:none}
/* modal */
.modal-backdrop{position:fixed;inset:0;background:rgb(2 6 23/.5);display:grid;place-items:center;z-index:60;padding:20px}
.modal{background:var(--bg-surface);border-radius:var(--r);box-shadow:var(--e2);max-width:480px;width:100%;max-height:85vh;overflow-y:auto;padding:20px 22px}
.modal-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:14px}
.modal-head h2{font-size:17px;margin:0}
.modal-close{border:0;background:transparent;color:var(--text2);font-size:20px;cursor:pointer;line-height:1;padding:4px;flex-shrink:0}
.modal-close:hover{color:var(--text)}
.modal-row{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-top:1px solid var(--border);font-size:13.5px}
.modal-row:first-child{border-top:0}
.modal-row .k{color:var(--text2)}
.modal-row .v{font-weight:600;text-align:end}
.contact-card{border:1px solid var(--border);border-radius:10px;padding:10px 12px;margin-bottom:8px;display:flex;gap:10px;align-items:flex-start}
.contact-card .cc-ava{flex-shrink:0;width:32px;height:32px;border-radius:50%;background:var(--bg-subtle);color:var(--text2);display:grid;place-items:center;font-size:11.5px;font-weight:700}
.contact-card .cc-body{min-width:0;flex:1}
.contact-card .cn{font-weight:600;font-size:13.5px;display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.contact-card .cc-title{font-size:11px;font-weight:600;color:var(--text2);background:var(--bg-subtle);border-radius:20px;padding:1px 8px;white-space:nowrap}
.contact-card .cc-meta{display:flex;flex-wrap:wrap;gap:3px 14px;margin-top:4px}
.contact-card .cc-item{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--text2);min-width:0}
.contact-card .cc-item svg{flex-shrink:0;opacity:.65}
.comment-card{border:1px solid var(--border);border-radius:10px;padding:9px 12px;margin-bottom:8px}
.comment-card .cn{font-size:12px;color:var(--text2);font-weight:600}
.comment-card .cn span{font-weight:500;color:var(--muted)}
.comment-card .cb{font-size:13px;margin-top:4px;white-space:pre-wrap;line-height:1.5}
.doc-card{border:1px solid var(--border);border-radius:10px;padding:9px 12px;margin-bottom:8px;display:flex;flex-direction:column;gap:2px}
.doclink{color:var(--brand);font-weight:600;font-size:13.5px;cursor:pointer}
.doclink:hover{text-decoration:underline}
.mytask-row{display:flex;align-items:center;gap:8px;padding:8px 0;border-top:1px solid var(--border);cursor:pointer}
.mytask-row:first-child{border-top:0}
.mytask-row:hover{background:var(--bg-hover)}
.mytask-title{flex:1;font-size:13.5px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mytask-due{font-size:11.5px;color:var(--muted);flex-shrink:0}
.mytask-due.overdue{color:var(--danger);font-weight:600}
textarea{border:1px solid var(--border-strong);background:var(--bg-surface);color:var(--text);border-radius:var(--r2);padding:8px 10px;font:inherit;resize:vertical;width:100%}
/* login / OTP — mobile-first: full-height, single column, large tap targets */
#main.auth-mode{max-width:none;padding:20px;min-height:100vh;display:grid;place-items:center;box-sizing:border-box}
.auth-wrap{display:flex;flex-direction:column;align-items:center;gap:18px;width:100%;max-width:400px}
.auth-card{width:100%;max-width:400px;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--r);box-shadow:var(--e2);padding:32px 26px}
.auth-brand{display:flex;align-items:center;gap:10px;margin-bottom:22px;min-width:0}
.auth-brand img{width:36px;height:36px}
.auth-brand b{font-family:var(--font-heading);font-size:15px}
.auth-card h1{font-size:21px;line-height:1.3;margin:0 0 10px}
.auth-restricted{font-size:12.5px;color:var(--text2);line-height:1.5;margin:0 0 22px;padding-bottom:20px;border-bottom:1px solid var(--border)}
.auth-card .fld{margin-bottom:14px}
.auth-card .fld label{font-size:13px}
/* 44px, matching the Log in button and the touch-target floor. These were 37px on a desktop
   and 44 on a phone, so the form looked cramped next to its own primary button. */
.auth-card .fld input{padding:12px;font-size:15px;width:100%;min-height:44px}

/* The reveal toggle sits INSIDE the field. inset-inline-end and padding-inline-end mirror for
   Arabic on their own — a `right` here would have put the button on top of the caret in RTL. */
.pw-wrap{position:relative;display:flex;align-items:center}
.pw-wrap input{padding-inline-end:44px}
.pw-toggle{position:absolute;inset-inline-end:4px;top:50%;transform:translateY(-50%);
  display:flex;align-items:center;justify-content:center;width:36px;height:36px;
  background:none;border:0;border-radius:var(--r2);color:var(--muted);cursor:pointer;padding:0}
.pw-toggle:hover{color:var(--text);background:var(--bg-hover)}
.pw-toggle:focus-visible{outline:2px solid var(--brand);outline-offset:2px;color:var(--text)}
.pw-toggle svg{display:block}
/* The generate/copy pair under a password an administrator is about to hand over. */
.pw-tools{display:flex;gap:6px;margin-top:6px}

/* A six-digit code is read back character by character against a phone or an inbox, so it is
   spaced out and set in the tabular face rather than left as ordinary prose. */
.otp-input{font-family:var(--font-body);font-size:20px;font-weight:600;letter-spacing:.32em;
  font-variant-numeric:tabular-nums;text-indent:.32em}
.auth-card .btn.primary{width:100%;padding:13px;font-size:14.5px;margin-top:6px}
.auth-error{background:var(--danger-bg,#fef2f2);color:var(--danger);font-size:13px;border-radius:8px;padding:10px 12px;margin-top:14px;line-height:1.5}
.dev-mode-banner{background:var(--warn-bg);color:var(--warn);font-size:12.5px;border-radius:8px;padding:10px 12px;margin-bottom:16px;line-height:1.5}
.dev-mode-banner b{font-size:16px;letter-spacing:.04em;font-variant-numeric:tabular-nums}
.auth-back{background:none;border:0;color:var(--text2);font-size:12.5px;cursor:pointer;padding:0;margin-bottom:14px}
/* The same quiet treatment as .auth-back, but sitting UNDER the primary action rather than
   above the heading — a secondary way out of the card, not a way back up it. Full width so its
   hit area matches the button above it rather than being a word-sized target. */
.auth-link{background:none;border:0;color:var(--text2);font-size:12.5px;cursor:pointer;padding:10px 0 0;margin:0;width:100%;text-align:center}
.auth-link:hover{color:var(--text)}
.auth-back:hover{color:var(--text)}
@media(min-width:560px){.auth-card{padding:40px 36px}}
/* rows that open a detail modal */
.rowlink{cursor:pointer}
.rowlink:hover{background:var(--bg-hover)}
@media(max-width:900px){.kpis{grid-template-columns:repeat(2,1fr)}.form{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:560px){.kpis{grid-template-columns:1fr}.bsub{display:none}.form{grid-template-columns:1fr}.fld{grid-column:1/-1!important}#whoName{display:none}.top-inner{padding-inline:10px}.top-row{gap:8px}
  /* The .li-row reflow that used to live here described a FIVE-column grid that no longer
     exists — the row gained an SN cell and a unit-cost cell, so its nth-child rules were
     placing the wrong children, and its !important beat the newer rule below. On a phone that
     left the description clipped mid-word, cost and price scattered unlabelled, and the line
     total cut off at the card edge. The current layout is defined once, near .li-row itself. */
}

/* ══ mobile app shell ═══════════════════════════════════════════════════════════════════
   Below 680px this stops being a desktop page with smaller margins and behaves like an app.
   Three measured problems drove it, taken on a 375×812 phone:

     · the 12-item tab strip wrapped onto four rows and ate 188px — with the header, 257px
       of an 812px screen (32%) was gone before a single row of data
     · every data table laid out at 752px inside a 375px viewport, so reading one lead meant
       scrolling sideways
     · destinations sat at the top of a 812px screen, the hardest place for a thumb to reach

   So: navigation moves to a fixed bottom bar, the tab strip is dropped on mobile only, and
   table rows become cards. Everything is logical-property based (inset-inline, padding-inline)
   so the Arabic RTL build mirrors for free. */

/* The bar itself is desktop-hidden; it is the ONLY nav below 680px, where .tabs is dropped. */
/* All three exist in the DOM at every width; everything that STYLES them lives in the media
   query below. So they must be hidden here, or above 680px the sheet renders as a plain div at
   the end of the page and its icons — whose fill:none/stroke rules are also in the query —
   fall back to a solid black fill. */
.mnav{display:none}
.msheet{display:none}
.msheet-back{display:none}
/* Belt and braces: the icons carry their own presentation attributes too, so a path can never
   be painted as a filled silhouette even if it is rendered outside the styled context. */
.mnav svg,.msheet svg{fill:none;stroke:currentColor}
@media(max-width:680px){
  /* Reclaim the 188px. The bottom bar carries these destinations now. */
  .tabs{display:none}
  .top-inner{padding:8px 14px}
  /* Bottom padding clears the fixed bar plus the home-indicator strip on notched phones. */
  .wrap{padding:16px 14px calc(74px + env(safe-area-inset-bottom,0px))}

  .mnav{
    display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);
    position:fixed;inset-inline:0;bottom:0;z-index:40;
    /* Translucent over content, the way a native tab bar sits above a scrolling list. */
    background:color-mix(in srgb,var(--bg-surface) 88%, transparent);
    -webkit-backdrop-filter:saturate(180%) blur(16px);backdrop-filter:saturate(180%) blur(16px);
    border-top:1px solid var(--border);
    padding-bottom:env(safe-area-inset-bottom,0px);
  }
  .mnav button{
    display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;
    background:none;border:0;margin:0;padding:6px 2px 7px;cursor:pointer;font:inherit;
    /* 54px + the safe-area strip clears the 44px WCAG 2.5.5 target with room to spare. */
    min-height:54px;color:var(--muted);-webkit-tap-highlight-color:transparent;
  }
  .mnav .micon{display:grid;place-items:center;width:44px;height:26px;border-radius:20px;
    transition:background .16s,color .16s}
  .mnav svg{width:21px;height:21px;fill:none;stroke:currentColor;stroke-width:1.7;
    stroke-linecap:round;stroke-linejoin:round}
  .mnav .mlabel{font-size:10px;font-weight:600;line-height:1.15;max-width:100%;
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  /* The active destination is the one thing on the bar that must be unmistakable. */
  .mnav button.on{color:var(--brand)}
  .mnav button.on .micon{background:var(--brand-subtle)}
  .mnav button:active .micon{transform:scale(.92)}

  /* ── the "More" sheet ────────────────────────────────────────────────────────────────
     Four destinations fit a thumb bar; this app has up to twelve. The rest live in a sheet
     that rises from the bottom edge — reachable, and it does not push content around. */
  .msheet-back{display:block;position:fixed;inset:0;z-index:45;background:rgba(15,23,42,.45);
    opacity:0;pointer-events:none;transition:opacity .2s}
  .msheet-back.open{opacity:1;pointer-events:auto}
  .msheet{display:block;position:fixed;inset-inline:0;bottom:0;z-index:46;
    background:var(--bg-surface);border-top:1px solid var(--border);
    border-radius:18px 18px 0 0;box-shadow:0 -8px 40px rgba(15,23,42,.18);
    padding:8px 12px calc(16px + env(safe-area-inset-bottom,0px));
    transform:translateY(101%);transition:transform .26s cubic-bezier(.32,.72,0,1);
    max-height:72vh;overflow-y:auto}
  .msheet.open{transform:none}
  /* The grab handle is what tells a phone user this panel is a sheet, not a page. */
  .msheet::before{content:"";display:block;width:38px;height:4px;border-radius:4px;
    background:var(--border-strong);margin:2px auto 12px}
  .msheet .msheet-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px}
  .msheet button{display:flex;flex-direction:column;align-items:center;justify-content:center;
    gap:7px;background:none;border:1px solid transparent;border-radius:var(--r3);
    padding:14px 6px;min-height:76px;cursor:pointer;font:inherit;color:var(--text2);
    -webkit-tap-highlight-color:transparent}
  .msheet button svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:1.7;
    stroke-linecap:round;stroke-linejoin:round}
  .msheet button span{font-size:11.5px;font-weight:600;text-align:center;line-height:1.25}
  .msheet button.on{color:var(--brand);background:var(--brand-subtle);border-color:transparent}

  /* ── tables become cards ─────────────────────────────────────────────────────────────
     A 7-column table cannot be read on a 375px screen; shrinking the type only makes it
     unreadable AND cramped. Each row becomes a card, each cell a "label: value" line, with
     the first cell as the card's title. The labels come from the table's own <thead>, stamped
     onto the cells at render time (see labelTableCells) — so this applies to all 18 tables
     and to any added later, with no change to a single table renderer. */
  .tbl-wrap{overflow-x:visible}
  .tbl-wrap table{display:block;width:100%;border-collapse:separate;border-spacing:0}
  .tbl-wrap thead{display:none}
  .tbl-wrap tbody,.tbl-wrap tfoot{display:block}
  .tbl-wrap tbody tr,.tbl-wrap tfoot tr{display:block;background:var(--bg-surface);
    border:1px solid var(--border);border-radius:var(--r3);padding:12px 14px;margin-bottom:10px}
  .tbl-wrap tbody td,.tbl-wrap tfoot td{display:flex;align-items:baseline;justify-content:space-between;
    gap:14px;padding:6px 0;border:0;white-space:normal;
    /* Beats the inline text-align:end that money columns carry for the desktop layout. */
    text-align:start!important}
  .tbl-wrap tbody td::before,.tbl-wrap tfoot td::before{content:attr(data-label);flex:none;
    color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}
  /* An unlabelled cell (the actions column, or a colspan empty-state row) gets no prefix. */
  .tbl-wrap tbody td:not([data-label])::before,.tbl-wrap tfoot td:not([data-label])::before{content:none}
  .tbl-wrap tbody td[data-full],.tbl-wrap tfoot td[data-full]{display:block;text-align:center!important}
  /* Cell one is the card's headline, not a labelled field. */
  .tbl-wrap tbody td[data-title]{display:block;font-family:var(--font-heading);font-size:15px;
    font-weight:700;color:var(--text);padding:0 0 9px;margin-bottom:5px;
    border-bottom:1px solid var(--border)}
  .tbl-wrap tbody td[data-title]::before{content:none}
  /* Row actions get room to be tapped rather than being squeezed into a table cell. */
  .tbl-wrap tbody td .btn{min-height:40px}

  /* Filters and toolbars stack instead of overflowing. */
  .rb-filter select,.rb-filter input{max-width:none;flex:1 1 100%}
  .toolbar,.rowbar{flex-wrap:wrap}
}

/* ══ people directory ═══════════════════════════════════════════════════════════════════
   The Team screen was an admin grid: twelve empty invite fields above the fold, then eight
   columns in which every row carried three dropdowns and three buttons. Two problems that no
   amount of column-width tuning fixes — the page answered "add a person" before "who is here",
   and the controls out-shouted the people they belonged to.

   So: a roster of person cards. Identity leads (face, name, title), the assignments read as
   VALUES that happen to be editable, and the invite form is behind a button because inviting
   is the rare act and looking someone up is the common one. */
.people{display:flex;flex-direction:column;gap:8px}
.person{display:grid;gap:14px;align-items:center;padding:12px 14px;border:1px solid var(--border);
  border-radius:var(--r3);background:var(--bg-surface);
  grid-template-columns:auto minmax(210px,1.4fr) repeat(3,minmax(120px,1fr)) auto;
  transition:border-color .14s,box-shadow .14s}
.person:hover{border-color:var(--border-strong);box-shadow:0 1px 3px rgba(15,23,42,.06)}
.person.is-off{opacity:.62}

.person .pava{width:38px;height:38px;border-radius:50%;display:grid;place-items:center;flex:none;
  background:var(--brand-subtle);color:var(--brand);font-family:var(--font-heading);
  font-weight:700;font-size:13px;letter-spacing:.01em}
.person .pid{min-width:0}
.person .pname{font-family:var(--font-heading);font-weight:700;font-size:14px;line-height:1.3;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.person .ptitle{font-size:11.5px;color:var(--text2);line-height:1.35;margin-top:1px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.person .pmeta{font-size:11px;color:var(--muted);margin-top:3px;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* A labelled field that reads as a value and edits in place. Quiet until you reach for it —
   the border only appears on hover or focus, so twelve rows do not look like twelve forms. */
.pfield{min-width:0}
.pfield > span{display:block;font-size:9.5px;font-weight:700;letter-spacing:.06em;
  text-transform:uppercase;color:var(--muted);margin-bottom:3px}
.pfield select,.pfield .pstatic{width:100%;max-width:100%;font:inherit;font-size:12.5px;
  color:var(--text);background:transparent;border:1px solid transparent;border-radius:var(--r2);
  padding:4px 6px;transition:border-color .14s,background .14s}
.pfield select:hover{border-color:var(--border-strong);background:var(--bg-surface)}
.pfield select:focus{border-color:var(--brand);background:var(--bg-surface);outline:none}
.person .pend{display:flex;align-items:center;gap:6px;justify-content:flex-end;flex-wrap:wrap}
.person .pend .btn.sm{padding:4px 10px;font-size:11.5px}

/* Summary strip — a people page should answer "how many, and in what state" on arrival. */
.pstats{display:flex;gap:22px;flex-wrap:wrap;padding:2px 2px 16px}
.pstat b{display:block;font-family:var(--font-heading);font-size:21px;line-height:1.15}
.pstat span{font-size:11px;color:var(--muted)}
.ptoolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:14px}
.ptoolbar input[type="search"]{flex:1 1 240px;min-width:0;border:1px solid var(--border-strong);
  background:var(--bg-surface);color:var(--text);border-radius:var(--r2);padding:8px 11px;
  font:inherit;font-size:13px}
.ptoolbar input[type="search"]:focus{border-color:var(--brand);outline:none}

@media(max-width:980px){
  .person{grid-template-columns:auto minmax(180px,1fr) repeat(2,minmax(110px,1fr));row-gap:10px}
  .person .pend{grid-column:1/-1;justify-content:flex-start}
}
@media(max-width:680px){
  /* One column on a phone; the labels are what keep each value readable without a header row. */
  .person{grid-template-columns:auto 1fr;row-gap:12px;padding:14px}
  .person .pfield{grid-column:1/-1}
  .person .pfield select{border-color:var(--border)}   /* no hover on touch — show the affordance */
  .person .pend{grid-column:1/-1}
}

/* ══ org chart · horizontal (top-down) ═══════════════════════════════════════════════════
   The vertical tree is an indented list: it scales to any depth and reads well on a phone, but
   it does not show SHAPE — how wide a manager's span is, or who sits at the same level. The
   horizontal mode is the classic org chart for exactly that: siblings on one line under their
   parent. It costs width, so the container scrolls rather than squeezing the cards.

   Same markup drives both. Only the class on the root <ul> changes, so nothing about the tree
   building, the reports-to editor, or the orphan handling is duplicated. */
/* One variable so every connector segment is guaranteed to match; they used to be four separate
   literals and a change to one silently left the others behind. */
:root{--org-line:#a9b4c7}
:root[data-theme="dark"]{--org-line:#5a6480}
.org-scroll{overflow-x:auto;padding-bottom:6px}
/* width:max-content + min-width:100% is the pair that makes a CENTRED flex row scrollable:
   with justify-content:center alone the overflow spills past both edges of the scroll box and
   the far side cannot be reached at all. This way it centres while it fits and scrolls once it
   does not. */
.org-h{width:max-content;min-width:100%;align-items:flex-start}
.org-h,.org-h ul{display:flex;justify-content:center;list-style:none;margin:0;padding:0}
.org-h ul{padding-top:20px;position:relative}
.org-h li{list-style:none;position:relative;padding:20px 9px 0;text-align:center;display:flex;
  flex-direction:column;align-items:center}
/* Connectors: each child draws half the horizontal rail plus its own drop line. */
.org-h li::before,.org-h li::after{content:'';position:absolute;top:0;inset-inline-end:50%;
  border-top:2px solid var(--org-line);width:50%;height:20px}
.org-h li::after{inset-inline-end:auto;inset-inline-start:50%;
  border-inline-start:2px solid var(--org-line)}
/* An only child needs a straight drop, not a rail. */
.org-h li:only-child{padding-top:20px}
.org-h li:only-child::before,.org-h li:only-child::after{display:none}
.org-h li:only-child > .org-card::before{content:'';position:absolute;top:-20px;inset-inline-start:50%;
  height:20px;border-inline-start:2px solid var(--org-line)}
/* The outermost ends of the rail are removed, not merely un-bordered: zeroing the border still
   leaves the box, and its rounded corner painted a stray dash out in the whitespace beyond the
   first and last card. */
.org-h li:first-child::before,.org-h li:last-child::after{display:none}
.org-h li:last-child::before{border-inline-end:2px solid var(--org-line);border-start-end-radius:6px}
.org-h li:first-child::after{border-start-start-radius:6px}
/* Drop line from a parent card into its children's rail. */
.org-h ul::before{content:'';position:absolute;top:0;inset-inline-start:50%;height:20px;
  border-inline-start:2px solid var(--org-line)}
.org-h > li{padding-top:0;align-self:flex-start}
.org-h > li::before,.org-h > li::after{display:none}

/* Cards go compact and stacked when laid out side by side. */
.org-h .org-card{position:relative;flex-direction:column;width:172px;gap:6px;text-align:center;
  padding:12px 10px;align-items:center}
.org-h .org-who{align-items:center;text-align:center}
.org-h .org-meta{justify-content:center}
.org-h .org-edit{width:100%;opacity:1;margin-top:8px;padding-top:8px;
  border-top:1px solid var(--border);flex-direction:column;gap:4px}
.org-h .org-edit select{max-width:100%;width:100%}
.org-h .org-count{position:absolute;top:8px;inset-inline-end:8px}

/* ── org chart zoom ──────────────────────────────────────────────────────────────────────
   A wide organisation does not fit a laptop screen, and scrolling a chart you cannot see the
   shape of defeats the point of drawing one. `zoom` rather than `transform: scale()` on purpose:
   transform does not affect layout, so the scroll container keeps the unscaled size and the
   scrollbars lie about the extent. */
.org-zoomwrap{zoom:var(--orgz,1)}
.zoomctl{display:inline-flex;align-items:center;gap:2px;border:1px solid var(--border-strong);
  border-radius:var(--r2);overflow:hidden;background:var(--bg-surface)}
.zoomctl button{border:0;background:none;color:var(--text);font:inherit;font-size:14px;
  line-height:1;padding:7px 11px;cursor:pointer;min-width:34px}
.zoomctl button:hover{background:var(--bg-hover)}
.zoomctl button:disabled{opacity:.4;cursor:default;background:none}
.zoomctl .zlevel{font-size:11.5px;font-weight:700;color:var(--text2);min-width:46px;
  text-align:center;padding:0 4px;font-variant-numeric:tabular-nums}


/* ── org chart · horizontal ──────────────────────────────────────────────────────────────
   Cards are placed absolutely on the grid the SERVER computed with layoutOrg — the same
   function the PDF and PowerPoint use. The nested-list version let every subtree size itself,
   so columns came out ragged and the connectors, drawn as CSS borders on pseudo-elements,
   vanished under `zoom` when a 1px rule fell below one device pixel. These are real SVG
   strokes: they scale with the zoom instead of being rounded away by it. */
.oc-stage{position:relative;margin:0 auto}
.oc-lines{position:absolute;inset:0;overflow:visible;pointer-events:none}
.oc-lines path{fill:none;stroke:var(--org-line);stroke-width:1.6;
  stroke-linecap:square;shape-rendering:crispEdges}
.oc-card{position:absolute;display:flex;flex-direction:column;justify-content:space-between;
  box-sizing:border-box;padding:11px 12px 10px;border:1px solid var(--border);border-radius:10px;
  background:var(--surface);box-shadow:0 1px 2px rgba(16,24,40,.05);cursor:pointer;overflow:hidden;
  transition:border-color .16s ease,box-shadow .16s ease,transform .16s ease}
.oc-card::before{content:'';position:absolute;inset-inline:0;top:0;height:3px;background:var(--brand)}
.oc-card:hover{border-color:var(--brand);box-shadow:0 6px 16px rgba(16,24,40,.10);transform:translateY(-1px)}
.oc-card:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
.oc-top{display:flex;align-items:flex-start;gap:8px;min-width:0}
.oc-ava{flex:none;width:26px;height:26px;border-radius:50%;display:grid;place-items:center;
  font-size:10px;font-weight:700;letter-spacing:.02em;background:var(--brand-weak);color:var(--brand)}
/* min-width:0 is what lets the ellipsis engage — without it a flex item refuses to shrink
   below its content and the name pushes the count badge out of the card instead. */
.oc-id{display:flex;flex-direction:column;min-width:0;flex:1}
.oc-name{font-size:12.5px;font-weight:650;line-height:1.3;color:var(--ink);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.oc-title{font-size:11px;color:var(--muted);line-height:1.35;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.oc-count{flex:none;min-width:18px;height:18px;padding:0 5px;border-radius:9px;
  display:grid;place-items:center;font-size:10px;font-weight:700;
  background:var(--brand-weak);color:var(--brand)}
.oc-role .pill{font-size:9.5px;padding:1px 7px}
.oc-hint{margin-top:12px;font-size:12px;color:var(--muted)}
/* `zoom` scales layout, so the stage's own width drives the scrollbar honestly. */
.org-zoomwrap{zoom:var(--orgz,1)}
@media (max-width:680px){ .oc-card{padding:8px 10px} .oc-name{font-size:12px} }


/* ── table typography ────────────────────────────────────────────────────────────────────
   A cell's primary line and its supporting line are two different jobs, and until now both
   leaned on `.empty` — a class that exists for empty STATES and carries its own padding. The
   result was a secondary line that could not be tuned without moving genuine empty states. */
/* The primary line may run to two lines — a tender title is not something to truncate at
   half a word — but the supporting reference/agency line is held to one, with the full text
   on hover. Measured: letting both wrap freely gave a four-line cell and a 96px row, so rows
   varied in height by 20% down the table and nothing lined up across columns. */
td .t-main{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
  font-weight:650;color:var(--ink);line-height:1.35}
td .t-sub{display:block;margin-top:3px;font-size:12px;line-height:1.45;color:var(--muted);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* A note that belongs to the value beside it, not a line of its own. */
.score-note{font-size:10px;color:var(--brand);font-weight:700;margin-inline-start:2px;cursor:help}
.tbl-note{margin:12px 12px 0;font-size:11.5px;line-height:1.5;color:var(--muted)}
.score{white-space:nowrap}
/* Numeric columns line up only if the digits are the same width. */
/* A date is one token. DD-MM-YYYY wrapping to "05-08-" / "2026" is not a narrow column, it is
   a broken value — and the hyphens are exactly what the browser treats as break opportunities,
   so every date column does this the moment it is a few pixels tight. */
td.tnum,th.tnum{font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;
  white-space:nowrap}
.tnum{white-space:nowrap}

/* ── tenders · column rhythm ─────────────────────────────────────────────────────────────
   Widths are declared rather than left to the browser's content-based guess, which starved
   the title column (242px, forcing two-line wraps) while giving the two action buttons 248px.
   The title column takes the slack; everything else is sized to its longest real value. */
.tbl-tenders{table-layout:fixed}
.tbl-tenders th:nth-child(1),.tbl-tenders td:nth-child(1){width:auto;min-width:200px}
.tbl-tenders th:nth-child(2),.tbl-tenders td:nth-child(2){width:118px}
.tbl-tenders th:nth-child(3),.tbl-tenders td:nth-child(3){width:92px}
.tbl-tenders th:nth-child(4),.tbl-tenders td:nth-child(4){width:106px}
.tbl-tenders th:nth-child(5),.tbl-tenders td:nth-child(5),
.tbl-tenders th:nth-child(6),.tbl-tenders td:nth-child(6){width:96px}
.tbl-tenders th:nth-child(7),.tbl-tenders td:nth-child(7){width:80px}
.tbl-tenders th:nth-child(8),.tbl-tenders td:nth-child(8){width:252px}
.tbl-tenders td:nth-child(8){white-space:nowrap}
.tbl-tenders .btn.sm{margin-inline-end:6px}
@media (max-width:900px){ .tbl-tenders{table-layout:auto} }


/* ── person detail ───────────────────────────────────────────────────────────────────────
   Reached from a card in the org chart or a row in the list. Identity first, then what they
   are carrying, then what they have done — in that order, because "who is this" has to be
   answered before the numbers underneath it mean anything. */
.pdetail{display:flex;flex-direction:column;gap:16px}
.phead{display:grid;grid-template-columns:auto 1fr;gap:14px 18px;align-items:center}
.phead .pava{width:56px;height:56px;border-radius:50%;display:grid;place-items:center;flex:none;
  background:var(--brand-subtle);color:var(--brand);font-family:var(--font-heading);
  font-weight:700;font-size:18px;letter-spacing:.01em}
.phead-id h2{margin:0;font-size:20px;line-height:1.25;font-weight:700}
.phead-sub{margin-top:4px;font-size:13px;color:var(--muted);display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.phead-fields{grid-column:1/-1;display:grid;gap:12px 22px;
  grid-template-columns:repeat(auto-fit,minmax(190px,1fr));padding-top:14px;border-top:1px solid var(--border)}
.pdfield{min-width:0}
.pdf-l{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
.pdf-v{display:block;margin-top:2px;font-size:13.5px;line-height:1.45;color:var(--ink);
  overflow-wrap:anywhere}
.sec-h{margin:0 0 12px;font-size:14px;font-weight:650;letter-spacing:.01em}
.pstats-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(132px,1fr))}
.pstats-grid .pstat{padding:12px 14px;border:1px solid var(--border);border-radius:var(--r3);background:var(--bg-subtle)}
.pstats-grid .pstat-v{display:block;font-size:19px;font-weight:700;line-height:1.2;color:var(--ink);
  font-variant-numeric:tabular-nums}
.pstats-grid .pstat-l{display:block;margin-top:3px;font-size:11.5px;line-height:1.4;color:var(--muted)}
/* An overdue count of zero is good news and should not be styled as an alarm. */
.pstats-grid .pstat.warn{border-color:var(--danger-border,#f3c2c2);background:var(--danger-subtle,#fdf2f2)}
.pstats-grid .pstat.warn .pstat-v{color:var(--danger,#b42318)}
/* The placeholder mirrors the real tile's height, so the panel does not jump when the numbers
   land — a skeleton that resizes on load is worse than no skeleton. */
@keyframes shimmer{ from{background-position:100% 0} to{background-position:0 0} }
.pstats-grid .pstat.skel{height:66px;background:linear-gradient(90deg,var(--bg-subtle) 25%,var(--bg-hover) 37%,var(--bg-subtle) 63%);
  background-size:400% 100%;animation:shimmer 1.4s ease infinite}
@media (prefers-reduced-motion:reduce){ .pstats-grid .pstat.skel{animation:none} }
.tbl-audit td:first-child{white-space:nowrap;color:var(--muted);font-size:12.5px}
.chg{font-size:12.5px;color:var(--muted)}

/* The name doubles as the way in to a person's record. Styled as text, not a button, because
   it is the row's heading first and a control second. */
/* Padded to a 24px-tall target and pulled back by the same amount, so it is comfortably
   clickable without moving the row a pixel. At its natural 18px line box it was the smallest
   control in the application and the one people reach for most often in this table. */
.pname-link{appearance:none;background:none;border:0;padding:4px 2px;margin:-4px -2px;
  font:inherit;color:inherit;text-align:start;cursor:pointer;border-radius:var(--r2)}
.pname-link:hover{color:var(--brand);text-decoration:underline;text-underline-offset:2px}
.pname-link:focus-visible{outline:2px solid var(--brand);outline-offset:2px}


/* ── offer totals + VAT ──────────────────────────────────────────────────────────────────
   Subtotal and VAT read as working figures; the grand total is the one the client signs
   against, so it carries the weight. Three rows of identical emphasis would make the reader
   do that ranking themselves. */
.offer-total.sub{background:transparent;border:0;padding:6px 14px;font-size:13px;color:var(--muted)}
.offer-total.sub .v{color:var(--ink);font-weight:600}
.offer-total.sub .pill{font-size:10px;padding:1px 6px;margin-inline-start:4px}
.offer-total.grand{margin-top:4px;font-size:15px;font-weight:700}
.offer-total.grand .v{font-size:17px}
.vat-admin{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:14px;
  padding:12px 14px;border:1px solid var(--border);border-radius:var(--r3);background:var(--bg-subtle)}
.vat-admin label{font-size:12px;font-weight:650;color:var(--ink)}
.vat-admin input{width:92px}
.vat-hint{flex:1 1 260px;font-size:11.5px;line-height:1.45;color:var(--muted)}
@media (max-width:680px){ .vat-admin input{width:100%} }

/* Both parties' tax registration, shown on the form — so the numbers are checked before the
   quote is issued rather than after a client's finance team bounces it. */
.parties{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));margin-bottom:14px}
.party{padding:11px 13px;border:1px solid var(--border);border-radius:var(--r3);background:var(--bg-subtle)}
.pt-h{display:block;font-size:10px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}
.pt-n{display:block;margin:3px 0 5px;font-size:13.5px;font-weight:650;color:var(--ink)}
.pt-r{display:block;font-size:11.5px;line-height:1.5;color:var(--muted);font-variant-numeric:tabular-nums}
.vat-admin input[inputmode="numeric"]{width:160px}
@media (max-width:680px){ .vat-admin input[inputmode="numeric"]{width:100%} }


/* ── offer line items: SN · description · qty · cost · price · [discount] · total ─────────
   Cost sits between quantity and price because that is the order the numbers are reasoned
   about, and it is tinted so nobody mistakes an internal figure for something the client sees. */
.li-row{display:grid;gap:8px;align-items:center;padding:8px 10px;border-radius:var(--r3);
  background:var(--bg-subtle);margin-bottom:8px;
  /* SN · description · qty · unit cost · cost total · margin · unit price · [discount] · total · × */
  grid-template-columns:28px minmax(0,1fr) 64px 92px 100px 74px 92px 106px 36px}
.li-row.with-disc{grid-template-columns:28px minmax(0,1fr) 60px 88px 96px 70px 88px 120px 100px 36px}

/* Money and percentages line up on their digits, so a column can be scanned down for an
   outlier. They are text inputs, not type=number: the spinner read as a counter next to a
   riyal amount, and step="0.01" quietly made a four-decimal unit rate invalid — which the
   no-rounding rule exists to prevent. */
.li-num{text-align:end;font-variant-numeric:tabular-nums}
[dir="rtl"] .li-num{text-align:start}

/* Cost total is computed, never typed. It reads as an output: no box, no border, muted. */
.li-derived{display:block;text-align:end;font-variant-numeric:tabular-nums;
  font-size:13px;color:var(--text2);padding-inline-end:2px}
.li-c-num{min-width:0}
.li-sn{font-size:12px;font-weight:700;color:var(--muted);text-align:center;font-variant-numeric:tabular-nums}
.li-cost,.li-margin{background:var(--warn-subtle,#fff8ec);border-color:var(--warn-border,#f0dcc0)}
.li-disc{display:flex;gap:4px}
.li-disc input{width:64px} .li-disc select{width:58px}
.li-total{font-weight:650;text-align:end}
.disc-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:12px 0 6px;
  padding:10px 12px;border:1px solid var(--border);border-radius:var(--r3)}
.disc-bar label{font-size:12px;font-weight:650}
.disc-bar select,.disc-bar input{width:auto;min-width:96px}
/* Internal figures, visually separated from everything the client will see. */
.cost-note{margin-top:10px;padding:8px 12px;border-radius:var(--r3);font-size:12px;
  color:var(--muted);background:var(--warn-subtle,#fff8ec);border:1px solid var(--warn-border,#f0dcc0)}
.cost-note b{color:var(--ink)}
/* A cell wraps each input so the field can carry a label. On a wide screen the labels live in
   the header row instead and each cell is just a box; below 900px the row stacks and every
   cell shows its own. */
.li-c{display:block;min-width:0}
.li-c input{width:100%;box-sizing:border-box}
.li-c::before{content:attr(data-l);display:none;font-size:10px;font-weight:700;
  letter-spacing:.05em;text-transform:uppercase;color:var(--muted);margin-bottom:3px}
.li-c-total{text-align:end;align-self:center}
.li-head{background:none;margin-bottom:2px;padding-block:0}
.li-head span{font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;
  color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.li-head .ta-end{text-align:end}

@media (max-width:900px){
  /* One field per line, each labelled. The row is a small form, not a squeezed table row —
     five boxes cannot share 390px and stay legible or tappable. */
  .li-row,.li-row.with-disc{grid-template-columns:1fr;row-gap:10px;padding:12px}
  .li-head{display:none}
  .li-c::before{display:block}
  .li-sn{text-align:start;font-size:11px}
  .li-c-total{text-align:start}
  .li-c-total .li-total{font-size:17px}
  .li-disc{display:grid;grid-template-columns:1fr 84px;gap:6px}
  .li-disc input,.li-disc select{width:100%}
  .li-x{justify-self:end;min-width:44px}
}

/* ── offer preview ───────────────────────────────────────────────────────────────────────
   The real PDF in an iframe, not a second rendering of the document — see previewOffer(). */
.preview-overlay{position:fixed;inset:0;z-index:60;display:flex;align-items:center;
  justify-content:center;padding:24px;background:rgba(16,24,40,.55);backdrop-filter:blur(2px)}
.preview-panel{display:flex;flex-direction:column;width:min(980px,100%);height:min(92vh,100%);
  background:var(--bg-surface);border-radius:var(--r3);overflow:hidden;
  box-shadow:0 24px 64px rgba(16,24,40,.28)}
.preview-bar{display:flex;align-items:center;justify-content:space-between;gap:12px;
  padding:12px 16px;border-bottom:1px solid var(--border);flex-wrap:wrap}
.preview-title{font-size:12.5px;color:var(--muted)}
.preview-actions{display:flex;gap:8px}
.preview-frame{flex:1;width:100%;border:0;background:#525659}
.preview-loading{flex:1;display:grid;place-items:center;color:var(--muted);font-size:13px}
@media (max-width:680px){ .preview-overlay{padding:0} .preview-panel{height:100%;border-radius:0} }

/* ── Touch targets on phones ────────────────────────────────────────────────────────────────
   A deep-QA sweep at 390px found controls well under the 44px the rest of the app already
   holds to (see the header .btn rule near the top of this file): the in-place role/status
   selects on Team measured 28px, the offer discount select 19px, and the org-chart person
   name 18px. Those sizes are fine under a mouse and wrong under a thumb — 19px in particular
   fails WCAG 2.5.8's 24px floor outright, never mind the 44px this product aims for.

   Two different fixes, because the controls are two different things:
   ·  A form control is a box, so it simply gets taller.
   ·  A NAME is text. Growing it to 44px would wreck the row's typography, so it keeps its size
      and gains an invisible hit area via padding, pulled back with a negative margin so the
      visual rhythm is untouched. Same trick, same numbers as the header button. */
@media (max-width:680px){
  .pfield select,
  .pfield .pstatic,
  select.rolesel,
  #discMode, #discKind, #discValue,
  [data-li-field], [data-li-disc-kind], [data-li-disc-value]{
    min-height:44px;
  }
  /* Vertically centre the text now that the box is taller than its line. */
  .pfield select{padding-block:0}

  .pname-link{
    min-height:44px; display:inline-flex; align-items:center;
    padding-block:13px; margin-block:-13px;   /* hit area grows, layout does not */
  }
}

/* The unambiguous echo of a native date input's value — see the comment at #of_validuntil. */
.fld-echo{display:block;margin-top:4px;font-size:11.5px;font-weight:600;color:var(--text2);
  font-variant-numeric:tabular-nums;min-height:14px}
.fld-echo:empty{display:none}

/* ── client activity log ─────────────────────────────────────────────────────────────────
   A record of what was DONE with a client, on the client itself — no project required. Reads
   as a timeline: the kind is an icon so the eye can sort meetings from payments without
   reading, and the amount sits right-aligned because when it is there it is the point. */
.act-card{border:1px solid var(--border);border-radius:var(--r3);padding:10px 12px;margin-bottom:8px;
  background:var(--bg-surface)}
.act-card.is-editing{border-color:var(--brand);background:var(--brand-subtle)}
.act-head{display:flex;align-items:flex-start;gap:10px}
.act-kind{flex-shrink:0;width:30px;height:30px;border-radius:50%;background:var(--bg-subtle);
  display:grid;place-items:center;font-size:14px;line-height:1}
.act-main{flex:1;min-width:0}
.act-subject{font-weight:650;font-size:13.5px;line-height:1.35;overflow-wrap:anywhere}
.act-meta{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:3px;
  font-size:11.5px;color:var(--muted)}
.act-amount{flex-shrink:0;font-weight:700;font-size:14px;color:var(--ok,var(--brand))}
.act-notes{margin-top:7px;font-size:12.5px;line-height:1.5;color:var(--text2);
  white-space:pre-wrap;overflow-wrap:anywhere}
.act-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:9px}
.act-files{margin-top:10px}
/* The attachment panel is a full card in its own right; inside a row it needs no second frame. */
.act-files .card.panel{margin:0;box-shadow:none;background:var(--bg-subtle)}
/* entityDocsHtml's upload form is laid out for a full-width page — span-3 and span-2 columns
   across a six-column grid. Inside a modal that is ~560px wide the Upload button ran off the
   edge. Here the three controls simply stack. */
.act-files .form{grid-template-columns:1fr!important}
.act-files .form .fld{grid-column:1/-1!important}
.act-files .form button{width:100%}
.act-form{margin-top:4px}
.fld-hint{display:block;margin-top:4px;font-size:11px;color:var(--muted);line-height:1.4}
@media (max-width:680px){
  .act-amount{width:100%;text-align:start;margin-top:4px}
  .act-head{flex-wrap:wrap}
}

/* ── document management ─────────────────────────────────────────────────────────────────
   A result answers "which file is this?" before "what is it called": the owning record leads,
   because somebody searching a document store already knows roughly what they want and needs
   to tell four similarly-named contracts apart.

   The row is ONE target. Clicking anywhere opens the viewer, so the only button is Download —
   the thing the row does not already do — and it stays quiet until the pointer arrives. */

/* Search — the field is the primary action on this screen, so it is sized like one. */
.doc-searchbox{position:relative;display:flex;align-items:center}
.doc-searchbox > svg{position:absolute;inset-inline-start:14px;width:18px;height:18px;
  color:var(--muted);pointer-events:none}
.doc-search input[type=search]{width:100%;font:inherit;font-size:var(--fs-md);
  padding:13px 14px 13px 42px;min-height:48px;border:1px solid var(--border-strong);
  border-radius:var(--r3);background:var(--bg-surface);color:var(--text);
  transition:border-color .12s,box-shadow .12s}
[dir=rtl] .doc-search input[type=search]{padding:13px 42px 13px 14px}
.doc-search input[type=search]::-webkit-search-cancel-button{display:none}
.doc-search input[type=search]:focus{border-color:var(--brand);outline:none;
  box-shadow:0 0 0 3px color-mix(in srgb,var(--brand) 18%,transparent)}
.doc-clear{position:absolute;inset-inline-end:10px;border:0;background:var(--bg-subtle);
  color:var(--text2);width:26px;height:26px;border-radius:50%;cursor:pointer;font-size:17px;
  line-height:1;display:grid;place-items:center}
.doc-clear:hover{background:var(--border-strong);color:var(--text)}

/* Filters — labelled, so each row says what it narrows. */
.fgroup{display:flex;align-items:baseline;gap:12px;margin-top:14px;flex-wrap:wrap}
.flabel{flex-shrink:0;font-size:10px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;
  color:var(--muted);min-width:74px}
.fchips{display:flex;flex-wrap:wrap;gap:6px}
.fchip{border:1px solid var(--border);background:var(--bg-surface);color:var(--text2);
  border-radius:20px;padding:6px 13px;font:inherit;font-size:var(--fs-sm);font-weight:600;
  cursor:pointer;min-height:32px;transition:border-color .12s,background .12s,color .12s}
.fchip:hover{border-color:var(--border-strong);color:var(--text);background:var(--bg-subtle)}
.fchip.on{background:var(--brand);border-color:var(--brand);color:var(--on-brand)}
.fchip:focus-visible{outline:2px solid var(--brand);outline-offset:2px}

.doc-count{font-size:var(--fs-sm);color:var(--muted);margin:18px 2px 10px;font-weight:600}
.doc-list{display:flex;flex-direction:column;gap:10px}

.doc-row{display:flex;gap:14px;align-items:flex-start;padding:14px 16px;cursor:pointer;
  border:1px solid var(--border);border-radius:var(--r);background:var(--bg-surface);
  box-shadow:var(--e1);transition:border-color .14s,box-shadow .14s,transform .14s}
.doc-row:hover{border-color:var(--border-strong);box-shadow:var(--e2);transform:translateY(-1px)}
.doc-row:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
/* Motion explains where a row went; for anyone who has asked not to see it, it does not move. */
@media (prefers-reduced-motion:reduce){ .doc-row{transition:none} .doc-row:hover{transform:none} }

/* The file-type badge. A tinted plate rather than a bare glyph, so type is legible before the
   filename is read — and semantic, so PDF/sheet/image are told apart by hue, not by squinting. */
.doc-ico{flex-shrink:0;width:40px;height:40px;border-radius:var(--r3);display:grid;
  place-items:center;background:var(--bg-subtle);color:var(--text2)}
.doc-ico-svg{width:21px;height:21px}
.doc-ico.is-pdf{background:var(--danger-bg);color:var(--danger)}
.doc-ico.is-sheet{background:var(--ok-bg);color:var(--ok)}
.doc-ico.is-image{background:var(--warn-bg);color:var(--warn)}
.doc-ico.is-doc{background:var(--info-bg);color:var(--info)}
.doc-ico.is-text{background:var(--bg-subtle);color:var(--text2)}

.doc-body{flex:1;min-width:0}
.doc-name{font-family:var(--font-heading);font-weight:650;font-size:var(--fs-md);line-height:1.35;
  overflow-wrap:anywhere;color:var(--text)}
.doc-owner{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-top:5px;
  font-size:var(--fs-sm)}
.doc-owner b{font-weight:650;color:var(--text2)}
.doc-desc{font-size:var(--fs-sm);color:var(--text2);margin-top:5px}

/* The matched passage, as a quotation from the document — a rule and quieter type, not the
   filled grey block it was, which read as a code sample rather than as prose someone wrote. */
.doc-snip{margin:8px 0 0;padding:2px 0 2px 12px;border:0;
  border-inline-start:2px solid var(--brand);
  font-size:var(--fs-sm);line-height:1.6;color:var(--text2);overflow-wrap:anywhere}
[dir=rtl] .doc-snip{padding:2px 12px 2px 0}
.doc-snip.is-none{border-inline-start-color:var(--border-strong);color:var(--muted);font-style:italic}

.doc-meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:8px;
  font-size:11px;color:var(--muted)}
.doc-dot{width:3px;height:3px;border-radius:50%;background:currentColor;opacity:.5}

/* Quiet until wanted: the row's own click is the primary action, so this must not compete. */
.doc-dl{flex-shrink:0;width:36px;height:36px;border-radius:var(--r2);border:1px solid transparent;
  background:none;color:var(--muted);cursor:pointer;display:grid;place-items:center;
  opacity:0;transition:opacity .14s,background .12s,color .12s}
.doc-dl svg{width:18px;height:18px}
.doc-row:hover .doc-dl,.doc-row:focus-within .doc-dl{opacity:1}
.doc-dl:hover{background:var(--brand-subtle);color:var(--brand)}
.doc-dl:focus-visible{opacity:1;outline:2px solid var(--brand);outline-offset:2px}

/* Viewer */
.preview-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;
  padding:14px 18px;border-bottom:1px solid var(--border);background:var(--bg-surface)}
.preview-title{font-family:var(--font-heading);font-weight:700;font-size:var(--fs-md);
  min-width:0;overflow-wrap:anywhere}
.preview-sub{display:block;font-family:var(--font-body);font-weight:500;font-size:var(--fs-sm);
  color:var(--muted);margin-top:3px}
.doc-img-wrap{flex:1;display:grid;place-items:center;background:#525659;overflow:auto;padding:16px}
.doc-img-wrap img{max-width:100%;max-height:100%;object-fit:contain}
.doc-text-wrap{flex:1;overflow:auto;background:var(--bg-surface);padding:20px 22px}
.doc-text-h{font-size:10px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;
  color:var(--muted);margin-bottom:10px}
/* A document reads as prose: a comfortable measure, not the full width of a wide panel. */
.doc-text{white-space:pre-wrap;overflow-wrap:anywhere;font-size:var(--fs-base);line-height:1.7;
  color:var(--text);margin:0;font-family:inherit;max-width:72ch}

@media (max-width:680px){
  .doc-row{gap:12px;padding:13px 14px}
  .flabel{min-width:0}
  .fgroup{gap:8px}
  /* No hover on a touch screen, so the one action must be permanently visible or it is
     invisible — the exact failure mode of a hover-revealed control on a phone. */
  .doc-dl{opacity:1;border-color:var(--border);width:44px;height:44px}
  .fchip{min-height:44px;padding-inline:14px}
}

}

/* A designed dead end: says what happened and offers the one thing that still works. */
.doc-notice{background:var(--warn-bg);color:var(--warn);border:1px solid color-mix(in srgb,var(--warn) 25%,transparent);
  border-radius:var(--r3);padding:10px 13px;font-size:var(--fs-sm);line-height:1.5;margin-bottom:16px}
.doc-empty-view{display:grid;place-items:center;gap:14px;padding:48px 20px;text-align:center}
.doc-empty-h{font-size:var(--fs-base);color:var(--muted);max-width:38ch;line-height:1.6}
.doc-add{margin-bottom:16px;border-color:var(--brand);box-shadow:var(--e2)}

/* ── document table ─────────────────────────────────────────────────────────────────────── */
.dt-filters{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-top:14px}
.dt-stats{display:flex;gap:32px;margin:18px 2px 12px}
.dt-stat b{display:block;font-family:var(--font-heading);font-size:var(--fs-display);line-height:1.1}
.dt-stat span{display:block;font-size:10px;font-weight:700;letter-spacing:.07em;
  text-transform:uppercase;color:var(--muted);margin-top:4px}
.tbl-docs{width:100%;border-collapse:collapse}
.tbl-docs th{text-align:start;font-size:10px;font-weight:700;letter-spacing:.07em;
  text-transform:uppercase;color:var(--muted);padding:12px 14px;border-bottom:1px solid var(--border);
  white-space:nowrap}
.tbl-docs td{padding:14px;border-bottom:1px solid var(--border);vertical-align:top;
  font-size:var(--fs-base)}
.tbl-docs tbody tr:last-child td{border-bottom:0}
.tbl-docs tbody tr:hover{background:var(--bg-hover)}
.dt-file{display:flex;gap:12px;align-items:flex-start}
.dt-fmeta{min-width:0}
.dt-name{font-family:var(--font-heading);font-weight:650;font-size:var(--fs-base);
  overflow-wrap:anywhere;display:flex;align-items:center;gap:7px;flex-wrap:wrap}
.dt-ver{font-family:var(--font-body);font-size:10px;font-weight:700;letter-spacing:.03em;
  background:var(--brand-subtle);color:var(--brand);border-radius:20px;padding:1px 7px}
.dt-desc{font-size:var(--fs-sm);color:var(--text2);margin-top:3px}
.dt-snip{margin-top:6px;padding-inline-start:10px;border-inline-start:2px solid var(--brand);
  font-size:var(--fs-sm);line-height:1.55;color:var(--text2);overflow-wrap:anywhere}
.dt-owner{font-weight:650;color:var(--text2);margin-top:4px;font-size:var(--fs-sm)}
.dt-by{font-size:11px;color:var(--muted);margin-top:3px;white-space:nowrap}
/* A date is one token. "10-08-" on one line and "2026" on the next is not a date the eye can
   read at a glance, and it makes every row in the column a different height. The column is
   sized to its content so the wrap never happens rather than being hidden after it does. */
.tbl-docs td:last-child,.tbl-docs th:last-child{white-space:nowrap;width:1%}
.tbl-docs td:nth-last-child(2),.tbl-docs th:nth-last-child(2){white-space:nowrap;width:1%}
/* An expiry is only worth recording because somebody must act before it. Colour says how soon. */
.dt-expiry{display:inline-block;margin-top:5px;font-size:11px;font-weight:650;padding:2px 8px;
  border-radius:20px;background:var(--bg-subtle);color:var(--text2)}
.dt-expiry.is-soon{background:var(--warn-bg);color:var(--warn)}
.dt-expiry.is-expired{background:var(--danger-bg);color:var(--danger)}
.dt-pager{display:flex;align-items:center;justify-content:space-between;gap:12px;
  margin-top:14px;flex-wrap:wrap}
.dt-showing{font-size:var(--fs-sm);color:var(--muted)}
.dt-pagebtns{display:flex;gap:8px}
@media (max-width:900px){
  /* Stacked cards have the full width, so the nowrap that keeps a date on one line in the
     table would only force the card wider. */
  .tbl-docs td:last-child,.tbl-docs td:nth-last-child(2){white-space:normal;width:auto}
  /* Six columns cannot be read on a phone. The row becomes one block: the file, then what it
     is about, then the rest as a line of meta — the same information, stacked. */
  .tbl-docs thead{display:none}
  .tbl-docs,.tbl-docs tbody,.tbl-docs tr,.tbl-docs td{display:block;width:100%}
  .tbl-docs tr{border:1px solid var(--border);border-radius:var(--r);margin-bottom:10px;
    background:var(--bg-surface);padding:4px 0}
  .tbl-docs td{border-bottom:0;padding:6px 14px}
  .tbl-docs td:empty{display:none}
  .dt-stats{gap:24px}
  .dt-pagebtns .btn{min-height:44px}
}

/* ── team workload ───────────────────────────────────────────────────────────────────────
   Sorted by who is most behind, so the row that needs attention is the first one read. */
.tbl-team{width:100%;border-collapse:collapse}
.tbl-team th{text-align:start;font-size:10px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;
  color:var(--muted);padding:10px 16px;border-bottom:1px solid var(--border);white-space:nowrap}
.tbl-team td{padding:11px 16px;border-bottom:1px solid var(--border);font-size:var(--fs-base)}
.tbl-team tbody tr:last-child td{border-bottom:0}
/* A tint, not a red row: being behind is information, not an error, and a table of red rows
   stops meaning anything. */
.tbl-team tr.tt-behind{background:color-mix(in srgb,var(--danger-bg) 55%,transparent)}
.tt-overdue{font-weight:700;color:var(--danger)}
.tt-chased{font-size:10.5px;color:var(--muted);margin-top:3px}
.tr-log{display:flex;flex-direction:column;gap:8px;margin-bottom:10px}
.tr-row{border-inline-start:2px solid var(--brand);padding:4px 0 4px 10px;font-size:var(--fs-sm);
  color:var(--text2)}
[dir=rtl] .tr-row{padding:4px 10px 4px 0}
.tr-note{margin-top:3px;color:var(--text);white-space:pre-wrap}
@media (max-width:680px){ .tbl-team th,.tbl-team td{padding:9px 12px} }

/* ── the member record ───────────────────────────────────────────────────────────────────
   Opened from a Team workload row, and from the overdue count already filtered to it. */
.tbl-team tr.tt-row{cursor:pointer}
.tbl-team tr.tt-row:hover{background:var(--bg-hover)}
.tbl-team tr.tt-row:focus-visible{outline:2px solid var(--brand);outline-offset:-2px}
/* The count is the affordance — it looks like the number it is, and behaves like the link
   it also is. A button reset, not a new visual language. */
/* A single digit is a 6px-wide target. Padded to a real one — this is the control the whole
   panel exists to offer, and it was the hardest thing on the screen to hit. */
button.tt-overdue{background:none;border:0;padding:3px 8px;margin:-3px -8px;min-width:24px;
  min-height:24px;font:inherit;font-weight:700;color:var(--danger);cursor:pointer;
  text-decoration:underline;text-underline-offset:3px;text-decoration-thickness:1px}
button.tt-overdue:hover{text-decoration-thickness:2px}
button.tt-overdue:focus-visible{outline:2px solid var(--brand);outline-offset:2px;border-radius:3px}

.mr-who{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:var(--fs-sm);color:var(--muted);margin:-4px 0 14px}
/* Flex, not grid: an auto-fit grid leaves a dangling empty cell whenever the number of stats
   does not divide by the column count — which it did, and a hole in a stat strip reads as a
   value that failed to load. Flex lets the last row stretch to fill instead.

   Separated by space rather than by rules. Seven boxed cells inside an already-bordered dialog
   read as a spreadsheet pasted into a card; the numbers are large enough to group themselves. */
.mr-stats{display:flex;flex-wrap:wrap;gap:4px 0;background:var(--bg-subtle);
  border-radius:var(--r3);padding:14px 8px;margin-bottom:16px}
.mr-stat{padding:6px 10px;text-align:center;flex:1 1 92px;min-width:0}
.mr-v{font-size:21px;font-weight:700;line-height:1.15;letter-spacing:-.02em;
  font-variant-numeric:tabular-nums}
.mr-v.bad{color:var(--danger)}
.mr-k{font-size:9.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);
  margin-top:4px;font-weight:600;line-height:1.35}
.mr-seg{margin-bottom:12px}
.mr-list{display:flex;flex-direction:column;gap:8px;max-height:46vh;overflow-y:auto}
.mr-task{border:1px solid var(--border);border-radius:var(--r3);padding:11px 13px;cursor:pointer;
  background:var(--bg-surface);transition:border-color .15s ease,background .15s ease,box-shadow .15s ease}
.mr-task:hover{border-color:var(--border-strong);background:var(--bg-hover);box-shadow:var(--e1)}
.mr-task:focus-visible{outline:2px solid var(--brand);outline-offset:1px}
/* A tint on the start edge rather than a red card: the list is meant to be read, and a wall
   of red says nothing about which of the late ones is worst. */
.mr-task.overdue{border-inline-start:3px solid var(--danger)}
.mr-t1{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.mr-title{font-weight:600;font-size:var(--fs-base)}
.mr-t2{display:flex;gap:6px 14px;flex-wrap:wrap;font-size:var(--fs-sm);color:var(--muted);margin-top:5px}
.mr-late{color:var(--danger);font-weight:600}
.mr-esc{margin-top:6px;font-size:10.5px;color:var(--muted);padding-top:6px;border-top:1px dashed var(--border)}
.task-deleg{font-size:10.5px;color:var(--muted);margin-top:4px}
@media (max-width:680px){
  .mr-stats{padding:12px 4px}
  .mr-stat{flex-basis:76px;padding:6px 6px}
  .mr-v{font-size:18px}
  .mr-list{max-height:none}
}
/* The consequence of letting a deadline slip, said in the app and not only in an email. A
   statement of policy, not an error — so it is quiet until the date it names arrives. */
.task-next-esc{font-size:11px;color:var(--warn);margin-top:4px;font-weight:600}

/* ── segmented control ───────────────────────────────────────────────────────────────────
   `.seg` / `.segb` were used in four places — the org-chart toolbar, its zoom stepper, its
   export pair, and the member record's filters — and had NO CSS at all. They were rendering
   as raw native <button> elements, which is why they read as "very old style": that IS the
   operating system's own button chrome, thirty years of it, sitting inside an otherwise
   designed page.

   The idiom here is a recessed track with the selected option raised onto a surface chip.
   It carries the selection in elevation rather than in a border, so the control reads as one
   object with one thing chosen — not as three buttons that happen to be adjacent. */
.seg{display:inline-flex;align-items:center;gap:2px;padding:3px;border-radius:calc(var(--r2) + 3px);
  background:var(--bg-subtle);border:1px solid var(--border);max-width:100%;overflow-x:auto;
  scrollbar-width:none}
.seg::-webkit-scrollbar{display:none}
.segb{appearance:none;-webkit-appearance:none;border:0;background:transparent;color:var(--text2);
  font:inherit;font-size:var(--fs-sm);font-weight:600;line-height:1;white-space:nowrap;cursor:pointer;
  padding:7px 12px;min-height:30px;border-radius:var(--r2);
  transition:background .15s ease,color .15s ease,box-shadow .15s ease}
.segb:hover:not(:disabled):not(.on){background:var(--bg-hover);color:var(--text)}
.segb:active:not(:disabled){transform:translateY(.5px)}
.segb.on{background:var(--bg-surface);color:var(--text);box-shadow:var(--e1)}
.segb:focus-visible{outline:2px solid var(--brand);outline-offset:1px}
.segb:disabled{opacity:.4;cursor:not-allowed}
/* The zoom stepper's − and + are single glyphs; squaring them stops them reading as short words. */
.seg .segb[data-org-zoom="out"],.seg .segb[data-org-zoom="in"]{
  min-width:30px;padding:7px 0;text-align:center;font-size:15px}
.segb.zlbl{font-variant-numeric:tabular-nums;min-width:52px;text-align:center}
@media (prefers-reduced-motion:reduce){ .segb{transition:none} .segb:active{transform:none} }

/* ── modal ───────────────────────────────────────────────────────────────────────────────
   Refinements on top of the shell at the top of this file, not a replacement for it. */
/* A blurred scrim rather than a flat wash: the page stays legible as context while unmistakably
   receding, which is what makes a dialog feel layered instead of pasted on. */
.modal-backdrop{backdrop-filter:blur(3px) saturate(.9);-webkit-backdrop-filter:blur(3px) saturate(.9);
  animation:modalIn .18s ease-out}
.modal{animation:modalRise .2s cubic-bezier(.2,.8,.3,1)}
@keyframes modalIn{from{opacity:0}to{opacity:1}}
@keyframes modalRise{from{opacity:0;transform:translateY(8px) scale(.985)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion:reduce){ .modal-backdrop,.modal{animation:none} }
/* The header stays put while a long record scrolls under it — losing the name of the person
   whose record you are reading is the fastest way to lose your place in it. */
.modal-head{position:sticky;top:-20px;z-index:1;background:var(--bg-surface);
  padding-top:20px;margin-top:-20px;padding-bottom:12px;margin-bottom:14px;
  border-bottom:1px solid var(--border);align-items:center}
.modal-head h2{font-size:var(--fs-lg);font-weight:700;letter-spacing:-.01em;line-height:1.25}
/* 32px, hit-testable, and visibly a control on hover. A bare glyph with 4px of padding is a
   19px target — under half the 44px minimum, and the one control every dialog needs. */
.modal-close{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;
  padding:0;border-radius:50%;font-size:22px;transition:background .15s ease,color .15s ease}
.modal-close:hover{background:var(--bg-hover);color:var(--text)}
.modal-close:focus-visible{outline:2px solid var(--brand);outline-offset:1px}
/* The count inside a segment: present, subordinate, and never wider than the word it follows. */
.segn{display:inline-block;margin-inline-start:6px;padding:1px 5px;border-radius:99px;
  background:var(--bg-hover);color:var(--muted);font-size:10.5px;font-weight:700;
  font-variant-numeric:tabular-nums;line-height:1.5}
.segb.on .segn{background:var(--brand-subtle);color:var(--brand)}

/* ── date field ──────────────────────────────────────────────────────────────────────────
   A native <input type="date"> renders in the BROWSER's locale — Chrome shows mm/dd/yyyy and
   ignores lang= entirely, and its internals are a closed shadow root. The native control is
   kept for the picker and for its value, and hidden behind a text input that reads DD-MM-YYYY
   like every other date on the platform. See upgradeDateFields() in index.html. */
/* A date field cannot be narrower than a date. DD-MM-YYYY is ten characters plus the calendar
   button, and in a tight grid cell the year was being cut off — "05-06-202" — which is worse
   than a scrollbar because it looks like a value rather than a truncation. Sized in ch so it
   tracks the font rather than assuming one. */
/* 10 characters of date, plus the field's own padding and the calendar button. Expressed that
   way rather than as one number so the text part tracks the font and the chrome part does not. */
.datefield{position:relative;display:block;min-width:calc(10ch + 46px)}
.datefield input.df-text{width:100%;box-sizing:border-box;padding-inline-end:32px;
  font-variant-numeric:tabular-nums}
/* Not display:none — a hidden input cannot open showPicker(), and the field would lose its
   validation. Collapsed under the calendar button instead, where the picker still anchors. */
.datefield .df-native{position:absolute;inset-inline-end:6px;top:50%;transform:translateY(-50%);
  width:26px;height:26px;opacity:0;pointer-events:none;border:0;padding:0;margin:0;background:none}
.datefield .df-cal{position:absolute;inset-inline-end:3px;top:50%;transform:translateY(-50%);
  display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;
  border:0;background:none;color:var(--muted);cursor:pointer;border-radius:var(--r2);
  transition:background .15s ease,color .15s ease}
.datefield .df-cal:hover{background:var(--bg-hover);color:var(--text)}
.datefield .df-cal:focus-visible{outline:2px solid var(--brand);outline-offset:1px}
[dir=rtl] .datefield .df-text{text-align:start}

/* ── the offer form ──────────────────────────────────────────────────────────────────────
   Laid out as the document it becomes: who it is for, what is being sold, what it comes to,
   what the terms are. It was previously one flat card with all of that plus two tenant
   settings interleaved, and no grouping to say which was which. */
.of-form{padding:0;overflow:hidden}
.of-head{display:flex;align-items:center;justify-content:space-between;gap:16px;flex-wrap:wrap;
  padding:16px 20px;border-bottom:1px solid var(--border);background:var(--bg-subtle)}
/* flex-shrink defaults to 1, which crushed "Create offer" to 30px beside a long hint. The
   action is the one thing in this header that cannot give ground. */
.of-head > .btn{flex:none}
.of-head > div{min-width:0}
.of-head .ptitle{font-size:var(--fs-md)}
.of-sub{font-size:var(--fs-sm);color:var(--muted);margin-top:2px}
.of-block{padding:18px 20px;border-bottom:1px solid var(--border)}
.of-block:last-child{border-bottom:0}
/* A section heading, not a card title — smaller and quieter than the form's own, so the
   hierarchy reads in one glance instead of every heading competing. */
.of-h{font-size:11px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;
  color:var(--muted);margin:0 0 12px}
.of-add{margin-top:10px}

/* The summary, right-aligned like the total block on the printed page. */
.of-block-summary{background:var(--bg-subtle)}
.of-summary{margin-inline-start:auto;max-width:420px;background:var(--bg-surface);
  border:1px solid var(--border);border-radius:var(--r3);padding:6px 14px}
.of-row{display:flex;align-items:center;justify-content:space-between;gap:14px;
  padding:9px 0;border-bottom:1px solid var(--border);font-size:var(--fs-base)}
.of-row:last-child{border-bottom:0}
.of-row .l{color:var(--text2)}
.of-row .v{font-weight:600;white-space:nowrap}
.of-off .v{color:var(--danger)}
/* The one figure anybody is looking for. */
.of-grand{padding:12px 0;border-top:2px solid var(--border-strong);border-bottom:0;margin-top:2px}
.of-grand .l{font-weight:700;color:var(--text)}
.of-grand .v{font-size:19px;font-weight:800;letter-spacing:-.01em}
.of-disc-row .v{font-weight:400}
.of-disc{display:flex;gap:6px;justify-content:flex-end;flex-wrap:wrap}
.of-disc select,.of-disc input{min-height:32px;font-size:var(--fs-sm)}
.of-disc input{width:88px;text-align:end}
.of-hint{font-size:var(--fs-sm);color:var(--muted);text-align:end;padding-bottom:8px}
/* Internal-only: cost and margin never reach the client's document. Set apart so nobody
   mistakes it for part of the quotation. */
.of-margin{margin-top:10px;padding:9px 11px;border-radius:var(--r2);background:var(--bg-subtle);
  border:1px dashed var(--border-strong);font-size:var(--fs-sm);color:var(--muted)}
.of-margin b{color:var(--text)}

/* Tenant configuration, collapsed. Set once, then never — it has no business sitting in the
   middle of a form somebody fills in several times a day. */
.of-settings{padding:14px 20px;background:var(--bg-subtle);border-top:1px solid var(--border)}
.of-settings > summary{cursor:pointer;font-size:var(--fs-sm);font-weight:600;color:var(--text2);
  list-style:none;display:flex;align-items:center;gap:7px;padding:4px 0}
.of-settings > summary::-webkit-details-marker{display:none}
.of-settings > summary::before{content:'▸';font-size:10px;transition:transform .15s ease;color:var(--muted)}
.of-settings[open] > summary::before{transform:rotate(90deg)}
.of-settings > summary:hover{color:var(--text)}
.of-settings > summary:focus-visible{outline:2px solid var(--brand);outline-offset:2px;border-radius:var(--r2)}
@media (max-width:680px){
  .of-head{padding:14px}
  .of-head .btn{width:100%}
  .of-block,.of-settings{padding:14px}
  .of-summary{max-width:none}
}
@media (prefers-reduced-motion:reduce){ .of-settings > summary::before{transition:none} }
/* The priced rows, inside the redesigned form: a table, not a strip of boxed inputs. The row
   carries the surface and the fields sit flush in it, so the eye follows columns rather than
   counting rectangles. */
.of-form .li-row{background:var(--bg-surface);border:1px solid var(--border);padding:7px 10px}
.of-form .li-row:hover{border-color:var(--border-strong)}
.of-form .li-row:focus-within{border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-subtle)}
.of-form .li-c input,.of-form .li-c select{border-color:transparent;background:var(--bg-subtle);
  min-height:34px}
.of-form .li-c input:hover,.of-form .li-c select:hover{border-color:var(--border)}
.of-form .li-c input:focus,.of-form .li-c select:focus{border-color:var(--brand);
  background:var(--bg-surface);outline:none}
/* The unit COST is internal and never printed; the tint is the only thing stopping somebody
   typing it into the unit PRICE box beside it. */
.of-form .li-cost,.of-form .li-margin{background:var(--warn-bg) !important}
.of-form .li-head{padding-inline:10px}
/* A remove control, not a danger button — deleting an empty row you just added is not a
   destructive act, and dressing it in red made every row look like a warning. */
.of-form .li-x{background:none;border:0;color:var(--muted);font-size:17px;line-height:1;
  min-width:28px;min-height:28px;border-radius:var(--r2);padding:0}
.of-form .li-x:hover:not(:disabled){background:var(--danger-bg);color:var(--danger)}
.of-form .li-x:disabled{opacity:.3}

/* ── a button is never narrower than its label ───────────────────────────────────────────
   .form lays out on repeat(6, minmax(0,1fr)). The 0 floor is deliberate and correct for text
   and inputs — it stops one long unbroken word blowing the whole grid out — but it applies to
   every child, and a button in one of those tracks was cut to "ave", "eate" and "Add membe".
   Truncated prose is still readable; a truncated verb is a different word.

   Fixed at the CELL, not by letting the button overflow it, so the row still lays out instead
   of the control sliding under its neighbour. */
.btn{white-space:nowrap}
.fld:has(> .btn:only-child){min-width:max-content}
.form > .btn{min-width:max-content}
/* The calendar glyph sits in a fixed square by design; its natural width includes padding it
   does not need, so it is exempt from the rule above. */
.datefield .df-cal{min-width:0}

/* ── forms adapt to the width they actually get ───────────────────────────────────────────
   .form was repeat(6, minmax(0,1fr)). Six tracks is right in a full-width card and absurd in a
   392px one — each track is 50px there, so a text input renders 111px wide, overflows its
   track, and paints straight over the button beside it. That is how "Create" came to read
   "eate" behind the Deadline field, and "Save" to read "ave".

   auto-fit rather than a container query. The first attempt put container-type on the card, and
   that has to be withheld from cards holding a wide table — containment turns their horizontal
   scroll into a clip, and the custody list lost its last column to it. Which excluded the
   per-project Tasks card, the very card in the bug report, because its task list IS a table.
   auto-fit needs no container: it fits as many 150px columns as the real width allows, so a
   narrow card gets two and a wide one gets six, and tables are untouched.

   `grid-column: span 2` in the existing markup still means "twice a normal field", which is
   what it was always for. */
.form{grid-template-columns:repeat(auto-fit,minmax(150px,1fr))}

/* ── composition bar ─────────────────────────────────────────────────────────────────────
   Replaces five stacked tracks, each drawn to one row's share of the total. That encoding
   was wrong twice: the empty part of every track meant nothing, and five near-identical
   rounded rectangles gave the eye no composition to read — which is what the data IS.

   One bar, segments summing to the whole, numbers in a legend where they can be compared as
   a list. Same shape as the Task health widget's .sbar, so the app has one visual language
   for "parts of a whole" rather than two. */
.comp{display:block}
.comp-bar{display:flex;height:26px;border-radius:var(--r2);overflow:hidden;background:var(--bg-subtle);
  box-shadow:inset 0 0 0 1px var(--border)}
.comp-seg{transition:width .45s cubic-bezier(.2,0,0,1);min-width:2px}
.comp-seg:hover{filter:brightness(1.08)}
/* One family, ordered light-to-dark by rank — these are shares of a single total, not
   unrelated categories, and six unrelated hues would imply a distinction that is not there.
   Rank already carries the meaning, so lightness carries the encoding. */
.t1{background:#5b3fd1} .t2{background:#7358e0} .t3{background:#8b74ea}
.t4{background:#a494f0} .t5{background:#c0b5f5} .t6{background:#d8d1f9}
:root[data-theme="dark"] .t1{background:#a78bfa} :root[data-theme="dark"] .t2{background:#9575f0}
:root[data-theme="dark"] .t3{background:#8361e3} :root[data-theme="dark"] .t4{background:#7050d2}
:root[data-theme="dark"] .t5{background:#5e41bd} :root[data-theme="dark"] .t6{background:#4d34a3}

.comp-legend{margin-top:12px;display:flex;flex-direction:column;gap:1px}
.comp-row{display:grid;grid-template-columns:12px minmax(0,1fr) auto auto;align-items:center;
  gap:10px;padding:7px 2px;border-bottom:1px solid var(--border);font-size:var(--fs-base)}
.comp-row:last-child{border-bottom:0}
.comp-legend .ldot{width:10px;height:10px;border-radius:3px;flex:none}
.comp-name{color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.comp-val{font-weight:600;white-space:nowrap}
/* The share is secondary to the amount — the bar above already shows the proportion. */
.comp-pct{color:var(--muted);min-width:52px;text-align:end;white-space:nowrap;font-size:var(--fs-sm)}
@media (prefers-reduced-motion:reduce){ .comp-seg{transition:none} }

/* The raw failure text on the boot-error card. Monospace and scrollable because it exists to be
   copied into a message to an administrator, not read as prose — and it must not stretch the
   card or the page when the message is a long stack trace. */
.err-detail{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:var(--fs-sm);
  line-height:1.6;white-space:pre-wrap;word-break:break-word;background:var(--bg-subtle);
  color:var(--text2);border:1px solid var(--border);border-radius:var(--r2);
  padding:10px 12px;margin:12px 0;max-height:180px;overflow:auto}

/* Line-item toolbar: add on the start edge, the Excel round-trip on the end edge. Wraps rather
   than overflowing, because Arabic labels here run about a third longer. */
.li-tools{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:4px}
.li-tools-end{display:flex;align-items:center;gap:8px;margin-inline-start:auto;flex-wrap:wrap}

/* What the last import did. Neutral when everything came through, warm when rows were refused —
   the count alone is easy to read past. */
.li-import-report{position:relative;margin-top:12px;padding:12px 34px 12px 14px;
  border:1px solid var(--border);border-radius:var(--r2);background:var(--bg-subtle);font-size:13px}
[dir="rtl"] .li-import-report{padding:12px 14px 12px 34px}
.li-import-report.has-problems{background:var(--warn-bg);border-color:var(--warn)}
.li-import-head{font-weight:650}
.li-import-sub{margin-top:6px;color:var(--text2)}
.li-import-list{margin:6px 0 0;padding-inline-start:18px;color:var(--text2);line-height:1.7}
.li-import-x{position:absolute;inset-inline-end:6px;top:6px;width:26px;height:26px;
  display:flex;align-items:center;justify-content:center;background:none;border:0;
  color:var(--muted);font-size:18px;cursor:pointer;border-radius:var(--r2)}
.li-import-x:hover{color:var(--text);background:var(--bg-hover)}

/* A deadline row: time remaining first, because that is the thing being decided on — a calendar
   date has to be subtracted from today before it means anything. */
.dl-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:10px;align-items:center;
  padding:7px 0;border-bottom:1px solid var(--border);font-size:13px}
.dl-row:last-child{border-bottom:0}
.dl-when{font-weight:700;font-size:12px;white-space:nowrap;font-variant-numeric:tabular-nums;
  color:var(--text2);min-width:62px}
.dl-when.soon{color:var(--warn)}
.dl-when.over{color:var(--danger)}
.dl-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dl-date{color:var(--muted);font-size:12px;white-space:nowrap}

/* The sign-in deadline summary. Capped and scrollable: forty overdue items is a real state, and
   a dialog taller than the window has no reachable dismiss button. */
.dl-modal{max-width:520px}
.dl-sub{margin:0 0 12px;color:var(--text2);font-size:13px}
.dl-list{max-height:46vh;overflow-y:auto}
.dl-more{padding-top:8px;color:var(--muted);font-size:12px}
.dl-actions{display:flex;justify-content:flex-end;margin-top:16px}
