/* ════════════════════════════════════════════════════════════════════════════
   mannbot-ui.css — MannBot shared design system (M-S33)

   THE SINGLE SOURCE OF TRUTH FOR DESIGN TOKENS.

   Why this file exists
   --------------------
   Until M-S33 the "brand system" was prose in CLAUDE.md and eleven private
   copies of a token block, one per module HTML file. M-S32 proved the cost:
   scheduler.html used var(--warm-black) and var(--mann-orange) without ever
   declaring them, so `background: var(--warm-black)` was invalid at
   computed-value time, fell back to `transparent`, and the scheduler's header
   band had no background at all — in either theme, for several sessions, with
   nothing logged. Patching that file locally fixed the symptom and left the
   cause.

   From M-S33 forward, tokens are declared HERE and nowhere else. Module files
   consume them and may declare only genuinely module-PRIVATE properties (e.g.
   the scheduler's runtime sticky offsets). scripts/check_css_tokens.py enforces
   both halves: a var() with no declaration here fails, and a module
   re-declaring a token declared here fails.

   Served like mannbot.js (U-S44 precedent) and loaded by a module BEFORE its
   own inline <style>, so a module can still override a shared component rule at
   equal specificity when it genuinely needs to.

   Design direction: B "Field Office" — light SaaS chrome + warm-black icon
   rail. Picked by Jordan 18-Aug-2026 from the three-direction mockup; token
   values are ported from that mockup's html[data-dir="b"] blocks with the
   direction scoping stripped, because there is only one direction now.

   Typography: Lato (headings/UI titles, Black 900 for hero numbers and the
   wordmark) + Poppins (body/labels/data), self-hosted from /static/fonts/.
   Bebas Neue and DM Sans retire as modules are migrated. NOTE: the brand
   folder ships no Lato SemiBold, so anything the mockup drew at Lato 600 is
   set at 700 here rather than letting the browser synthesise a fake weight.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── FONTS ──────────────────────────────────────────────────────────────────
   Self-hosted rather than Google Fonts: prod must not depend on a third-party
   CDN's uptime for legible chrome. WOFF2, converted from the brand-folder TTFs
   with fonttools (7 faces, ~276 KB total). SIL OFL licences ship beside them
   in /static/fonts/. */
@font-face{font-family:'Lato';font-style:normal;font-weight:400;font-display:swap;src:url('/static/fonts/Lato-Regular.woff2') format('woff2')}
@font-face{font-family:'Lato';font-style:normal;font-weight:700;font-display:swap;src:url('/static/fonts/Lato-Bold.woff2') format('woff2')}
@font-face{font-family:'Lato';font-style:normal;font-weight:900;font-display:swap;src:url('/static/fonts/Lato-Black.woff2') format('woff2')}
@font-face{font-family:'Poppins';font-style:normal;font-weight:400;font-display:swap;src:url('/static/fonts/Poppins-Regular.woff2') format('woff2')}
@font-face{font-family:'Poppins';font-style:normal;font-weight:500;font-display:swap;src:url('/static/fonts/Poppins-Medium.woff2') format('woff2')}
@font-face{font-family:'Poppins';font-style:normal;font-weight:600;font-display:swap;src:url('/static/fonts/Poppins-SemiBold.woff2') format('woff2')}
@font-face{font-family:'Poppins';font-style:normal;font-weight:700;font-display:swap;src:url('/static/fonts/Poppins-Bold.woff2') format('woff2')}

/* ── TOKENS · LIGHT (the default) ───────────────────────────────────────────
   Every name here is stable across themes; only the values change below. */
