/* ══════════════════════════════════════════════════════════════════════
   Lake Cement Ltd — DESIGN SYSTEM  |  design-system.css
   ──────────────────────────────────────────────────────────────────────
   Loaded LAST in every <head>, after styles.css and after each page's own
   inline <style> block. That load order is deliberate: it means the rules
   here settle equal-specificity ties instead of losing them, so a single
   file can hold the line on legibility, touch size and responsive shell
   across all 28 pages without editing 28 files.

   What lives here:
     1. TOKENS        one spacing scale, one type scale, one chart palette
     2. FOUNDATION    focus rings, motion, selection, scroll behaviour
     3. RESPONSIVE    the mobile shell — drawer, app bar, bottom tab bar
     4. LEGIBILITY    the mobile type floor (nothing below 12px)
     5. TOUCH         44px minimum targets on coarse pointers
     6. TABLES        scroll affordance + card fallback for wide tables

   What does NOT live here: page-specific layout. That stays with its page.

   Design doctrine (see docs/DESIGN_SYSTEM.md for the full rationale):
     · Few          — maximise data-ink; colour carries meaning, never decoration
     · Knaflic      — one focal point per view; grey is the default, colour is the exception
     · Wathan/Schoger — hierarchy from weight and colour before size; spacing from a scale
     · Johnson      — legibility floors, Gestalt grouping, never colour alone
     · Tidwell      — use the known pattern (off-canvas drawer, bottom tabs, disclosure)
   ══════════════════════════════════════════════════════════════════════ */

/* ══════════════════════════════════════════════════════════════════════
   1. TOKENS
   ══════════════════════════════════════════════════════════════════════ */
:root {
  /* ── Spacing — 4px base. Every gap/padding in new work uses these. ── */
  --sp-1:   4px;
  --sp-2:   8px;
  --sp-3:  12px;
  --sp-4:  16px;
  --sp-5:  20px;
  --sp-6:  24px;
  --sp-8:  32px;
  --sp-10: 40px;
  --sp-12: 48px;
  --sp-16: 64px;

  /* ── Type scale ──────────────────────────────────────────────────
     Desktop is a dense analytical surface and density is correct there
     (Few); mobile raises the floor because the plant floor is bright,
     the device is at arm's length, and the readers are not 22. The
     --fs-* values below are the DESKTOP scale; the mobile block in §4
     lifts the bottom of it.

     12px is the hard floor and it is enforced at the source, not by
     override: a codemod raised 1,845 declarations across the HTML, CSS
     and JS that were between 7.5px and 11.5px. Below ~12px, size stops
     working as a hierarchy cue anyway (Johnson) — weight and colour
     carry it instead, which is why merging the old 8/9/10/11px tiers
     into one cost no legible structure. Nothing may go below --fs-xs;
     e2e/responsive-audit.js fails the build if anything does. */
  --fs-xs:     12px;   /* THE FLOOR — badges, axis ticks, dense cells  */
  --fs-sm:     13px;   /* secondary labels, captions                   */
  --fs-base:   14px;   /* body copy, data cells                        */
  --fs-md:     15px;   /* emphasised body                              */
  --fs-lg:     17px;   /* card titles                                  */
  --fs-xl:     20px;   /* section titles                               */
  --fs-2xl:    25px;   /* KPI tile values                              */
  --fs-3xl:    31px;   /* hero number                                  */
  --fs-4xl:    39px;   /* single headline figure                       */

  --lh-tight:  1.2;
  --lh-snug:   1.35;
  --lh-normal: 1.5;

  --fw-normal:   400;
  --fw-medium:   500;
  --fw-semibold: 600;
  --fw-bold:     700;
  --fw-heavy:    800;

  /* Tabular figures — every number in a data app must align vertically.
     Applied to values via .num / [data-num] and to table cells in §6. */
  --font-num: 'Inter', system-ui, sans-serif;

  /* ── Radii & elevation ──────────────────────────────────────────── */
  --r-sm:   6px;
  --r-md:   9px;
  --r-lg:  14px;
  --r-pill: 999px;

  --el-1: 0 1px 2px rgba(16,24,40,.06), 0 1px 3px rgba(16,24,40,.10);
  --el-2: 0 2px 4px rgba(16,24,40,.06), 0 4px 12px rgba(16,24,40,.08);
  --el-3: 0 4px 8px rgba(16,24,40,.06), 0 12px 28px rgba(16,24,40,.12);
  --el-4: 0 8px 16px rgba(16,24,40,.08), 0 24px 48px rgba(16,24,40,.16);

  /* ── Motion — one duration set, one easing. ─────────────────────── */
  --dur-fast: 120ms;
  --dur-med:  200ms;
  --dur-slow: 300ms;
  --ease:     cubic-bezier(.4, 0, .2, 1);
  --ease-out: cubic-bezier(0, 0, .2, 1);

  /* ── Touch ──────────────────────────────────────────────────────── */
  --tap:      44px;   /* WCAG 2.2 AA target size (min 24, 44 is the humane floor) */
  --tap-cozy: 40px;   /* inside dense data grids only                              */

  /* ── Status — reserved. Never reused as a chart series colour. ──── */
  /* Was #157F4B (4.9:1 on white) but only 4.48:1 on --st-good-bg, the
     mint fill .n-chip.is-good and other status chips actually paint text
     on — axe caught it as a serious violation once a KPI/report status
     chip rendered "Approved" in real data (2026-09-15, surfaced by the
     Production review page's own a11y run; the same .n-chip.is-good
     component on dispatch.html had the identical pre-existing defect,
     just not yet hit by a page whose e2e audit rendered that status).
     #0F7A41 is 5.41:1 on white and 4.81:1 on --st-good-bg — both clear
     of the 4.5:1 AA floor. */
  --st-good:      #0F7A41;   /* 5.4:1 on white, 4.8:1 on --st-good-bg */
  --st-good-bg:   #E7F5ED;
  --st-good-bd:   #A8DCC1;
  --st-warn:      #9A6100;   /* 4.6:1 on white                       */
  --st-warn-bg:   #FDF3E2;
  --st-warn-bd:   #F0CE92;
  --st-bad:       #C0271F;   /* 5.6:1 on white                       */
  --st-bad-bg:    #FDECEA;
  --st-bad-bd:    #F3B4AE;
  --st-neutral:   #5A6270;
  --st-neutral-bg:#F1F3F7;
  --st-neutral-bd:#DDE3EC;

  /* ── Chart palette ───────────────────────────────────────────────
     Nyati-anchored categorical set. Slot 1 is the brand navy stepped
     into a usable chart blue; slot 2 is the brand orange stepped up in
     chroma so it survives colour-vision deficiency next to slot 4.

     VALIDATED — do not re-order and do not add a 9th hue by hand.
       node scripts/validate_palette.js "<the 8 hexes>" --mode light
       → all checks pass. Worst adjacent CVD ΔE 9.1 (protan, ≥8 target);
         worst adjacent normal-vision ΔE 19.6 (≥15 floor).
     Slots 3, 4 and 5 fall below 3:1 against a white surface, so any
     chart using them owes the relief rule: visible direct labels or a
     table view. Past 3 series in a scatter/bubble form, fold to
     "Other" or facet — the all-pairs floors do not clear at 4+. */
  --series-1: #1F5FA8;   /* blue    — Nyati navy family */
  --series-2: #EB6834;   /* orange  — Nyati orange family */
  --series-3: #1BAF7A;   /* aqua   */
  --series-4: #EDA100;   /* yellow */
  --series-5: #E87BA4;   /* magenta */
  --series-6: #008300;   /* green  */
  --series-7: #4A3AA7;   /* violet */
  --series-8: #E34948;   /* red    */

  /* Sequential — one hue, light→dark (magnitude only, never identity) */
  --seq-1: #EAF1F9;
  --seq-2: #C5D8EE;
  --seq-3: #93B6DC;
  --seq-4: #5B8FC7;
  --seq-5: #2F6BAE;
  --seq-6: #1F5FA8;
  --seq-7: #14406F;

  /* Diverging — two poles + a NEUTRAL GREY midpoint (never a hue) */
  --div-neg-3: #A32E24;
  --div-neg-2: #D8604F;
  --div-neg-1: #F0A99C;
  --div-mid:   #EFEFEF;
  --div-pos-1: #A5D9BF;
  --div-pos-2: #4FA97C;
  --div-pos-3: #14713F;

  /* Chart chrome — recessive by construction */
  --chart-grid:    #EDF0F5;
  --chart-axis:    #C8CDD5;
  --chart-surface: #FFFFFF;
  --chart-ink:     #111827;
  --chart-ink-2:   #5A6270;

  /* ── Shell metrics ──────────────────────────────────────────────── */
  --appbar-h:     56px;
  --bottomnav-h:  60px;
  --drawer-w:     min(86vw, 320px);
  --safe-b:       env(safe-area-inset-bottom, 0px);
  --safe-t:       env(safe-area-inset-top, 0px);

  --z-scrim:     1150;
  --z-drawer:    1200;
  --z-bottomnav: 1100;
  --z-toast:     2400;
}