:root{
  /* type */
  --font-head:'Lato',sans-serif;
  --font-body:'Poppins',sans-serif;

  /* spacing scale */
  --sp-1:4px; --sp-2:8px; --sp-3:12px; --sp-4:16px; --sp-5:20px; --sp-6:24px;

  /* surfaces */
  --bg:#F4F5F7; --surface:#FFFFFF; --surface2:#F8F9FB; --surface3:#EEF0F3;
  /* lines */
  --border:#E3E6EB; --border-soft:#EDEFF3;
  /* ink
     M-S44 contrast pass. The M-S45b gates measured three chrome controls below
     WCAG AA in LIGHT mode, all on shared tokens and therefore on every migrated
     module: .appbar-search 2.75:1, the KPI active tier pill 3.77:1, and the
     .mb-nav-prev breadcrumb 3.06:1. Fixed at the TOKEN, not per control — a
     per-control patch would have left the next consumer of the same token to
     rediscover it, which is exactly how these three arrived.
     Standard hit: text-bearing chrome >= 4.5:1, non-text indicators >= 3:1,
     BOTH themes, measured against the deepest surface each token is drawn on
     (including the composited .appbar-search chip, which is bar-text at 7% over
     bar-bg and is darker than the bar itself — the surface that made the naive
     value look passable). --text-muted 2.64 -> 5.38 on white, 4.71 on the
     search chip and surface3. */
  --text:#20221F; --text-dim:#5F6368; --text-muted:#656B73;
  /* brand
     --accent stays the BRAND FILL — a background with #fff on top, a pairing
     that was never the problem. --accent-ink (M-S44) is brand-coloured TEXT on
     a light surface, which is a different job with a different floor: the
     active tier pill draws its label in this, not in --accent. Splitting them
     is what lets the fill keep its brand value while the ink clears AA. */
  --accent:#C96939; --accent-hover:#B55C30; --accent-dim:rgba(201,105,57,.09);
  --accent-ink:#A9522A;
  /* app bar + rail — --bar-dim 3.22 -> 5.28 on the bar, 4.62 on the search chip */
  --bar-bg:#FFFFFF; --bar-text:#20221F; --bar-dim:#686C72; --bar-rule:#E3E6EB;
  --rail-bg:#1C1B1A; --rail-dim:rgba(255,255,255,.45);
  /* semantic */
  --good:#25855A; --warn:#AD7A0B; --bad:#C23A3A; --info:#3468C0;
  --today:#C96939;
  /* shape */
  --r-card:14px; --r-ctl:9px; --r-chip:999px;
  /* elevation */
  --sh-card:0 1px 2px rgba(16,24,40,.04),0 6px 24px rgba(16,24,40,.05);
  --sh-pop:0 12px 32px rgba(16,24,40,.14);
  /* density */
  --row-h:46px; --row-pad-v:8px; --row-pad-h:12px; --th-pad-v:10px;

  /* ── COMPANY ACCENTS (Brand Guide 2026) ──────────────────────────────────
     Company identity is NEVER colour-alone: every chip carries its code as
     text. ASD's #BAC133 fails contrast on light surfaces (dataviz palette
     validator, 18-Aug), so light mode uses a darkened variant; dark mode keeps
     the brand value. RGE and VDS blues are only ΔE≈9 apart — another reason
     the text code is load-bearing, not decorative. */
  --co-dme:#C96939; --co-rge:#2D5EA2; --co-asd:#8A9426; --co-vds:#107EB1;
  --co-mog:#C96939;

  /* ── Z-LADDER (M-S29, made structural in M-S31, moved here M-S33) ────────
     ONE source of truth for stacking. Read these; never hard-code a competing
     number, and never "fix" a clipped popover by bumping a bar.
     Two standing rules learned the hard way:
       (1) Page content can never outrank the bars — the page wrapper carries
           `isolation: isolate`, confining every z-index inside it.
       (2) A popover must LEAVE THE DOM, not just the layout. position:fixed
           escapes layout but not the stacking context, so a menu declared
           inside a sticky bar can never paint above a higher bar whatever its
           own z-index. Re-parent it to <body> on open. */
  --z-bar:     100;   /* app bar */
  --z-subnav:   95;   /* tab row */
  --z-toolbar:  90;   /* board toolbar */
  --z-thead:    80;   /* sticky table head */
  --z-modal:   300;
  --z-popover:1000;

  /* ── LEGACY ALIASES (M-S33 bridge) ───────────────────────────────────────
     Modules built before M-S33 reference these names at hundreds of sites.
     Renaming every call site in one session would be a large, purely
     mechanical diff with real regression risk, so the old names are declared
     HERE — as aliases onto the new tokens, not as a second palette. That keeps
     the single-source-of-truth property intact (there is still exactly one
     file where a colour is decided) while letting modules migrate their call
     sites at their own pace in M-S34+. Delete an alias only once no module
     uses it. */
  --mann-orange:  var(--accent);
  --warm-black:   #1C1B1A;
  --border-light: var(--border-soft);
  --red:          var(--bad);
  --green:        var(--good);
  --blue:         var(--info);
  --row-hover:    var(--accent-dim);
  --gantt-today:  var(--today);
  --shadow-sm:    0 1px 2px rgba(16,24,40,.05);
  --shadow-md:    var(--sh-card);
  --shadow-lg:    var(--sh-pop);
  --radius:       var(--r-ctl);
  --radius-sm:    6px;
  /* Pre-M-S33 ladder names, aliased so untouched modules keep their stacking. */
  --z-header:  var(--z-bar);
  --z-kpi:      92;
}

/* ── TOKENS · DARK ─────────────────────────────────────────────────────────
   Only the values that actually change are restated; everything else inherits
   from :root above. */
[data-theme="dark"]{
  --bg:#15171A; --surface:#1C1F23; --surface2:#22262B; --surface3:#2A2F35;
  --border:#343A42; --border-soft:#2A2F36;
  /* M-S44 contrast pass, dark half. M-S45b reported dark as clean, and it was
     — for the three controls it measured, which used --bar-dim. --text-muted
     was NOT among them and sat at 3.51:1 on the bar and 3.23:1 on surface2, so
     ratcheting the gate to 4.5 without moving it would have turned a passing
     suite red on a token nobody had looked at. Same floors, same surfaces. */
  --text:#E8EAED; --text-dim:#A3A9B1; --text-muted:#959BA3;
  --accent:#D97B48; --accent-hover:#E28A58; --accent-dim:rgba(217,123,72,.14);
  --accent-ink:#DD7D49;
  --bar-bg:#1C1F23; --bar-text:#E8EAED; --bar-dim:#90969E; --bar-rule:#343A42;
  --rail-bg:#101214; --rail-dim:rgba(255,255,255,.4);
  --good:#41B278; --warn:#D3A032; --bad:#E05C5C; --info:#6BA3E8;
  --today:#D97B48;
  --sh-card:0 1px 3px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.32);
  --sh-pop:0 12px 32px rgba(0,0,0,.55);
  /* ASD returns to the brand green on dark surfaces; RGE/VDS lift for contrast. */
  --co-asd:#BAC133; --co-rge:#5B87C9; --co-vds:#3FA3D4;
  /* legacy aliases whose literal values differ in dark */
  --warm-black:#111110;
  --shadow-sm:0 1px 3px rgba(0,0,0,.3);
}

/* ── ICONS ─────────────────────────────────────────────────────────────────
   Sprite symbols live in mannbot-icons.svg, injected into <body> by
   mannbot.js. Usage: <svg class="ic"><use href="#i-cal"/></svg>
   Symbols carry geometry only — size, stroke and colour are decided here, so
   an icon always matches the text it sits beside. */
.ic{width:16px;height:16px;flex:none;stroke:currentColor;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;vertical-align:-3px}
.ic-s{width:13px;height:13px}
.ic-l{width:20px;height:20px}
.ic-xl{width:24px;height:24px}

/* ── APP SHELL ─────────────────────────────────────────────────────────────
   Direction B wraps the page in a fixed icon rail + a scrolling main column. */