/* ══════════════════════════════════════════════════════════════════════
   2. FOUNDATION
   ══════════════════════════════════════════════════════════════════════ */

/* The `hidden` attribute has to actually hide.
   The UA sheet says `[hidden] { display: none }` at the lowest possible
   specificity, so ANY class rule that sets a display — and this app sets
   `display: flex` on most of its section headers, rows and strips —
   silently beats it. Hiding a section with el.hidden left its heading on
   screen, which is how the sales failure state ended up showing a clean
   error panel above seven captions for content that was not coming. */
[hidden] { display: none !important; }

/* Focus — the app had no visible focus ring, which fails WCAG 2.4.7 and
   makes the whole thing keyboard-hostile. :focus-visible keeps it off for
   mouse users, so nothing looks noisier than before for existing staff. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 2px solid var(--orange, #F49545);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
/* Dark surfaces (sidebar, header) need a light ring to be seen at all. */
.sidebar :where(a, button, [tabindex]):focus-visible,
.top-header :where(a, button, [tabindex]):focus-visible,
.ms-bottomnav :where(a, button):focus-visible {
  outline-color: #FFFFFF;
  outline-offset: -2px;
}

/* Skip link — first tab stop on every page. */
.ms-skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 3000;
  background: var(--navy, #173158);
  color: #fff;
  padding: var(--sp-4) var(--sp-5);
  min-height: var(--tap);
  border-radius: 0 0 var(--r-md) 0;
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
  text-decoration: none;
}
.ms-skip:focus {
  left: 0;
}