.shell{display:flex;min-height:100vh}
.main-col{flex:1;min-width:0}
.rail{
  display:flex;flex-direction:column;align-items:center;gap:6px;
  width:64px;flex:none;background:var(--rail-bg);padding:14px 0;
  position:sticky;top:0;height:100vh;z-index:calc(var(--z-bar) + 1);
}
.rail-logo{width:36px;height:36px;margin-bottom:14px}
.rail-btn{
  width:44px;height:44px;border-radius:12px;
  display:flex;align-items:center;justify-content:center;
  color:var(--rail-dim);position:relative;transition:all .15s;
  background:none;border:none;cursor:pointer;text-decoration:none;
}
.rail-btn:hover{color:#fff;background:rgba(255,255,255,.07)}
.rail-btn.active{color:#fff;background:var(--accent)}
.rail-tip{
  position:absolute;left:54px;top:50%;transform:translateY(-50%);
  background:#0E0D0C;color:#fff;font-size:11px;font-weight:500;
  padding:4px 10px;border-radius:6px;white-space:nowrap;
  opacity:0;pointer-events:none;transition:opacity .12s;z-index:50;
}
.rail-btn:hover .rail-tip{opacity:1}
.rail-spacer{flex:1}

/* ── APP BAR ───────────────────────────────────────────────────────────────
   54px, sticky, one row: brand · module crumb · search · theme · user. */
.appbar{
  background:var(--bar-bg);color:var(--bar-text);
  border-bottom:1px solid var(--bar-rule);box-shadow:0 1px 2px rgba(16,24,40,.04);
  height:54px;padding:0 var(--sp-5);
  display:flex;align-items:center;gap:var(--sp-4);
  position:sticky;top:0;z-index:var(--z-bar);
}
.brand{display:flex;align-items:center;gap:10px;text-decoration:none;color:inherit}
.brand-mark{width:30px;height:30px}
.brand-word{font-family:var(--font-head);font-size:18px;font-weight:900;letter-spacing:-.01em;line-height:1}
.brand-word b{color:var(--accent-ink,var(--accent));font-weight:900}
.brand-word span{font-weight:400;opacity:.92}
.appbar-module{
  display:flex;align-items:center;gap:8px;
  font-family:var(--font-head);font-weight:700;font-size:13.5px;letter-spacing:.01em;
  padding-left:var(--sp-4);
  border-left:1px solid color-mix(in srgb,var(--bar-dim) 30%,transparent);
  color:var(--bar-text);
}
.appbar-module .crumb{color:var(--bar-dim);font-weight:400}
.appbar-spacer{flex:1}
.appbar-search{
  display:flex;align-items:center;gap:8px;
  background:color-mix(in srgb,var(--bar-text) 7%,transparent);
  border:1px solid color-mix(in srgb,var(--bar-text) 12%,transparent);
  color:var(--bar-dim);border-radius:var(--r-ctl);
  height:32px;padding:0 12px;width:220px;font-size:12px;cursor:pointer;
}
.appbar-search:hover{color:var(--text);border-color:var(--border)}
.appbar-search kbd{
  margin-left:auto;font-family:var(--font-body);font-size:10px;
  border:1px solid color-mix(in srgb,var(--bar-text) 18%,transparent);
  border-radius:4px;padding:0 5px;opacity:.8;
}
.appbar-btn{
  display:flex;align-items:center;justify-content:center;gap:6px;
  height:32px;min-width:32px;padding:0 10px;border-radius:var(--r-ctl);
  color:var(--bar-dim);border:1px solid transparent;background:none;cursor:pointer;
  transition:all .15s;font-size:12px;font-weight:500;font-family:inherit;
}
.appbar-btn:hover{color:var(--bar-text);background:color-mix(in srgb,var(--bar-text) 8%,transparent)}
.user-chip{
  display:flex;align-items:center;gap:8px;height:34px;padding:0 10px 0 4px;
  border-radius:999px;background:none;cursor:pointer;font-family:inherit;
  border:1px solid color-mix(in srgb,var(--bar-text) 14%,transparent);
  color:var(--bar-text);font-weight:500;font-size:12px;
}
.user-chip:hover{background:color-mix(in srgb,var(--bar-text) 8%,transparent)}
.avatar{
  width:26px;height:26px;border-radius:50%;background:var(--accent);color:#fff;
  display:flex;align-items:center;justify-content:center;flex:none;
  font-family:var(--font-head);font-weight:700;font-size:11px;
}

/* ── TAB ROW ───────────────────────────────────────────────────────────────
   Direction B: pill tabs on the page background, no rule underneath. */
.tabrow{
  background:transparent;padding:var(--sp-3) var(--sp-5) 0;
  display:flex;align-items:flex-end;gap:2px;flex-wrap:wrap;
  position:sticky;top:54px;z-index:var(--z-subnav);
}
.tab{
  display:flex;align-items:center;gap:7px;
  font-family:var(--font-head);font-weight:700;font-size:13px;color:var(--text-dim);
  border:1px solid transparent;border-radius:999px;padding:7px 14px;margin-bottom:6px;
  background:none;cursor:pointer;transition:all .15s;white-space:nowrap;
}
.tab:hover{color:var(--text);background:var(--accent-dim)}
.tab.active{background:var(--surface);border-color:var(--border);color:var(--accent-ink,var(--accent));box-shadow:var(--sh-card)}
.tab .badge{
  font-family:var(--font-body);font-size:10px;font-weight:600;
  min-width:16px;height:16px;padding:0 4px;
  display:flex;align-items:center;justify-content:center;
  border-radius:999px;background:var(--accent);color:#fff;
}

/* ── COMPANY SCOPE BAR ─────────────────────────────────────────────────────
   Rendered ONLY for MOG/executive sessions. This is a VIEW filter layered on
   top of the security boundary, never a substitute for it: a single-company
   user's session company_code already scopes every query server-side and they
   never see this row. Do not add a company parameter to an API call to make
   these chips work. */
.scope-bar{display:flex;align-items:center;gap:10px;padding:12px 20px 0;flex-wrap:wrap}
.scope-label{
  font-size:10.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;
  color:var(--text-muted);display:flex;align-items:center;gap:6px;
}
.scope-chips{display:flex;gap:6px;flex-wrap:wrap}
.scope-chip{
  display:inline-flex;align-items:center;gap:7px;height:29px;padding:0 13px;
  border-radius:999px;border:1px solid var(--border);background:var(--surface);
  font-size:12px;font-weight:600;color:var(--text-dim);font-family:inherit;
  box-shadow:var(--sh-card);transition:all .13s;cursor:pointer;
}
.scope-chip:hover{border-color:var(--accent);color:var(--accent-ink,var(--accent))}
.scope-chip .dot{width:7px;height:7px;border-radius:50%;background:var(--co,var(--text-muted))}
.scope-chip.on{background:var(--accent);border-color:var(--accent);color:#fff}
.scope-chip.on .dot{background:#fff}
.scope-note{margin-left:auto;font-size:11px;color:var(--text-muted)}

/* ── KPI STRIP ─────────────────────────────────────────────────────────────
   Scrolls away with the page (M-S33): the plan data, not the KPIs, is what a
   scheduling meeting needs pinned. */
.kpi-strip{display:flex;align-items:stretch;gap:var(--sp-3);padding:var(--sp-4) var(--sp-5) 0;flex-wrap:wrap}
.kpi-tile{
  background:var(--surface);border:1px solid var(--border);border-radius:var(--r-card);
  box-shadow:var(--sh-card);padding:12px 16px;min-width:180px;
  display:flex;flex-direction:column;gap:2px;position:relative;
}
.kpi-label{
  font-size:10.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;
  color:var(--text-muted);display:flex;align-items:center;gap:5px;
}
.kpi-label .ic{width:12px;height:12px;stroke-width:2}
.kpi-value{
  font-family:var(--font-head);font-weight:900;font-size:26px;line-height:1.15;
  letter-spacing:-.01em;font-variant-numeric:tabular-nums;color:var(--text);
}
.kpi-value.good{color:var(--good)}
.kpi-value.info{color:var(--info)}
.kpi-value.accent{color:var(--accent-ink,var(--accent))}
.kpi-sub{font-size:11px;color:var(--text-dim)}
.kpi-sub b{color:var(--text);font-weight:600}
.kpi-flexspacer{flex:1}
.statset{display:flex;align-items:center;gap:var(--sp-2);align-self:center;flex-wrap:wrap;justify-content:flex-end}
.stat-pill{
  display:flex;align-items:center;gap:7px;height:30px;padding:0 12px;
  background:var(--surface);border:1px solid var(--border);border-radius:999px;
  font-size:12px;color:var(--text-dim);box-shadow:var(--sh-card);
}
.stat-pill b,.stat-pill strong{color:var(--text);font-weight:600;font-variant-numeric:tabular-nums}
.stat-dot{width:7px;height:7px;border-radius:50%;flex:none}
.stat-dot.muted{background:var(--text-muted)}
.stat-dot.accent{background:var(--accent)}
.stat-dot.good{background:var(--good)}
.alert-chip{
  display:flex;align-items:center;gap:7px;height:30px;padding:0 12px;border-radius:999px;
  background:color-mix(in srgb,var(--bad) 10%,var(--surface));
  border:1px solid color-mix(in srgb,var(--bad) 35%,var(--border));
  color:var(--bad);font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;
}

/* ── BOARD CARD ────────────────────────────────────────────────────────────
   Toolbar and table live in ONE card, so the sticky stack is app bar / tab row
   / (toolbar + thead) — three layers, not five. overflow stays `visible`:
   an overflow-x wrapper creates an implicit scroll container and silently
   breaks position:sticky on the head. */
.board{
  margin:var(--sp-4) var(--sp-5) 90px;
  background:var(--surface);border:1px solid var(--border);border-radius:var(--r-card);
  box-shadow:var(--sh-card);overflow:visible;
}
.toolbar{
  display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap;
  padding:var(--sp-3) var(--sp-4);
  border-bottom:1px solid var(--border-soft);
  border-radius:var(--r-card) var(--r-card) 0 0;
  background:var(--surface);
  position:sticky;top:calc(54px + 50px);z-index:var(--z-toolbar);
}
.toolbar-div{width:1px;height:22px;background:var(--border);margin:0 4px}
.toolbar-spacer{flex:1}
.search{
  display:flex;align-items:center;gap:8px;height:34px;padding:0 12px;width:250px;
  background:var(--surface2);border:1px solid var(--border);border-radius:var(--r-ctl);
  color:var(--text-muted);font-size:12.5px;
}
.search:focus-within{border-color:var(--accent)}
.search input{border:none;background:transparent;outline:none;color:var(--text);font-family:inherit;font-size:12.5px;width:100%}

/* ── CONTROLS ──────────────────────────────────────────────────────────────*/
.btn{
  display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 13px;
  border-radius:var(--r-ctl);border:1px solid var(--border);background:var(--surface);
  font-family:var(--font-body);font-size:12.5px;font-weight:500;color:var(--text-dim);
  transition:all .15s;white-space:nowrap;cursor:pointer;
}
.btn:hover{color:var(--text);border-color:color-mix(in srgb,var(--text) 25%,var(--border));background:var(--surface2)}
.btn.active{border-color:var(--accent);color:var(--accent-ink,var(--accent));background:var(--accent-dim)}
.btn.primary{
  background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600;
  box-shadow:0 2px 8px color-mix(in srgb,var(--accent) 35%,transparent);
}
.btn.primary:hover{background:var(--accent-hover);border-color:var(--accent-hover)}
.btn.danger{color:var(--bad);border-color:color-mix(in srgb,var(--bad) 40%,var(--border))}
.btn.danger:hover{background:color-mix(in srgb,var(--bad) 8%,transparent);border-color:var(--bad);color:var(--bad)}
.btn.sm{height:28px;padding:0 10px;font-size:11.5px}
.btn:disabled{opacity:.5;cursor:not-allowed}
.seg{
  display:flex;height:34px;background:var(--surface2);border:1px solid var(--border);
  border-radius:var(--r-ctl);padding:3px;gap:2px;
}
.seg button{
  display:flex;align-items:center;gap:6px;padding:0 11px;
  border-radius:calc(var(--r-ctl) - 3px);border:none;background:none;cursor:pointer;
  font-family:inherit;font-size:12px;font-weight:500;color:var(--text-dim);transition:all .12s;
}
.seg button:hover{color:var(--text)}
.seg button.active{background:var(--surface);color:var(--accent-ink,var(--accent));font-weight:600;box-shadow:var(--sh-card)}

/* ── PERSON-CHIP SHELF ─────────────────────────────────────────────────────*/
.shelf{
  display:flex;align-items:center;gap:var(--sp-2);flex-wrap:wrap;
  padding:10px var(--sp-4);border-bottom:1px solid var(--border-soft);background:var(--surface2);
}
.shelf-title{
  font-family:var(--font-head);font-weight:700;font-size:12px;letter-spacing:.02em;
  color:var(--text-dim);display:flex;align-items:center;gap:7px;margin-right:4px;
}
.shelf-count{
  min-width:18px;height:18px;padding:0 5px;border-radius:999px;background:var(--accent);color:#fff;
  font-size:10.5px;font-weight:600;display:inline-flex;align-items:center;justify-content:center;
}
.shelf-note{font-weight:400;color:var(--text-muted)}
.person-chip{
  display:inline-flex;align-items:center;gap:7px;height:28px;padding:0 11px 0 6px;
  background:var(--surface);border:1px solid var(--border);border-radius:999px;
  font-size:12px;color:var(--text);cursor:grab;transition:all .15s;box-shadow:var(--sh-card);
}
.person-chip:hover{border-color:var(--accent);color:var(--accent-ink,var(--accent))}
.person-chip .avatar{width:18px;height:18px;font-size:8.5px}
.person-chip .role{color:var(--text-muted);font-size:11px}
.person-chip.more{border-style:dashed;color:var(--text-muted);cursor:pointer}

/* ── CHIPS & BADGES ────────────────────────────────────────────────────────*/
.co-chip{
  display:inline-flex;align-items:center;gap:5px;height:20px;padding:0 8px;
  border-radius:var(--r-chip);font-size:10px;font-weight:700;letter-spacing:.05em;
  background:color-mix(in srgb,var(--co) 12%,var(--surface));
  color:var(--co);border:1px solid color-mix(in srgb,var(--co) 30%,transparent);
}
.co-chip::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--co)}
.co-chip.dme{--co:var(--co-dme)} .co-chip.rge{--co:var(--co-rge)}
.co-chip.asd{--co:var(--co-asd)} .co-chip.vds{--co:var(--co-vds)}
.co-chip.mog{--co:var(--co-mog)}
.plan-chip{
  display:inline-flex;align-items:center;height:19px;padding:0 8px;border-radius:var(--r-chip);
  font-size:10px;font-weight:600;letter-spacing:.04em;
}
.plan-chip.firm{color:var(--text-muted);border:1px solid var(--border)}
.plan-chip.pipeline{
  color:var(--info);border:1px dashed color-mix(in srgb,var(--info) 55%,transparent);
  background:color-mix(in srgb,var(--info) 8%,transparent);
}
.role-badge{
  display:inline-flex;align-items:center;height:21px;padding:0 9px;border-radius:var(--r-chip);
  font-size:11px;font-weight:500;color:var(--text-dim);
  background:var(--surface2);border:1px solid var(--border-soft);
}
.crew-badge{
  display:inline-flex;align-items:center;gap:5px;height:21px;padding:0 9px;border-radius:var(--r-chip);
  font-size:11px;font-weight:600;color:var(--accent-ink,var(--accent));background:var(--accent-dim);
}
.warn-badge{
  display:inline-flex;align-items:center;gap:4px;height:19px;padding:0 7px;border-radius:var(--r-chip);
  font-size:10px;font-weight:600;color:var(--bad);
  background:color-mix(in srgb,var(--bad) 10%,transparent);
  border:1px solid color-mix(in srgb,var(--bad) 30%,transparent);
}
.warn-badge.warn{
  color:var(--warn);background:color-mix(in srgb,var(--warn) 10%,transparent);
  border-color:color-mix(in srgb,var(--warn) 30%,transparent);
}
.warn-badge.info{
  color:var(--info);background:color-mix(in srgb,var(--info) 10%,transparent);
  border-color:color-mix(in srgb,var(--info) 30%,transparent);
}

/* ── BASIS CHIP (M-S51 · docs/180 §2.2) ────────────────────────────────────
   Every curve coefficient, OOP, zone boundary and cost-per-hour the Fleet
   Management module shows carries a VISIBLE basis label. No page shows a
   number whose basis is unstated. The vocabulary is M-S43's, adopted exactly:

     actual      measured from a source of record
     modelled    computed from a fitted curve or a stand-in
     calibrated  modelled, then adjusted against observed points
     legacy      imported before the basis field existed; provenance unknown
     declared    a judgment value someone SET — not derived from anything
                 (R2: the five age-zone boundaries are declared per class)
     unruled     a real figure whose DENOMINATOR has no owner ruling behind it
                 (R6: the four non-DME entities have no ruled denominators)

   `declared` and `unruled` are visually distinct from `modelled` on purpose.
   Their deficiency is not precision — a declared boundary is exact, it is
   simply somebody's judgment; an unruled figure is correctly computed against
   a denominator nobody has signed. Painting either as "modelled" would say the
   arithmetic is soft when what is soft is the authority.

   The default (no modifier) is the neutral outline used for `actual`. The
   hatched swatch marks every basis that is NOT a measurement, so the
   distinction survives greyscale, a projector, and colour-blind vision.

   This is a SHARED component. kpi.html keeps its own `.kpi-t-basis` — that is
   an M-S43 ruled page and I-56 does not let it move without an owner ruling. */
.basis{
  display:inline-flex;align-items:center;gap:5px;
  font-size:10px;font-weight:700;line-height:1;white-space:nowrap;
  border-radius:var(--r-chip);padding:3px 8px;
  border:1px solid var(--border);color:var(--text-dim);background:var(--surface2);
}
.basis i{
  width:10px;height:10px;border-radius:2px;display:inline-block;flex:none;
  background:currentColor;
}
.basis.modelled,.basis.calibrated{
  color:var(--warn);background:color-mix(in srgb,var(--warn) 8%,transparent);
  border-color:color-mix(in srgb,var(--warn) 32%,transparent);
}
.basis.declared{
  color:var(--info);background:color-mix(in srgb,var(--info) 8%,transparent);
  border-color:color-mix(in srgb,var(--info) 32%,transparent);
}
.basis.unruled{
  color:var(--bad);background:color-mix(in srgb,var(--bad) 8%,transparent);
  border-color:color-mix(in srgb,var(--bad) 32%,transparent);
}
.basis.legacy{ color:var(--text-muted); }
/* Anything that is not a measurement gets the hatch. */
.basis.modelled i,.basis.calibrated i,.basis.declared i,.basis.unruled i,.basis.legacy i{
  background:repeating-linear-gradient(135deg,currentColor 0 2px,transparent 2px 4px);
}
/* Coverage rides the chip rather than sitting loose beside it — §2.2 asks for
   the basis AND the coverage figure, and separating them is how one travels
   without the other. */
.basis .cov{ font-weight:400;opacity:.8; }
/* Status pill wraps the real <select> — appearance only, no behaviour change. */
.status-pill{
  display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 11px;
  border-radius:var(--r-chip);font-size:11.5px;font-weight:600;
  border:1px solid var(--border);background:var(--surface);cursor:pointer;position:relative;
}
.status-pill .dot{width:7px;height:7px;border-radius:50%;flex:none}
.status-pill.active{color:var(--good);border-color:color-mix(in srgb,var(--good) 40%,var(--border))}
.status-pill.active .dot{background:var(--good)}
.status-pill.onhold{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 40%,var(--border))}
.status-pill.onhold .dot{background:var(--warn)}
.status-pill.complete{color:var(--text-muted)}
.status-pill.complete .dot{background:var(--text-muted)}
.status-pill .ic-s{opacity:.45}
/* The <select> sits invisibly over the pill so the native picker still opens
   and setStatus() keeps firing on change. */
.status-pill select{
  position:absolute;inset:0;width:100%;height:100%;
  opacity:0;cursor:pointer;font-family:inherit;border:none;
}

/* ── TABLE BASE ────────────────────────────────────────────────────────────*/
.board table{width:100%;border-collapse:collapse;font-size:12.5px}
.board thead th{
  position:sticky;top:calc(54px + 50px + 61px);z-index:var(--z-thead);
  background:var(--surface);
  font-family:var(--font-head);font-weight:700;font-size:10.5px;
  letter-spacing:.08em;text-transform:uppercase;color:var(--text-muted);
  text-align:left;padding:10px 12px;border-bottom:1px solid var(--border);
  white-space:nowrap;cursor:pointer;user-select:none;
}
.board thead th:hover{color:var(--text)}
.board thead th.no-sort{cursor:default}
.board thead th.no-sort:hover{color:var(--text-muted)}
.board thead th .ic-s{opacity:.4;margin-left:2px}
.board tbody td{
  height:var(--row-h);padding:6px 12px;border-bottom:1px solid var(--border-soft);
  vertical-align:middle;white-space:nowrap;font-variant-numeric:tabular-nums;
}
.board tbody tr{transition:background .1s}
.board tbody tr:hover{background:var(--accent-dim)}
.board tbody tr:last-child td{border-bottom:none}
.td-num{font-weight:600;color:var(--accent-ink,var(--accent));font-size:11.5px}
.td-dim{color:var(--text-dim)}
.td-muted{color:var(--text-muted)}
.proj-cell{display:flex;align-items:center;gap:8px;min-width:0}
.proj-name{font-weight:600;color:var(--text);cursor:pointer}
.proj-name:hover{color:var(--accent-ink,var(--accent));text-decoration:underline;text-underline-offset:3px}
.emp-cell{display:flex;align-items:center;gap:8px}
.emp-cell .avatar{width:24px;height:24px;font-size:9.5px;background:var(--surface3);color:var(--text-dim)}
.emp-name{font-weight:500}
.avatar.vacant{background:transparent;border:1.5px dashed var(--bad);color:var(--bad)}

/* ── GANTT ─────────────────────────────────────────────────────────────────*/
.gantt-th{min-width:340px}
.gantt-months{display:flex;font-size:10px;letter-spacing:.06em;color:var(--text-muted)}
.gantt-months span{flex:1;border-left:1px solid var(--border-soft);padding-left:6px}
.gantt-cell{position:relative;min-width:340px;padding:6px 0 !important}
.gantt-track{position:relative;height:26px;width:100%}
.gantt-grid-line{position:absolute;top:0;bottom:0;width:1px;background:var(--border-soft);pointer-events:none}
.gantt-today{
  position:absolute;top:-7px;bottom:-7px;width:2px;border-radius:2px;
  background:var(--today);opacity:.85;z-index:2;pointer-events:none;
}
.gbar{
  position:absolute;top:5px;height:16px;border-radius:5px;z-index:1;min-width:4px;
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.06);transition:opacity .15s;
}
.gbar .glabel{
  position:absolute;left:8px;top:50%;transform:translateY(-50%);
  font-size:9.5px;font-weight:600;color:#fff;letter-spacing:.02em;white-space:nowrap;
  text-shadow:0 1px 1px rgba(0,0,0,.25);pointer-events:none;
  /* M-S44: clip the label TO its bar. Found while measuring the TV mode at
     1920×1080 — two labels were already spilling past their bar's right edge at
     the NORMAL 9.5px size, where the overflowing text lands on top of the
     following weeks and reads as a label on the wrong bar. It is the same
     defect the TV scale-up makes obvious, so it is fixed once, here, for every
     size. max-width resolves against .gbar, the positioned ancestor. */
  max-width:calc(100% - 16px);overflow:hidden;text-overflow:ellipsis;
}
/* M-S44: bar fills go through their own tokens so the TV presentation mode can
   deepen them (white label on an accent fill is only 3.05:1 in dark) without
   moving the brand colour anywhere else. Default values are the brand tokens —
   nothing changes until data-tv is set. */
.gbar.current{background:var(--gbar-current,var(--accent))}
.gbar.past{background:var(--gbar-past,var(--text-muted));opacity:.55}
.gbar.future{background:var(--gbar-future,var(--info));opacity:.85}
.gbar.onhold{background:var(--gbar-onhold,var(--warn));opacity:.8}
.gbar.complete{background:transparent;box-shadow:inset 0 0 0 1.5px var(--border);opacity:.8}
.gbar.complete .glabel{color:var(--text-muted);text-shadow:none}
.gbar.vacant{
  background:transparent;box-shadow:none;
  border:1.5px dashed color-mix(in srgb,var(--bad) 65%,transparent);
}
.gbar.vacant .glabel{color:var(--bad);text-shadow:none}
.gbar.pipeline{
  background:repeating-linear-gradient(45deg,
    color-mix(in srgb,var(--info) 55%,transparent) 0 5px,
    color-mix(in srgb,var(--info) 25%,transparent) 5px 10px);
}

/* ── VIEW PRESETS (Board · Timeline · Meeting) ─────────────────────────────
   One segmented control sets data-view on <html>; CSS does the rest. Board is
   the full data grid, Timeline collapses to one row per project with maximum
   gantt, Meeting is Timeline at TV scale for the Friday meeting and the shop
   screens. */