/* Numbers align. Non-negotiable in a reporting tool. */
.num, [data-num], .kpi-table td, .rt td, table td.tr, table td.tc {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

/* Respect the OS motion setting — vestibular safety, and it also makes
   the drawer instant for anyone who has asked for that. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ══════════════════════════════════════════════════════════════════════
   3. RESPONSIVE SHELL — mobile drawer + app bar + bottom tabs
   ──────────────────────────────────────────────────────────────────────
   Replaces the old `@media (max-width:959px)` rule in styles.css that
   turned the sidebar into a static full-width block. That block put
   ~1,500px of navy navigation above the content of EVERY page, so a
   phone user scrolled two full screens of menu before reaching a single
   number, and every page looked identical on open. This is the fix.
   ══════════════════════════════════════════════════════════════════════ */

@media (max-width: 959px) {

  /* ── Shell geometry ─────────────────────────────────────────────── */
  html body .app-shell {
    flex-direction: row;              /* undo the old column stack */
    min-height: 100vh;
  }

  /* Sidebar becomes an off-canvas drawer, out of flow entirely. */
  html body .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    width: var(--drawer-w);
    height: 100%;
    max-height: 100%;
    margin: 0;
    z-index: var(--z-drawer);
    transform: translateX(-100%);
    transition: transform var(--dur-med) var(--ease);
    box-shadow: none;
    overscroll-behavior: contain;
    padding-bottom: var(--safe-b);
    visibility: hidden;
  }
  html body .app-shell.ms-drawer-open .sidebar {
    transform: translateX(0);
    box-shadow: var(--el-4);
    visibility: visible;
  }
  /* A collapsed desktop preference must not shrink the mobile drawer. */
  html body .app-shell.sidebar-collapsed .sidebar {
    width: var(--drawer-w);
  }
  html body .app-shell.sidebar-collapsed .sb-group-label,
  html body .app-shell.sidebar-collapsed .sb-item-label,
  html body .app-shell.sidebar-collapsed .sb-top-label,
  html body .app-shell.sidebar-collapsed .sb-badge-fill,
  html body .app-shell.sidebar-collapsed .sb-badge-view,
  html body .app-shell.sidebar-collapsed .sb-stub-tag {
    display: revert;
  }
  html body .app-shell.sidebar-collapsed .sb-item {
    grid-template-columns: 26px 1fr auto;
    justify-items: stretch;
  }

  /* Main takes the full width and clears the fixed bottom bar. */
  html body .main,
  html body .app-shell.sidebar-collapsed .main {
    margin-left: 0;
    width: 100%;
    padding-bottom: calc(var(--bottomnav-h) + var(--safe-b) + var(--sp-2));
  }

  /* The desktop collapse toggle is meaningless on mobile — it becomes
     the drawer's close button instead (relabelled in mobile-shell.js). */
  html body .sb-top {
    height: var(--appbar-h);
    min-height: var(--appbar-h);
    padding-top: var(--safe-t);
  }

  /* ── Scrim ──────────────────────────────────────────────────────── */
  .ms-scrim {
    position: fixed;
    inset: 0;
    background: rgba(9, 18, 33, .55);
    backdrop-filter: blur(2px);
    z-index: var(--z-scrim);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-med) var(--ease);
  }
  .app-shell.ms-drawer-open ~ .ms-scrim,
  body.ms-drawer-open .ms-scrim {
    opacity: 1;
    pointer-events: auto;
  }

  /* Lock background scroll while the drawer is open. */
  body.ms-drawer-open {
    overflow: hidden;
    touch-action: none;
  }

  /* ── Top app bar ────────────────────────────────────────────────── */
  html body .top-header {
    height: auto;
    min-height: var(--appbar-h);
    padding: var(--safe-t) var(--sp-3) 0;
    gap: var(--sp-2);
  }
  html body .top-header > * { min-width: 0; }

  /* Hamburger — injected by mobile-shell.js as the first header child. */
  .ms-menu-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--tap);
    height: var(--tap);
    flex-shrink: 0;
    margin-left: -6px;
    border: 0;
    border-radius: var(--r-md);
    background: rgba(255, 255, 255, .10);
    color: #fff;
    cursor: pointer;
    transition: background var(--dur-fast) var(--ease);
  }
  .ms-menu-btn:active { background: rgba(255, 255, 255, .22); }
  .ms-menu-btn svg { width: 22px; height: 22px; stroke-width: 2.2; }

  /* Page title — tells you WHICH page you are on. The old mobile shell
     could not: every page opened to the same wall of navy menu. */
  .ms-title {
    display: block;
    flex: 1;
    min-width: 0;
    font-size: var(--fs-md);
    font-weight: var(--fw-bold);
    letter-spacing: .1px;
    color: #fff;
    /* Wraps, never truncates. "Dispatch & Sto…" and "KPI Dashb…" were
       the app bar telling the reader it had run out of room rather than
       telling them where they are — and telling them where they are is
       the bar's entire job. It is min-height, not height, so a second
       line costs nothing. */
    overflow-wrap: anywhere;
    line-height: var(--lh-tight);
    padding-block: 2px;
  }
  .ms-title small {
    display: block;
    font-size: var(--fs-xs);
    font-weight: var(--fw-semibold);
    color: rgba(255, 255, 255, .62);
    letter-spacing: .3px;
    text-transform: uppercase;
    margin-top: 1px;
  }

  /* Header furniture that does not earn its width on a phone. */
  html body .home-btn,
  html body .sh-sep,
  html body .header-meta,
  html body .header-center,
  html body .auth-user-details { display: none; }

  html body .header-left  { padding-right: 0; gap: var(--sp-2); flex: 1; min-width: 0; }
  html body .header-right { padding-left: 0; gap: var(--sp-2); min-width: 0; flex-shrink: 0; }
  html body .nyati-logo   { height: 28px; }
  html body .logo-block   { flex-shrink: 0; }
  html body .lc-wordmark  { display: none; }

  html body .auth-user-avatar {
    width: 34px; height: 34px;
    font-size: var(--fs-base);
  }
  /* Logout lives in the drawer on mobile; the avatar opens it. */
  html body .auth-logout-btn { display: none; }

  /* ── Bottom tab bar ─────────────────────────────────────────────
     The primary navigation on a phone. Thumb-reachable, role-derived,
     max 5 slots (Tidwell: a tab bar past 5 stops being scannable), the
     last always "Menu" which opens the full drawer. */
  .ms-bottomnav {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: var(--z-bottomnav);
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    align-items: stretch;
    height: calc(var(--bottomnav-h) + var(--safe-b));
    padding-bottom: var(--safe-b);
    background: rgba(255, 255, 255, .96);
    backdrop-filter: saturate(180%) blur(12px);
    border-top: 1px solid var(--border, #DDE3EC);
    box-shadow: 0 -2px 14px rgba(16, 24, 40, .08);
  }
  .ms-bn-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    min-height: var(--tap);
    padding: var(--sp-1) 2px;
    border: 0;
    background: none;
    color: var(--text-muted, #5A6270);
    font-family: var(--font, 'Inter', system-ui, sans-serif);
    font-size: var(--fs-xs);
    font-weight: var(--fw-bold);
    letter-spacing: 0;
    text-decoration: none;
    cursor: pointer;
    position: relative;
    -webkit-tap-highlight-color: transparent;
    transition: color var(--dur-fast) var(--ease);
  }
  .ms-bn-item svg { width: 21px; height: 21px; stroke-width: 2; }
  .ms-bn-item.active { color: var(--navy, #173158); }
  /* Identity is never colour-alone — the active tab also gets a bar. */
  .ms-bn-item.active::before {
    content: '';
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 26px;
    height: 3px;
    border-radius: 0 0 6px 6px;
    background: var(--orange, #F49545);
  }
  .ms-bn-item:active { color: var(--navy, #173158); }
  .ms-bn-label {
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  /* Count badge (pending approvals) — number + label, never colour alone */
  .ms-bn-badge {
    position: absolute;
    top: 4px;
    left: 50%;
    margin-left: 4px;
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    border-radius: var(--r-pill);
    background: var(--st-bad);
    color: #fff;
    font-size: 10px;
    font-weight: var(--fw-heavy);
    line-height: 17px;
    text-align: center;
  }

  /* Drawer rows get real touch height. */
  html body .sb-item {
    min-height: var(--tap);
    margin: 2px var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    font-size: var(--fs-base);
    border-radius: var(--r-md);
  }
  html body .sb-item-icon { width: 26px; height: 26px; }
  html body .sb-item-icon i, html body .sb-item-icon svg { width: 15px; height: 15px; }
  html body .sb-item-label { font-size: var(--fs-base); }
  html body .sb-group-label { font-size: var(--fs-xs); margin: var(--sp-4) var(--sp-3) var(--sp-2); }
  html body .sb-badge-fill,
  html body .sb-badge-view,
  html body .sb-stub-tag { font-size: 10px; padding: 2px 6px; }
}

/* Desktop keeps the drawer machinery inert. */
@media (min-width: 960px) {
  .ms-scrim, .ms-bottomnav, .ms-menu-btn, .ms-title { display: none !important; }
}

/* ══════════════════════════════════════════════════════════════════════
   3b. CONTAINMENT — the horizontal-overflow class of bug
   ──────────────────────────────────────────────────────────────────────
   The audit found 390px phones rendering pages 672–914px wide. Body
   carries `overflow-x: hidden`, so this never showed as a scrollbar — it
   showed as content silently cut off at the right edge, which is worse:
   nobody reports a bug they cannot see.

   Two root causes, both confirmed by walking the ancestor chain:

   (a) Each page's `<main class="*-main">` is a flex ITEM of `.main`
       (a column flex container). A flex item's default `min-width: auto`
       refuses to shrink below min-content, so one nowrap table cell or
       one chart canvas deep inside dragged the whole page wider than the
       screen — approvals.html reached 745px off a single nowrap <td>,
       sales.html 914px off a Chart.js canvas still sized for desktop.

   (b) Rows of controls (.mis-tabs, .period-strip) are flex rows that
       neither wrap nor scroll, so past ~3 items they simply spilled.
       Five 135px MIS tabs need 672px and had 390px.
   ══════════════════════════════════════════════════════════════════════ */
/* ── CONTAINMENT (all widths) ──────────────────────────────────────
   Each page's `<main class="*-main">` is a flex ITEM of `.main`, a column
   flex container. A flex item defaults to `min-width: auto` — "never
   shrink below your own min-content" — so a single nowrap table cell or a
   canvas still sized for a wider layout could drag the whole page wider
   than its column. That is not a mobile-only failure: completeness.html
   overflowed by 170px on a 1440px desktop, because the sidebar takes
   210px and the content column is narrower than the page.

   Tables are deliberately not touched. Their explicit min-widths
   (.kpi-table { min-width: 1400px }) are correct — a 40-column shift
   report is a grid, and it scrolls inside its wrapper. */
html body .main > main,
html body .main > section,
html body .main > div,
html body .main > article,
html body .main > header,
html body .main > footer {
  min-width: 0;
  max-width: 100%;
}

/* A Chart.js canvas holds the pixel width it was constructed at until its
   resize observer catches up — and on a first paint it may never have
   been constructed narrow at all. */
html body canvas { max-width: 100%; }

@media (max-width: 959px) {

  /* (a) and the canvas clamp are NOT in this media block — they apply at
     every width. See "CONTAINMENT (all widths)" above: completeness.html
     proved the same min-width:auto bug bites a 1440px desktop too, once
     the sidebar takes 210px out of the content column. */

  /* THE ROOT OF THE WHOLE OVERFLOW CLASS.
     A flex or grid item defaults to `min-width: auto`, which means "never
     shrink below your own min-content width". A `repeat(8, 1fr)` track is
     really `minmax(auto, 1fr)`, so one long uppercase label like
     MINES WORKSHOP sets a floor for its column, eight columns multiply
     it, and the grid quietly grows wider than the phone. An explicit
     `min-width: 0` overrides that automatic minimum and lets the content
     wrap instead.

     `:where()` contributes zero specificity, so this whole selector
     weighs only as much as `html body .main` — enough to beat a plain
     class rule, not enough to disturb anything more deliberate. Tables
     are deliberately absent from the list: their explicit min-widths
     (.kpi-table { min-width: 1400px }) are correct and scroll inside
     their wrapper. */
  html body .main :where(div, section, article, aside, header, footer, a, li, label) {
    min-width: 0;
  }

  /* Letting tracks shrink is only half the answer — eight columns on a
     390px screen would be 45px each. These grids re-flow to as many
     columns as will actually hold a readable tile, and no more. */
  html body .smry-scorecard { grid-template-columns: repeat(auto-fit, minmax(104px, 1fr)); }
  html body .smry-hero,
  html body .smry-hero-3,
  html body .smry-row-3,
  html body .smry-row-4,
  html body .sk-row,
  html body .sk-row-3,
  html body .rl-kpi-row,
  html body .kt-row { grid-template-columns: repeat(auto-fit, minmax(148px, 1fr)); }

  /* The MIS stat strip is a FLEX row, so it was never in the list above
     and the min-width:0 rule crushed it instead of wrapping it: at 820px
     its eight tiles shrank to ~92px each and a headcount of 277 rendered
     as "27" over "7", with "TOTAL EMPL / OYEES" beneath it. A number
     broken across two lines is not a legibility nitpick — it is a
     different number. Same auto-fit treatment as its grid siblings. */
  html body .mis-kv-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
  }
  /* And a value never breaks mid-figure, whatever the container does. */
  html body .mis-kv-val,
  html body .kt-val,
  html body .sk-value,
  html body .rl-stat-val { overflow-wrap: normal; word-break: keep-all; }

  /* Long unbroken strings (report codes, equipment tags) must not act as
     a width floor either. */
  html body .main { overflow-wrap: break-word; }

  /* Page gutters: 28px of side padding is generous on a 1440px desktop
     and is 16% of a 360px phone. Reclaim it for the data — the screen
     edge is not a scarce resource, horizontal space is. */
  html body .main > main,
  html body .dashboard-main,
  html body .in-main,
  html body .rl-main,
  html body .apv-main {
    padding-left: var(--sp-3);
    padding-right: var(--sp-3);
  }

  /* (b) Control strips scroll horizontally instead of spilling. This is
         the standard scrollable-tabs pattern: the strip keeps its single
         row, the overflow becomes a swipe, and scroll-snap makes each
         tab settle cleanly rather than half-off-screen. */
  html body .mis-tabs,
  html body .period-strip,
  html body .view-switcher,
  html body .ms-hscroll {
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
  }
  html body .mis-tabs::-webkit-scrollbar,
  html body .period-strip::-webkit-scrollbar,
  html body .view-switcher::-webkit-scrollbar,
  html body .ms-hscroll::-webkit-scrollbar { display: none; }

  html body .mis-tab,
  html body .per-chip,
  html body .period-strip-lbl,
  html body .vs-tab,
  html body .ms-hscroll > * {
    flex: 0 0 auto;
    scroll-snap-align: start;
  }
  /* Tabs and chips get real touch height without losing the row. */
  html body .mis-tab { padding: var(--sp-3) var(--sp-4); }
  html body .per-chip { padding: var(--sp-2) var(--sp-4); font-size: var(--fs-xs); }
}

/* ══════════════════════════════════════════════════════════════════════
   4. LEGIBILITY FLOOR (mobile)
   ──────────────────────────────────────────────────────────────────────
   The audit found 84–97% of all text rendering below 12px on a phone —
   9,660 of 9,998 text nodes on mis.html alone. Density is right for a
   desktop analyst (Few) and wrong for a superintendent reading in
   daylight on a 390px screen (Johnson). These rules lift the bottom of
   the scale on small viewports only; the desktop scale is untouched.

   !important is used deliberately and only here and in §5: 28 pages
   carry their own inline <style> blocks, and a legibility floor that
   any page can silently undercut is not a floor.
   ══════════════════════════════════════════════════════════════════════ */
@media (max-width: 959px) {

  /* Body copy and data cells */
  html body table td,
  html body table th,
  html body .kpi-table td,
  html body .rt td,
  html body .cv,
  html body p,
  html body li { font-size: var(--fs-sm) !important; line-height: var(--lh-snug); }

  html body table th,
  html body .kpi-table thead th,
  html body .rt th { font-size: var(--fs-xs) !important; }

  /* Labels, captions, chips — the 8–10px band that dominated the audit */
  html body .mis-badge,
  html body .mis-tag,
  html body .mis-bar-lbl,
  html body .mis-kv-lbl,
  html body .mis-kv-sub,
  html body .an-stat-lbl,
  html body .an-atype-spec,
  html body .ie-label,
  html body .sk-label,
  html body .sk-unit,
  html body .sk-status,
  html body .sk-dept-tag,
  html body .rcard-title,
  html body .rcard-chip,
  html body .kt-badge,
  html body .kt-unit,
  html body .kt-vs,
  html body .summary-chip,
  html body .ctx-dept,
  html body .dash-footer,
  html body label,
  html body .btn,
  html body option,
  html body small { font-size: var(--fs-xs) !important; }

  /* Values are never smaller than body copy — they are the point. */
  html body td.actual,
  html body td.target,
  html body td.status,
  html body .summary-chip span,
  html body .kt-value,
  html body .sk-value { font-size: var(--fs-base) !important; font-weight: var(--fw-bold); }

  /* Headings get real hierarchy back rather than 1px increments. */
  html body h1 { font-size: var(--fs-xl)  !important; line-height: var(--lh-tight); }
  html body h2 { font-size: var(--fs-lg)  !important; line-height: var(--lh-tight); }
  html body h3 { font-size: var(--fs-md)  !important; line-height: var(--lh-snug);  }

  /* iOS Safari zooms the whole page when a focused field is under 16px.
     This single rule is the difference between a usable and an
     infuriating mobile form. */
  html body input:not([type="checkbox"]):not([type="radio"]),
  html body select,
  html body textarea { font-size: 16px !important; }
}

/* ══════════════════════════════════════════════════════════════════════
   5. TOUCH TARGETS
   ──────────────────────────────────────────────────────────────────────
   Keyed off pointer capability, not width — a touchscreen laptop needs
   these too, and a phone in desktop-mode does not stop being a phone.
   The audit found 163 of 174 interactive elements on daily-report.html
   under 40px.
   ══════════════════════════════════════════════════════════════════════ */
@media (pointer: coarse) {
  html body button,
  html body .btn,
  html body a.home-btn,
  html body .vs-tab,
  html body .mis-tab,
  html body .per-chip,
  html body .sb-item,
  html body select,
  html body textarea,
  html body input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]) {
    min-height: var(--tap);
  }

  /* A class-scoped height beats the bare-element rule above on
     specificity, so the controls that set their own must be named. The
     KPI dashboard's three filter selects were the last holdout: 36px on
     a tablet, where the <768px block that fixes them never applies. */
  html body .filter-card select,
  html body .filter-card input,
  html body .dsr-controls input,
  html body .dsr-controls button,
  html body .an-month-input,
  html body .tb-ctl {
    min-height: var(--tap);
  }

  /* Chip-shaped links act as buttons and must be sized as buttons —
     .smry-ops-chip was rendering 20px tall, well under half a fingertip. */
  html body .smry-ops-chip,
  html body .rcard-chip,
  html body .rl-pill,
  html body .sec-tab,
  html body .summary-chip {
    min-height: var(--tap);
    display: inline-flex;
    align-items: center;
    padding-top: var(--sp-1);
    padding-bottom: var(--sp-1);
  }

  /* .sc-chip is named like those but is not one of them: it is a
     department scorecard TILE holding a name, a percentage, a bar and a
     status word stacked vertically. inline-flex laid that stack out as a
     ROW, so at six across a tablet it rendered "PROD 0 CON" — every one
     of the three clipped mid-word. It needs the tap height without the
     flex row. */
  html body .sc-chip {
    min-height: var(--tap);
    padding-top: var(--sp-2);
    padding-bottom: var(--sp-2);
  }

  /* Inside a dense data grid a 44px row would double the form's height,
     so grid cells get the cozy target instead — still well above the
     WCAG 2.2 24px minimum. */
  html body td button,
  html body td .btn,
  html body td input,
  html body td select,
  html body th button,
  html body .rt input,
  html body .rt select {
    min-height: var(--tap-cozy);
  }

  /* Checkboxes and radios are the one control people genuinely miss. */
  html body input[type="checkbox"],
  html body input[type="radio"] {
    width: 22px;
    height: 22px;
    accent-color: var(--navy, #173158);
  }

  /* Give adjacent inline controls breathing room so the wrong one is
     not tapped (Fitts: target size AND separation). */
  html body .filter-group,
  html body .toolbar,
  html body .btn-row { gap: var(--sp-2); }
}

/* ══════════════════════════════════════════════════════════════════════
   6. WIDE TABLES — scroll affordance
   ──────────────────────────────────────────────────────────────────────
   Several tables are legitimately 1,400–3,100px wide and should stay
   that way: a 40-column shift report is a grid, not a list, and turning
   it into stacked cards destroys the row-to-row comparison that is the
   entire reason it exists (Few). What was missing is any signal that
   more columns exist off-screen. These rules add the signal and make
   the first column sticky so a scrolled row keeps its identity.
   ══════════════════════════════════════════════════════════════════════ */
/* `.tbl-scroll` is the app's table scroll wrapper, but it was DEFINED in
   purchases.html and forms/forms.css and merely USED in sales.html —
   which loads neither, so its two 760–820px tables had a wrapper that
   did not scroll and spilled 478px off a 390px screen. Defining the
   utility once, here, fixes sales.html and every page that reaches for
   the class next. */
.tbl-scroll {
  overflow-x: auto;
  max-width: 100%;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}

/* The fades are absolutely positioned, so the scroller needs to be a
   containing block — but only if it is not already one. Widening the
   affordance to every element that actually scrolls (rather than a list
   of class names) caught .sec-nav on the stoppage form, which is
   `position: sticky; top: 121px`. Overriding that to `relative` turned
   its sticky offset into a 121px downward shift and dropped the section
   tabs straight through the KPI cards below them. mobile-shell.js marks
   anything already positioned so this rule leaves it alone. */
.ms-scroll-x:not(.ms-positioned) {
  position: relative;
}
/* Right-edge fade — appears only while there is more to scroll to.
   mobile-shell.js toggles .ms-has-more / .ms-has-prev on scroll. */
.ms-scroll-x::after,
.ms-scroll-x::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 26px;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-med) var(--ease);
  z-index: 4;
}
.ms-scroll-x::after {
  right: 0;
  background: linear-gradient(to left, rgba(255,255,255,.96), rgba(255,255,255,0));
}
.ms-scroll-x::before {
  left: 0;
  background: linear-gradient(to right, rgba(255,255,255,.96), rgba(255,255,255,0));
}
.ms-scroll-x.ms-has-more::after { opacity: 1; }
.ms-scroll-x.ms-has-prev::before { opacity: 1; }

/* A one-time hint, shown only on touch and only where it applies. */
.ms-swipe-hint {
  display: none;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--text-muted, #5A6270);
  background: var(--st-neutral-bg);
  border: 1px solid var(--st-neutral-bd);
  border-radius: var(--r-md);
}
@media (pointer: coarse) {
  .ms-swipe-hint.ms-show { display: flex; }
}
.ms-swipe-hint svg { width: 15px; height: 15px; flex-shrink: 0; }

@media (max-width: 959px) {
  html body .table-wrapper,
  html body .rt-wrap {
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
  }
  /* Sticky row identity: the label column stays put while the numbers
     scroll under it. Without this a scrolled wide table is unreadable —
     you are looking at a value with no idea which KPI it belongs to. */
  html body .rt thead th:first-child,
  html body .rt tbody td:first-child {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--white, #fff);
    box-shadow: 2px 0 5px rgba(16, 24, 40, .07);
  }
  html body .rt thead th:first-child { z-index: 3; background: var(--navy, #173158); }
}

/* ══════════════════════════════════════════════════════════════════════
   7. PRINT — reports get printed and pinned up in the control room.
   ══════════════════════════════════════════════════════════════════════ */
@media print {
  .ms-bottomnav, .ms-scrim, .ms-menu-btn, .ms-skip, .ms-swipe-hint { display: none !important; }
  .sidebar { display: none !important; }
  .main { margin-left: 0 !important; width: 100% !important; padding-bottom: 0 !important; }
  .ms-scroll-x::after, .ms-scroll-x::before { display: none !important; }
}

/* ══════════════════════════════════════════════════════════════════════
   8. FILTER BAR — collapsed on mobile, open on desktop
   ──────────────────────────────────────────────────────────────────────
   Three 44px selects plus a print button is ~250px of controls sitting
   above the data on every visit, to answer a question ("which period?")
   that gets re-answered maybe once a session. Collapsed, it is one line
   that still states the answer. See wireFilterCollapse() in
   mobile-shell.js. Desktop is untouched: there is room, and an analyst
   at a desk changes period constantly.
   ══════════════════════════════════════════════════════════════════════ */
.ms-fb-toggle { display: none; }

@media (max-width: 959px) {
  html body .filter-inner { padding: var(--sp-2) var(--sp-3); gap: var(--sp-2); }

  html body .ms-fb-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--sp-2);
    width: 100%;
    min-height: var(--tap);
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--border, #DDE3EC);
    border-radius: var(--r-md);
    background: var(--white, #fff);
    color: var(--navy, #173158);
    font-family: var(--font, 'Inter', system-ui, sans-serif);
    font-size: var(--fs-sm);
    font-weight: var(--fw-bold);
    text-align: left;
    cursor: pointer;
  }
  html body .ms-fb-toggle:active { background: var(--section-bg, #EEF2F8); }
  .ms-fb-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .ms-fb-caret {
    flex-shrink: 0;
    transition: transform var(--dur-med) var(--ease);
  }
  .filter-inner.ms-filters-open .ms-fb-caret { transform: rotate(180deg); }

  /* Collapsed: the controls and the print action stand down; the result
     chips stay, because they are the answer, not the question. */
  html body .filter-inner.ms-filters-collapsible:not(.ms-filters-open) .filter-group,
  html body .filter-inner.ms-filters-collapsible:not(.ms-filters-open) .print-btn {
    display: none;
  }
  html body .filter-inner.ms-filters-open .filter-group {
    display: grid;
    grid-template-columns: 1fr;
    gap: var(--sp-2);
    width: 100%;
  }
  html body .filter-inner.ms-filters-open .filter-card { width: 100%; }

  /* Result chips: three equal columns, value over label so each stays
     readable at a glance instead of ellipsising. */
  html body .filter-summary {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--sp-2);
    width: 100%;
  }
  html body .filter-summary .fill-monthly-btn,
  html body .filter-summary .print-btn { grid-column: 1 / -1; justify-content: center; }
}

/* ══════════════════════════════════════════════════════════════════════
   9. EXEC SUMMARY — mobile density
   ══════════════════════════════════════════════════════════════════════ */

/* A period badge must never wrap: "July 2026" breaking after "202" reads
   as a rendering fault, and the containment rules in §3b legitimately let
   narrow containers shrink far enough for it to happen. */
.smry-period-badge,
.kt-badge,
.sk-dept-tag,
.summary-chip span { white-space: nowrap; }

/* The Today's-Reports chips get their own scroll track (see the renderer
   in index.html). Wrapping freely they were five rows of pills — ~400px
   — restating what the summary line says in one. */
.smry-ops-track { display: contents; }

@media (max-width: 959px) {
  html body .smry-ops-strip {
    flex-wrap: wrap;
    padding: var(--sp-2) var(--sp-3);
    gap: var(--sp-2);
    row-gap: var(--sp-2);
  }
  /* DOM order stays label -> track -> sum so desktop is untouched; only
     the track is re-ordered onto its own line here. */
  html body .smry-ops-sum { order: 0; margin-left: auto; }

  html body .smry-ops-track {
    display: flex;
    order: 1;
    width: 100%;
    gap: var(--sp-2);
    flex-wrap: nowrap;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
    padding-bottom: 2px;
  }
  html body .smry-ops-track::-webkit-scrollbar { display: none; }
  html body .smry-ops-track .smry-ops-chip { flex: 0 0 auto; scroll-snap-align: start; }

  /* The section header stacks: a title and its period badge fighting for
     one 360px line is what forced the badge to wrap in the first place. */
  html body .smry-header { flex-wrap: wrap; gap: var(--sp-2); padding: var(--sp-4) var(--sp-3) var(--sp-3); }
}

/* ── KPI tiles on a narrow screen ────────────────────────────────────
   .sk-top puts the KPI name and its department tag on one flex row. At
   two tiles across a 390px phone the tag takes ~100px of a ~170px card,
   leaving too little for the name: "KILN OUTPUT" was breaking after
   "OUTPU". The tag is metadata — the name is the point — so on mobile
   the row becomes a stack and the name gets the full width. */
@media (max-width: 959px) {
  html body .sk-top {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-1);
  }
  html body .sk-label {
    width: 100%;
    letter-spacing: .2px;      /* tracking costs width the label needs */
    line-height: var(--lh-snug);
    /* Two lines reserved, so a tile whose name wraps and one whose name
       does not still put their values on the same baseline. Measured on
       the Energy Efficiency row: "SPECIFIC POWER — TOTAL" wraps and its
       three neighbours do not, which left four values at two heights. */
    min-height: calc(2 * var(--lh-snug) * var(--fs-xs));
  }
  html body .sk-dept-tag { order: 2; }

  /* The value is what the tile exists for — let it lead clearly. */
  html body .sk-value { font-size: var(--fs-2xl); line-height: var(--lh-tight); }
}

/* ══════════════════════════════════════════════════════════════════════
   10. CARD VIEW FOR ENTRY GRIDS
   ──────────────────────────────────────────────────────────────────────
   A shift grid as one card per row. Driven by `body.me-view-cards`, NOT
   by a media query: card view is a *view the reader chooses*, available
   at any width, not a mobile fallback. forms/mobile-entry.js owns the
   preference (auto | cards | table) and remembers it.

   The grid is only ever hidden, never destroyed — the switch in the
   sticky strip puts it straight back.
   ══════════════════════════════════════════════════════════════════════ */

:root { --pg-top: 64px; }                    /* desktop .top-header height */
@media (max-width: 959px) { :root { --pg-top: var(--appbar-h); } }

.me-cards  { display: none; }
.me-hidden { display: none; }

body.me-view-cards .me-cards {
  display: grid;
  /* Cards flow into as many columns as will hold one at a readable
     width. One card per full row is right on a phone and wasteful on a
     1440px screen, where it stretched every input to ~500px — the point
     of a card is that a field sits next to its label at a comfortable
     size, not that it fills the monitor. ~340px keeps a card's internal
     three-up rhythm identical at every width. */
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  align-items: start;
  gap: var(--sp-3);
  margin: 0 0 var(--sp-5);
}
/* The per-grid count and any section heading span the whole row rather
   than becoming a card-shaped column of their own. */
body.me-view-cards .me-cards > .me-toolbar,
body.me-view-cards .me-cards > .me-group { grid-column: 1 / -1; }

/* ── Per-grid count ─────────────────────────────────────────────────── */
.me-toolbar {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: var(--sp-2) var(--sp-1) 0;
}
.me-count {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  color: var(--text-muted, #5A6270);
  font-variant-numeric: tabular-nums;
}
.me-count.me-done { color: var(--st-good); }

/* ── Section heading lifted from a grid's own section row ───────────── */
.me-group {
  font-size: var(--fs-xs);
  font-weight: var(--fw-heavy);
  letter-spacing: .7px;
  text-transform: uppercase;
  color: var(--navy, #173158);
  background: var(--section-bg, #EEF2F8);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  margin-top: var(--sp-2);
}

/* ── The card ───────────────────────────────────────────────────────── */
.me-card {
  background: var(--white, #fff);
  border: 1px solid var(--border, #DDE3EC);
  border-radius: var(--r-lg);
  box-shadow: var(--el-1);
  overflow: hidden;
}
/* A read-only row is reference, not work: it recedes. */
.me-card.me-readonly { background: var(--off-white, #F7F8FA); box-shadow: none; }

.me-card-hd {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4) var(--sp-2);
  border-bottom: 1px solid var(--border, #DDE3EC);
}
/* The serial number several grids lead with. Kept, but as a quiet badge —
   it is not what identifies the row to a human. */
.me-card-sn {
  flex-shrink: 0;
  min-width: 22px;
  padding: 1px 6px;
  border-radius: var(--r-sm);
  background: var(--section-bg, #EEF2F8);
  color: var(--text-muted, #5A6270);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.me-card-title {
  margin: 0;
  flex: 1;
  min-width: 0;
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
  color: var(--text-main, #111827);
  line-height: var(--lh-snug);
}
/* The spec range is why a value is right or wrong — beside the parameter,
   not repeated under each of six identical inputs. */
.me-card-spec {
  flex-shrink: 0;
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--text-muted, #5A6270);
  background: var(--section-bg, #EEF2F8);
  border-radius: var(--r-pill);
  padding: 2px var(--sp-2);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* ── Entry fields ───────────────────────────────────────────────────── */
.me-body { padding: var(--sp-1) 0; }

/* A column group shown ONCE as a heading. "Material Consumption (MT)"
   covering On Date / MTD / YTD was being repeated in full on all three
   labels, which made every label three lines tall and left the inputs
   staggered down a staircase. */
.me-fgroup { padding: var(--sp-2) var(--sp-4) 0; }
.me-fgroup-h {
  font-size: var(--fs-xs);
  font-weight: var(--fw-heavy);
  letter-spacing: .5px;
  text-transform: uppercase;
  color: var(--navy, #173158);
  margin-bottom: var(--sp-1);
}
/* Ungrouped fields keep the same 3-column rhythm as the groups above
   them — a lone "Opening Stock" stretched across the whole card read as
   a different kind of thing than the three-up rows around it. */
.me-plain { padding: var(--sp-2) var(--sp-4) 0; }
.me-plain .me-fields { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 430px) {
  .me-plain .me-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.me-body > :last-child { padding-bottom: var(--sp-3); }

.me-fields {
  display: grid;
  grid-template-columns: repeat(var(--me-cols, 3), minmax(0, 1fr));
  gap: var(--sp-2);
}
/* Bottom-aligned: a two-line label must not push its input below its
   neighbour's. The label grows upward, the inputs stay on one line. */
.me-field {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: var(--sp-1);
  min-width: 0;
}
.me-field-lbl {
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  color: var(--text-muted, #5A6270);
  line-height: 1.25;
  min-width: 0;
  overflow-wrap: break-word;
}
.me-field input,
.me-field select,
.me-field textarea {
  width: 100%;
  min-height: var(--tap);
  padding: var(--sp-2);
  border: 1.5px solid var(--border, #DDE3EC);
  border-radius: var(--r-md);
  background: var(--white, #fff);
  color: var(--text-main, #111827);
  font-family: var(--font, 'Inter', system-ui, sans-serif);
  font-size: 16px;            /* under 16px iOS zooms the page on focus */
  font-weight: var(--fw-semibold);
  font-variant-numeric: tabular-nums;
  text-align: center;
  transition: border-color var(--dur-fast) var(--ease),
              box-shadow var(--dur-fast) var(--ease);
}
.me-field input:focus,
.me-field select:focus,
.me-field textarea:focus {
  outline: none;
  border-color: var(--orange, #F49545);
  box-shadow: 0 0 0 3px rgba(244, 149, 69, .18);
}
.me-field input.me-filled { border-color: var(--st-good-bd); background: #FCFEFD; }
.me-field input.ooc, .me-field .ooc {
  border-color: var(--st-bad);
  background: var(--st-bad-bg);
  color: var(--st-bad);
}

/* ── Computed read-back ─────────────────────────────────────────────
   A grid of key-over-value pairs. As a wrapping flex run it read as one
   long sentence — "OpeningStock 0 MTD 0 YTD 7,400 MTD 0 YTD 3,705" — in
   which you could not tell which MTD belonged to what. */
.me-derived {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
  gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  background: var(--off-white, #F7F8FA);
  border-top: 1px solid var(--border, #DDE3EC);
}
/* Bottom-aligned for the same reason the entry fields are: a key that
   wraps to two lines must not drop its value below the one beside it. */
.me-d {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 1px;
  min-width: 0;
}
.me-d-k {
  font-size: var(--fs-xs);
  font-weight: var(--fw-semibold);
  color: var(--text-muted, #5A6270);
  line-height: 1.25;
  overflow-wrap: break-word;
}
.me-d-v {
  font-size: var(--fs-base);
  font-weight: var(--fw-bold);
  color: var(--text-main, #111827);
  font-variant-numeric: tabular-nums;
}

/* ══════════════════════════════════════════════════════════════════════
   Sticky strip: progress · view switch · save
   Shown at every width, because all three are useful at every width —
   the page's own toolbar scrolls away on a desktop too.
   ══════════════════════════════════════════════════════════════════════ */
.me-pg {
  position: sticky;
  top: var(--pg-top);
  z-index: 90;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-4);
  background: rgba(255, 255, 255, .97);
  backdrop-filter: saturate(180%) blur(10px);
  border-bottom: 1px solid var(--border, #DDE3EC);
}
.me-pg-track {
  flex: 1;
  height: 6px;
  min-width: 40px;
  border-radius: var(--r-pill);
  background: var(--section-bg, #EEF2F8);
  overflow: hidden;
}
.me-pg-fill {
  display: block;
  height: 100%;
  width: 0;
  border-radius: var(--r-pill);
  background: var(--orange, #F49545);
  transition: width var(--dur-med) var(--ease);
}
.me-pg-done .me-pg-fill   { background: var(--st-good); }
.me-pg-locked .me-pg-fill { background: var(--st-neutral-bd); }
.me-pg-text {
  flex-shrink: 0;
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  color: var(--text-muted, #5A6270);
  font-variant-numeric: tabular-nums;
}
.me-pg-done .me-pg-text   { color: var(--st-good); }
.me-pg-locked .me-pg-text { color: var(--text-muted, #5A6270); }

/* Segmented Cards | Table switch. */
.me-viewsw {
  display: inline-flex;
  flex-shrink: 0;
  padding: 2px;
  border: 1px solid var(--border, #DDE3EC);
  border-radius: var(--r-md);
  background: var(--section-bg, #EEF2F8);
}
.me-vs {
  min-height: 30px;
  padding: 0 var(--sp-3);
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-muted, #5A6270);
  font-family: var(--font, 'Inter', system-ui, sans-serif);
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
.me-vs.active {
  background: var(--white, #fff);
  color: var(--navy, #173158);
  box-shadow: var(--el-1);
}

/* Save, within thumb reach of wherever the reader has scrolled to. */
.me-pg-save {
  flex-shrink: 0;
  min-height: 32px;
  padding: 0 var(--sp-4);
  border: 0;
  border-radius: var(--r-md);
  background: var(--orange, #F49545);
  color: var(--on-orange);
  font-family: var(--font, 'Inter', system-ui, sans-serif);
  font-size: var(--fs-xs);
  font-weight: var(--fw-heavy);
  cursor: pointer;
}
.me-pg-save:active   { filter: brightness(.93); }
.me-pg-save[disabled] { opacity: .6; }

/* These are primary controls in their own right, not cells in a data
   grid, so they take the full 44px target rather than the cozy one. */
@media (pointer: coarse) {
  .me-vs      { min-height: var(--tap); padding: 0 var(--sp-4); }
  .me-pg-save { min-height: var(--tap); }
}

/* Narrow screens: the strip is the only place these live, so it earns a
   little more room, and the count can go if space is tight. */
@media (max-width: 620px) {
  .me-pg { gap: var(--sp-2); padding: var(--sp-2) var(--sp-3); }
  .me-pg-text { font-size: var(--fs-xs); }
}
@media (max-width: 430px) {
  .me-pg-track { display: none; }
}

/* The collapse chevron is injected by forms/collapsible.js, which several
   pages load WITHOUT forms/forms.css — so on daily-report the same button
   fell back to browser defaults and rendered 19px tall with square
   corners while every form page drew it 22px and rounded. One button, one
   definition, stated where every page can see it. */
.rcard-toggle {
  width: 22px; height: 22px;
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, .12);
  border: none; cursor: pointer; padding: 0;
  display: flex; align-items: center; justify-content: center;
  color: #fff; flex-shrink: 0;
  transition: background var(--dur-fast) var(--ease);
}
.rcard-toggle:hover { background: rgba(255, 255, 255, .22); }
.rcard-toggle svg { width: 12px; height: 12px; stroke-width: 2.5; }

/* ── Report card headers wrap gracefully ───────────────────────────
   .rcard-hdr is a space-between flex row of title, date pill, department
   pill and a collapse chevron. On a phone the department pill takes the
   full width and pushed the chevron onto a line of its own, where
   align-items:stretch made it a tall empty box. */
@media (max-width: 959px) {
  html body .rcard-hdr { align-items: flex-start; row-gap: var(--sp-2); }
  html body .rcard-hdr > * { min-width: 0; }
  html body .rcard-toggle,
  html body .rcard-hdr > button:last-child {
    position: absolute;
    top: var(--sp-2);
    right: var(--sp-2);
    align-self: flex-start;
  }
  /* Reserve the chevron's corner across the whole header, not just the
     title — the subtitle runs the full width and was passing under it. */
  html body .rcard-hdr > :first-child { padding-right: 44px; }
}

/* ══════════════════════════════════════════════════════════════════════
   11. KPI TILES — one dimension, one channel
   ──────────────────────────────────────────────────────────────────────
   forms/forms.css carries the full rationale. The short version: the tile
   used to paint RAG status on a top bar, a body tint, a border and a bar
   fill, and data provenance on a bottom bar and a badge — in the same two
   hues. A failing formula KPI showed a red top edge and a green bottom
   edge and the reader could not tell which to believe.

   This block exists because daily-report.html and power-plant-report.html
   keep their OWN copies of the tile (they do not load forms.css), and
   both copies were the old design. Rather than a third and fourth
   divergent copy, the corrected treatment is stated once here, where it
   loads last and settles all of them.

   Status  → the left rail, the vs-line, the pct, the bar fill.
   Provenance → a neutral pill whose DOT alone carries the hue.

   This also retires the 74px padding reservation the label used to need
   to clear an absolutely-positioned badge. The badge is in the flow now,
   so the label gets its full width back — which is most of why the six
   values in a strip stopped landing at different heights.
   ══════════════════════════════════════════════════════════════════════ */
html body .kt {
  border-left: 3px solid var(--st-neutral-bd);
  display: flex;
  flex-direction: column;
}
html body .kt::before,
html body .kt::after { content: none; }

html body .kt.on-track { border-left-color: var(--st-good); background: #fff; border-top-color: var(--border); border-right-color: var(--border); border-bottom-color: var(--border); }
html body .kt.watch    { border-left-color: var(--st-warn); background: #fff; border-top-color: var(--border); border-right-color: var(--border); border-bottom-color: var(--border); }
html body .kt.concern  { border-left-color: var(--st-bad);  background: #fff; border-top-color: var(--border); border-right-color: var(--border); border-bottom-color: var(--border); }

html body .kt-badge {
  position: static;
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  margin-bottom: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--r-sm);
  background: var(--st-neutral-bg);
  color: var(--st-neutral);
  border: 1px solid var(--st-neutral-bd);
  font-weight: var(--fw-bold);
}
html body .kt-badge::before {
  content: '';
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--st-neutral);
  flex: none;
}
html body .kt-m .kt-badge::before { background: var(--amber-ink); }
html body .kt-s .kt-badge::before { background: var(--series-1); }
html body .kt-f .kt-badge::before { background: var(--green-ink); }

html body .kt-lbl { padding-right: 0; min-height: 32px; }
html body .kt-val { font-variant-numeric: tabular-nums; }
/* Nothing to show. An em-dash at 26px/800 in navy is a solid bar — it reads
   as a redacted figure rather than an absent one, so the slot goes quiet
   instead of shouting a blank. forms/kpi-live.js toggles .is-empty on every
   .kt-val it watches, but the rule only existed in forms/forms.css:503, which
   daily-report.html and power-plant-report.html do not link — both link only
   styles.css and design-system.css, so both still drew the navy bar (RAID
   I-8). It belongs here, next to the other .kt-val rules, rather than being
   imported with the other 698 lines of forms.css. */
html body .kt-val.is-empty { font-weight: 500; color: var(--text-muted); }
html body .kt-pct { font-variant-numeric: tabular-nums; }
html body .kt-bar-fill { background: var(--st-neutral); }
html body .kt.on-track .kt-bar-fill { background: var(--st-good); }
html body .kt.watch    .kt-bar-fill { background: var(--st-warn); }
html body .kt.concern  .kt-bar-fill { background: var(--st-bad); }
html body .kt.watch    .kt-pct { color: var(--amber-ink); }
html body .kt.concern  .kt-pct { color: var(--red-ink); }

/* Six across on a 1440 screen leaves ~190px a card, and these labels run
   to three lines as often as two. Reserving three puts every value in the
   row on one baseline. */
@media (min-width: 1024px) {
  html body .kt-lbl { min-height: 45px; }
}

@media (max-width: 959px) {
  html body .kpi-strip {
    grid-template-columns: repeat(auto-fit, minmax(158px, 1fr));
    gap: var(--sp-2);
  }
  html body .kt {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding: var(--sp-3);
  }
  html body .kt-badge {
    position: static;
    align-self: flex-start;
    margin-bottom: var(--sp-1);
  }
  html body .kt-lbl {
    padding-right: 0;
    width: 100%;
    letter-spacing: .2px;
    line-height: var(--lh-snug);
  }
  html body .kt-val { font-size: var(--fs-2xl); line-height: var(--lh-tight); }
  html body .kt-foot { width: 100%; }
}

/* ══════════════════════════════════════════════════════════════════════
   12. TEXT INKS — brand colours are fills, not text colours
   ──────────────────────────────────────────────────────────────────────
   The same lesson the chart palette taught, in a second place. A colour
   that identifies the company on a button does not automatically carry
   legible text: measured against WCAG 2.2 AA, the Nyati orange as TEXT
   scores 2.27:1 on white (needs 4.5) and white ON that orange scores the
   same 2.27:1. Brand green as text is 3.81:1. Together they accounted
   for 379 contrast failures across 19 pages.

   So each brand colour gets an "ink" sibling: the same hue, stepped dark
   enough to read. The fill colours are untouched — a button is still
   Nyati orange, its label is just no longer white.

   Every value here is computed, not chosen by eye. Ratios are against
   white and against the tint each colour actually sits on in this app.
   ══════════════════════════════════════════════════════════════════════ */
:root {
  --orange-ink: #A85C14;   /* 4.99:1 on white · 4.55:1 on var(--st-warn-bg)        */
  --green-ink:  #12703F;   /* 6.15:1 on white · 5.48:1 on var(--st-good-bg)        */
  --amber-ink:  #B45309;   /* 5.02:1 on white · 4.93:1 on var(--st-warn-bg)        */
  --blue-ink:   #2C5A9E;   /* 6.26:1 on #F1F5F9                          */
  --red-ink:    #C0271F;   /* 5.91:1 on white — red-500 as text is 3.76:1 */

  /* Text ON a brand-orange fill. Darkening the orange enough for white
     text would have cost the brand hue; brand navy on brand orange keeps
     both and measures better than white ever could. */
  --on-orange:  #173158;   /* 5.71:1 on #F49545                          */

  /* White on the navy ground needs 0.55 alpha to clear AA; the app was
     using 0.45 (3.89:1) in headers, footers and sidebar badges. */
  --on-navy:    rgba(255, 255, 255, .55);   /* 5.04:1 on #173158         */
}

/* Buttons that fill with brand orange take navy labels, not white.
   Scoped to the actual orange-filled controls rather than applied by
   background detection, which CSS cannot do. */
.btn-save,
.btn-submit,
.me-pg-save,
.btn-primary,
.rcard-chip.orange,
#login-btn,
#rl-apply,
#in-apply,
#cp-apply,
#an-apply,
.tb-apply,
button.apply-btn {
  color: var(--on-orange);
}
.btn-save .btn-label,
.btn-submit .btn-label,
.me-pg-save .btn-label,
#login-btn .btn-label,
.btn-primary .btn-label { color: inherit; }

/* Their icons follow the label rather than staying white. */
.btn-save svg, .btn-save i,
#login-btn svg, #login-btn i,
.btn-primary svg, .btn-primary i { color: inherit; stroke: currentColor; }

/* Read-only is a state of the page, so the rule that hides write actions
   has to be a rule and not a sweep — anything injected after auth.js has
   run is covered by this and was not covered before. */
body.is-readonly .btn-save,
body.is-readonly .btn-sub,
body.is-readonly .btn-out,
body.is-readonly button[type="submit"],
body.is-readonly .fill-monthly-btn { display: none !important; }

/* ══════════════════════════════════════════════════════════════════════
   VIEW-ONLY BANNER
   Most people on most forms are read-only, so this is one of the most
   frequently seen elements in the app. It gets to look composed: icon,
   then the fact, then what to do about it, stacked so it reads as one
   message at every width instead of splitting into columns.
   ══════════════════════════════════════════════════════════════════════ */
.ds-viewonly {
  position: sticky; top: 0; z-index: 200;
  display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-6);
  background: var(--navy, #173158);
  color: #fff;
  box-shadow: var(--el-2);
}
.ds-viewonly-icon { flex: none; margin-top: 2px; opacity: .8; }
.ds-viewonly-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.ds-viewonly-text strong {
  font-size: var(--fs-sm); font-weight: var(--fw-bold); letter-spacing: .2px;
}
.ds-viewonly-text > span {
  font-size: var(--fs-sm); font-weight: var(--fw-normal);
  color: var(--on-navy);
}
@media (min-width: 960px) {
  /* Room to sit on one line on a control-room screen. */
  .ds-viewonly { align-items: center; justify-content: center; }
  .ds-viewonly-icon { margin-top: 0; }
  .ds-viewonly-text { flex-direction: row; align-items: baseline; gap: var(--sp-2); }
}

/* A toast pinned to `bottom: 24px` lands underneath the bottom tab bar
   on a phone, covering three of the five destinations while it shows.
   Lift it clear of the bar and the home-indicator inset. */
@media (max-width: 959px) {
  html body .toast {
    bottom: calc(var(--bottomnav-h) + var(--safe-b) + var(--sp-3));
    left: var(--sp-3);
    right: var(--sp-3);
  }
}

/* ══════════════════════════════════════════════════════════════════════
   STATE PANEL — the page could not show you what you came for
   ──────────────────────────────────────────────────────────────────────
   When the Sales dashboard's upstream is unreachable it used to print
   one line of 12px grey text — "Failed to load … Make sure the backend
   is running" — into the hero row and leave seven empty white boxes
   below it, still captioned as though their content were coming. On a
   1920 control-room screen that reads as an application that has broken,
   not as data that is temporarily unavailable, and it offers the reader
   nothing to do about it.

   One panel, three parts: what happened, what it means, and the way out.
   Used for a hard failure (.is-error) and for a legitimately empty
   result (default), because the two look different to a reader and
   should: an empty month is normal, an unreachable upstream is not.
   ══════════════════════════════════════════════════════════════════════ */
.ds-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--sp-2);
  padding: var(--sp-10) var(--sp-6);
  border: 1px solid var(--st-neutral-bd);
  border-radius: var(--r-md);
  background: #fff;
  color: var(--st-neutral);
}
.ds-state-icon {
  display: flex; align-items: center; justify-content: center;
  width: 40px; height: 40px;
  border-radius: var(--r-pill);
  background: var(--st-neutral-bg);
  color: var(--st-neutral);
  margin-bottom: var(--sp-1);
}
.ds-state-icon svg { width: 20px; height: 20px; }
.ds-state-title {
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  color: var(--navy, #173158);
}
.ds-state-body {
  font-size: var(--fs-base);
  font-weight: var(--fw-medium);
  max-width: 46ch;
  line-height: var(--lh-normal);
}
.ds-state-act {
  margin-top: var(--sp-3);
  display: inline-flex; align-items: center; gap: var(--sp-2);
  min-height: var(--tap);
  padding: 0 var(--sp-5);
  border: 1px solid var(--navy, #173158);
  border-radius: var(--r-sm);
  background: var(--navy, #173158);
  color: #fff;
  font: inherit;
  font-size: var(--fs-base);
  font-weight: var(--fw-semibold);
  cursor: pointer;
}
.ds-state-act:hover { background: #1E3D70; }

/* Sized to sit inside a chart's own slot rather than beside it, so a
   card whose chart has no data explains itself where the chart was. */
.ds-state.is-compact {
  height: 100%;
  justify-content: center;
  padding: var(--sp-4);
  border: 1px dashed var(--st-neutral-bd);
  background: transparent;
}

.ds-state.is-error {
  border-color: var(--st-bad-bd);
  background: var(--st-bad-bg);
}
.ds-state.is-error .ds-state-icon { background: #fff; color: var(--red-ink); }
.ds-state.is-error .ds-state-body { color: var(--red-ink); }

/* ══════════════════════════════════════════════════════════════════════
   PROVENANCE LEGEND — Manual / System / Formula
   ──────────────────────────────────────────────────────────────────────
   Provenance says where a number came from. It is not status, and it
   must never be mistaken for status, so it is carried by a small solid
   swatch in one ink each — the same three inks the .kt-badge dots use.
   These were ten copies of an inline pastel style whose green was close
   enough to the "good" green that a formula-sourced KPI read as a
   passing one.
   ══════════════════════════════════════════════════════════════════════ */
.ld-m { background: var(--amber-ink); }
.ld-s { background: var(--series-1); }
.ld-f { background: var(--green-ink); }