html[data-view="timeline"] .view-board,
html[data-view="meeting"]  .view-board{display:none}
.view-timeline{display:none}
html[data-view="timeline"] .view-timeline,
html[data-view="meeting"]  .view-timeline{display:table}
html[data-view="meeting"] .kpi-strip,
html[data-view="meeting"] .shelf,
html[data-view="meeting"] .scope-bar{display:none !important}

/* Two fixed lines, never more. Letting the project line wrap pushed the
   foreman sub-line onto a third row, which made the label column taller than
   the gantt cell beside it and visually detached every bar from its project. */
.tl-cell{width:440px;max-width:440px;white-space:nowrap !important;overflow:hidden}
.tl-proj{display:flex;align-items:center;gap:8px;font-weight:600;min-width:0;flex-wrap:nowrap;overflow:hidden}
.tl-proj .proj-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.tl-proj > :not(.proj-name){flex:none}
.tl-chev{flex:none;opacity:.5;transition:transform .15s;cursor:pointer}
.tl-chev.closed{transform:rotate(-90deg)}
.tl-chev-spacer{display:inline-block;width:13px;flex:none}
.tl-sub{display:flex;align-items:center;gap:7px;color:var(--text-dim);font-size:11.5px;margin-top:3px;padding-left:21px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tl-sub .avatar{width:17px;height:17px;font-size:8px;background:var(--surface3);color:var(--text-dim)}
.tl-sub b{color:var(--text);font-weight:600}
tr.tl-row td{height:58px;border-bottom:1px solid var(--border-soft)}
tr.tl-member td{height:36px;background:var(--surface2);border-bottom:1px solid var(--border-soft)}
tr.tl-member .tl-mname{display:flex;align-items:center;gap:8px;padding-left:42px;font-size:12px;color:var(--text-dim)}
tr.tl-member .tl-mname .avatar{width:19px;height:19px;font-size:8.5px;background:var(--surface3);color:var(--text-dim)}
tr.tl-member.vacant .tl-mname{color:var(--bad)}
tr.tl-member .gbar{height:12px;top:8px;border-radius:4px}
tr.tl-member .gbar .glabel{font-size:9px}
tr.tl-more td{height:30px;background:var(--surface2);color:var(--text-muted);font-size:11px;padding-left:42px;cursor:pointer;border-bottom:1px solid var(--border-soft)}
tr.tl-more td:hover{color:var(--accent-ink,var(--accent))}

/* Meeting = TV scale. 1080p at distance has far fewer effective pixels than a
   laptop at arm's length, so the fix is size, not density. */
html[data-view="meeting"] .view-timeline{font-size:14px}
html[data-view="meeting"] .tl-cell{width:500px;max-width:500px}
html[data-view="meeting"] tr.tl-row td{height:66px}
html[data-view="meeting"] .tl-sub{font-size:12.5px}
html[data-view="meeting"] .gantt-track{height:32px}
html[data-view="meeting"] tr.tl-row .gbar{height:20px;top:6px}
html[data-view="meeting"] tr.tl-row .gbar .glabel{font-size:11px}
html[data-view="meeting"] .gantt-months{font-size:11.5px;font-weight:700}

/* M-S44 — TV mode's share of the timeline. Lives beside the Meeting preset it
   sits next to, and NOT in scheduler.html, because .tl-* are shared components.
   The identity column has to grow with the type or it just clips sooner: at
   1.4× the 440px cell truncated the crew/role/date sub-line mid-word, which is
   the one line telling you WHO is on the job. 440 → 616 is the same 1.4.
   These win over the Meeting rules by source order, which is correct — TV is
   the more specific intent, and the two compose (Meeting + TV is a normal
   thing to want on a Friday). */
html[data-tv="1"] .tl-cell{width:616px;max-width:616px}
html[data-tv="1"] .tl-sub{font-size:16px}
html[data-tv="1"] tr.tl-row td{height:74px}
html[data-tv="1"] .gantt-track{height:42px}
html[data-tv="1"] .gantt-months{font-size:14px;font-weight:700}
html[data-tv="1"] .view-timeline{font-size:17.5px}

/* ── DETAIL DRAWER ─────────────────────────────────────────────────────────
   Right-side panel holding the reference fields that left the default column
   set (address, PM, super, notes). Read-only presentation over the data the
   edit modal already loaded; its Edit button opens that modal. */
.drawer{
  position:fixed;top:0;right:0;bottom:0;width:380px;max-width:92vw;
  background:var(--surface);border-left:1px solid var(--border);
  box-shadow:var(--sh-pop);z-index:var(--z-modal);
  transform:translateX(100%);transition:transform .2s ease;
  display:flex;flex-direction:column;
}
.drawer.open{transform:none}
.drawer-head{
  display:flex;align-items:flex-start;gap:10px;padding:16px 18px;
  border-bottom:1px solid var(--border);
}
.drawer-title{font-family:var(--font-head);font-weight:700;font-size:15px;line-height:1.3;flex:1;min-width:0}
.drawer-close{background:none;border:none;cursor:pointer;color:var(--text-muted);padding:2px;flex:none}
.drawer-close:hover{color:var(--text)}
.drawer-body{padding:16px 18px;overflow-y:auto;flex:1;display:flex;flex-direction:column;gap:14px}
.drawer-field{display:flex;flex-direction:column;gap:3px}
.drawer-label{
  font-size:10px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--text-muted);
}
.drawer-value{font-size:13px;color:var(--text);white-space:pre-wrap;word-break:break-word}
.drawer-value.empty{color:var(--text-muted);font-style:italic}
.drawer-foot{padding:14px 18px;border-top:1px solid var(--border);display:flex;gap:8px}

/* ── SCROLLBAR ─────────────────────────────────────────────────────────────*/
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:var(--surface2)}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:5px}
::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}

/* ── PRINT ─────────────────────────────────────────────────────────────────
   The scheduler's print/PDF path renders into its own self-contained iframe
   document, so this only has to stop the on-screen chrome from reaching paper
   if a module ever prints the live page directly. */
@media print{
  .rail,.appbar,.tabrow,.toolbar,.scope-bar,.shelf,.drawer{display:none !important}
  .board{margin:0;border:none;box-shadow:none}
}


/* ═══════════════════════════════════════════════════════════════════════════
   M-S44 — TV PRESENTATION MODE (:root[data-tv="1"])
   ═══════════════════════════════════════════════════════════════════════════
   For the boardroom screen at 3–4 m, where the reader cannot lean in and cannot
   click. The layout half lives in the module that opts in (scheduler.html); the
   COLOUR half has to live here, because mannbot-ui.css is the only place a
   token is decided and a module re-declaring one is a checker FAIL, not a
   style choice.

   What changes and why:
   - Secondary ink goes to near-primary. On a screen that far away, mid-grey on
     dark is the first thing to disappear — the muted/dim distinction is doing
     typographic work at 60 cm that becomes illegibility at 3 m. Both roles land
     above 10:1 here, well past AA, because AA is a minimum for a reader at
     arm's length and this reader is not.
   - Bar fills deepen. The gantt label is white on the fill, which is 3.77:1 in
     light and 3.05:1 in dark at brand values — the single most-read text on the
     board sitting at the bottom of the contrast range. Deepened to clear 4.5:1
     against white in both themes.
   Nothing here applies unless a module sets data-tv="1" on the root element. */
:root[data-tv="1"]{
  --text-muted:#3A3D42;          /* 10.90:1 on surface */
  --text-dim:#2E3135;            /* 13.07:1 on surface */
  --bar-dim:#2E3135;
  --gbar-current:#B35D33;        /* white label 4.64:1 */
  --gbar-past:#656B73;           /* white label 5.38:1 */
  --gbar-future:#3468C0;         /* white label 5.40:1 — already clear */
  --gbar-onhold:#986B0A;         /* white label 4.73:1 */
  /* Density. --row-h is already the density token every board row measures
     against, so taller rows are a token change, not a per-table override. */
  --row-h:64px; --row-pad-v:12px; --th-pad-v:14px;
  /* The type scale the layout half multiplies by. One number, one place. */
  --tv-scale:1.4;
}
/* Explicit dark only. The `:not([data-theme="light"])` half of the house style
   belongs INSIDE the prefers-color-scheme block below and nowhere else — with no
   data-theme attribute set (the default "system" state) a bare
   :not([data-theme="light"]) matches in LIGHT mode too, which is how the first
   cut of this block shipped dark ink onto a white board. Caught by measuring
   the computed value rather than by reading the selector. */
:root[data-tv="1"][data-theme="dark"]{
  --text-muted:#D8DBE0;          /* 11.92:1 on surface */
  --text-dim:#E2E5E9;            /* 13.09:1 on surface */
  --bar-dim:#E2E5E9;
  --gbar-current:#AB6139;        /* white label 4.67:1 */
  --gbar-past:#70747A;           /* white label 4.70:1 */
}
@media (prefers-color-scheme: dark){
  :root[data-tv="1"]:not([data-theme="light"]){
    --text-muted:#D8DBE0; --text-dim:#E2E5E9; --bar-dim:#E2E5E9;
    --gbar-current:#AB6139; --gbar-past:#70747A;
  }
}
