  /* Professional blue & white theme (2026-08-04) -- matches the blue already
     used for the Epsilon logo and the printed quotation/PO letterhead
     table (#4D71AE in #print-sheet). .techspecs-table below is
     deliberately plain black-on-white, not themed to this blue -- see the
     comment above that rule (2026-08-05 request). Every
     component in this file is built on these custom properties, so this
     block is the only place colors need to change. success/warning/danger
     stay green/amber/red on purpose -- they're semantic status colors, and
     recoloring them to blue would make errors/warnings blend into the
     primary action color instead of standing out. */
  :root {
    --bg-page: #E4EAF3;
    --surface-1: #FFFFFF;
    --surface-2: #FFFFFF;
    --border: #D2DCEA;
    --border-strong: #A9BCD6;
    --text-primary: #16233D;
    --text-secondary: #3A4A5F;
    --text-muted: #657385;
    --accent-fill: #1E3A5F;
    --accent-bg: #DCE4F0;
    --accent-text: #152A45;
    --warning-fill: #B5730E;
    --warning-bg: #FBEEDA;
    --warning-text: #613905;
    --success-fill: #2C7A4B;
    --success-bg: #E4F3EA;
    --success-text: #1D5333;
    --danger-fill: #B23A3A;
    --danger-bg: #FBEAEA;
    --danger-text: #832727;
    --radius: 8px;
    --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  }
  * { box-sizing: border-box; }
  html, body { margin: 0; background: var(--bg-page); font-family: var(--font-sans); color: var(--text-primary); }
  h1, h2, h3, p { margin: 0; }
  label { font-size: 12px; color: var(--text-secondary); display: block; margin: 10px 0 4px; }
  input, select, textarea {
    width: 100%; font-size: 14px; padding: 9px 10px; border-radius: var(--radius);
    border: 0.5px solid var(--border-strong); background: var(--surface-1); color: var(--text-primary); font-family: var(--font-sans);
  }
  /* 2026-09-14 fix: the blanket width:100% above was also stretching every
     checkbox/radio to fill its container. Harmless-looking alone, but as a
     flex item next to a label span (.checkbox-grid, admin.js's Page access /
     Dashboard KPIs / bulk pickers) that meant the checkbox's flex-basis
     claimed the ENTIRE column width, leaving the label text 0 width to work
     with — text still had to render somewhere, so it overflowed past the
     column edge instead of wrapping, and the next column's checkbox ended up
     stranded at the cut-off point. Reported live as looking like every row
     had two checkboxes. Checkboxes/radios were never meant to stretch. */
  input[type="checkbox"], input[type="radio"] { width: auto; }
  textarea { resize: vertical; }
  button {
    font-family: var(--font-sans); font-size: 13px; padding: 9px 14px; border-radius: var(--radius);
    border: 0.5px solid var(--border-strong); background: var(--surface-2); color: var(--text-primary); cursor: pointer;
  }
  button.primary { background: var(--accent-fill); color: #fff; border: none; }
  button.success { background: var(--success-fill); color: #fff; border: none; }
  button.danger { background: var(--danger-fill); color: #fff; border: none; }
  button:disabled { color: var(--text-muted); cursor: not-allowed; opacity: 0.6; }
  button:hover:not(:disabled) { filter: brightness(0.97); }

  .badge { font-size: 11px; padding: 3px 8px; border-radius: var(--radius); display: inline-block; }
  .badge.warning { background: var(--warning-bg); color: var(--warning-text); }
  .badge.success { background: var(--success-bg); color: var(--success-text); }
  .badge.danger { background: var(--danger-bg); color: var(--danger-text); }
  .badge.neutral { background: var(--border); color: var(--text-secondary); }

  .card { background: var(--surface-1); border-radius: var(--radius); padding: 14px; border: 1px solid var(--border); box-shadow: 0 1px 2px rgba(22, 35, 61, 0.05); }
  .bordered { border: 0.5px solid var(--border); border-radius: var(--radius); padding: 14px; background: var(--surface-2); }

  #login-screen {
    min-height: 100vh; display: flex; align-items: center; justify-content: center;
    background: radial-gradient(ellipse 900px 600px at 50% -10%, var(--accent-bg), var(--bg-page) 70%);
  }
  .login-box {
    width: 320px; background: var(--surface-2); border-radius: 16px; padding: 32px;
    border: 0.5px solid var(--border-strong);
    box-shadow: 0 24px 48px -12px rgba(22, 35, 61, 0.18);
  }
  .login-box .login-logo { display: block; width: 190px; max-width: 100%; margin: 0 auto 6px; }
  .login-box h1 { font-size: 18px; font-weight: 500; text-align: center; }
  .login-box .sub { font-size: 13px; color: var(--text-secondary); text-align: center; margin-top: 4px; }
  .login-box .demo { font-size: 11px; color: var(--text-muted); margin-top: 14px; line-height: 1.6; text-align: center; }
  .error-text { color: var(--danger-text); background: var(--danger-bg); padding: 8px 10px; border-radius: var(--radius); font-size: 12px; margin-top: 10px; display: none; }

  #app-shell { display: none; min-height: 100vh; }
  #app-shell.visible { display: flex; }
  /* flex-shrink: 0 + min-width pin the sidebar at exactly 200px on every page.
     Without these, a page whose .main content is wider than the viewport
     (a table with many columns — Purchase Report, PO Balance Due, Admin, ...)
     forces the flexbox's implicit `min-width: auto` sizing to steal space
     from the sidebar, silently shrinking it on those pages while pages with
     narrower content (like Dashboard) kept it at full size. See the matching
     `min-width: 0; overflow-x: auto;` on .main below, which gives wide
     content its own horizontal scrollbar instead of pushing on the sidebar. */
  /* position:relative + a z-index above .main's default (auto/0) so the
     .notif-panel dropdown (a descendant, position:absolute) reliably paints
     above .main's content — without this, the sidebar's own entry animation
     (epSlideL, animates transform) makes .sidebar form its OWN stacking
     context, and since .main comes later in the DOM with an equally-implicit
     z-index, .main would otherwise win ties and visually cover the dropdown
     even though .notif-panel's own z-index is higher than anything in .main. */
  /* position:sticky + height:100vh keeps the nav in view on long pages
     instead of scrolling away with .main's content (there's no separate
     scroll container for .main -- the whole document scrolls together, so
     sticky is what pins the sidebar to the viewport). overflow-y:auto so a
     sidebar taller than the viewport gets its own scrollbar rather than
     clipping the bottom nav items. Reset back to a normal top bar in the
     max-width:800px mobile layout below, where the sidebar becomes
     horizontal instead of a full-height column. */
  .sidebar { width: 200px; flex-shrink: 0; min-width: 200px; background: var(--surface-1); border-right: 0.5px solid var(--border); padding: 18px 12px; display: flex; flex-direction: column; gap: 4px; position: sticky; top: 0; height: 100vh; overflow-y: auto; z-index: 5; }
  .sidebar .brand { padding: 0 8px 16px; }
  .sidebar .brand img { display: block; width: 132px; max-width: 100%; }
  .sidebar .brand .brand-caption { font-size: 11px; font-weight: 600; color: var(--text-secondary); letter-spacing: 0.02em; margin-top: 2px; }
  .sidebar .notif-pill { display: none; align-items: center; gap: 6px; margin: -8px 4px 12px; padding: 7px 10px; border-radius: var(--radius); background: var(--danger-bg); color: var(--danger-text); font-size: 12px; cursor: pointer; border: none; text-align: left; width: calc(100% - 8px); }
  .sidebar .notif-pill .notif-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--danger-fill); flex-shrink: 0; }

  /* ---------------- Notifications bell + history panel (2026-08-01) ---------------- */
  /* Separate from .notif-pill above (the live "needs your attention" count) —
     this is the popup + durable history for "your quotation was approved/
     rejected" type events. See refreshUserNotifications()/notifyUsers() in
     core.js. */
  .notif-bell-wrap { position: relative; margin: 0 4px 10px; }
  .notif-bell { display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%; padding: 8px 10px; border-radius: var(--radius); background: transparent; border: 0.5px solid var(--border); color: var(--text-primary); font-size: 12.5px; cursor: pointer; position: relative; }
  .notif-bell:hover { background: var(--bg-page); }
  .notif-bell-icon { font-size: 14px; line-height: 1; }
  .notif-bell-badge { display: none; align-items: center; justify-content: center; position: absolute; top: -6px; right: -6px; min-width: 17px; height: 17px; padding: 0 4px; border-radius: 999px; background: var(--danger-fill); color: #fff; font-size: 10px; font-weight: 700; line-height: 1; }
  /* position:fixed (not absolute) — .sidebar has overflow-y:auto for its own
     nav scrolling (see the comment above .sidebar), and per the CSS overflow
     spec, setting only one axis to a non-visible value computes the OTHER
     axis to 'auto' too (there's no way to keep one axis clipped and the
     other genuinely visible on the same element). Since .notif-panel used to
     be position:absolute — a descendant of .notif-bell-wrap, itself inside
     .sidebar — that silent overflow-x:auto clipped off however much of the
     300px-wide panel extended past the 200px sidebar, which is most of it.
     position:fixed escapes that entirely (.sidebar has no transform/filter/
     perspective/contain, so it isn't a containing block for fixed
     descendants) — top/left are set from JS in wireNotifBell() (core.js)
     from the bell's own on-screen position when it's opened, since a fixed
     element can no longer inherit position from its (no longer relevant)
     relatively-positioned ancestor the way absolute did. */
  .notif-panel { display: none; position: fixed; width: 300px; max-width: calc(100vw - 40px); background: var(--surface-2); border: 0.5px solid var(--border-strong); border-radius: var(--radius); box-shadow: 0 8px 24px rgba(0,0,0,0.16); z-index: 150; overflow: hidden; text-align: left; }
  .notif-panel.open { display: block; }
  /* The panel itself caps at 300px regardless of viewport (see
     positionNotifPanel() in core.js), too narrow to reliably fit
     "Notifications" plus two buttons on one line once the push-notifications
     toggle (2026-08-26) joins "Mark all read" — so this always wraps to two
     rows: the label, then every button beneath it, rather than risk
     clipping/overlap on any screen size. */
  .notif-panel-header { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; padding: 10px 12px; border-bottom: 0.5px solid var(--border); font-size: 12px; font-weight: 700; color: var(--text-primary); }
  .notif-panel-header > span { flex: 1 0 100%; }
  .notif-panel-header button { font-size: 11px; padding: 4px 8px; }
  .notif-panel-body { max-height: 340px; overflow-y: auto; }
  .notif-empty { padding: 22px 12px; text-align: center; color: var(--text-muted); font-size: 12px; }
  .notif-item { padding: 10px 12px; border-bottom: 0.5px solid var(--border); border-left: 3px solid transparent; cursor: pointer; }
  .notif-item:last-child { border-bottom: none; }
  .notif-item:hover { background: var(--bg-page); }
  .notif-item.unread { background: var(--accent-bg); border-left-color: var(--accent-fill); }
  .notif-item.unread.type-approved { border-left-color: var(--success-fill); }
  .notif-item.unread.type-rejected { border-left-color: var(--danger-fill); }
  .notif-item-msg { font-size: 12.5px; color: var(--text-primary); line-height: 1.4; }
  .notif-item.unread .notif-item-msg { font-weight: 600; }
  .notif-item-time { font-size: 11px; color: var(--text-muted); margin-top: 3px; }

  .sidebar .navbtn { text-align: left; border: none; background: transparent; padding: 9px 10px; border-radius: var(--radius); font-size: 13px; color: var(--text-primary); }
  .sidebar .navbtn.active { background: var(--accent-fill); color: #fff; }
  .sidebar .userinfo { margin-top: auto; padding: 10px 8px; font-size: 12px; color: var(--text-secondary); border-top: 0.5px solid var(--border); }
  .sidebar .userinfo b { color: var(--text-primary); display: block; font-size: 13px; }

  .main { flex: 1; min-width: 0; overflow-x: auto; padding: 28px 34px; max-width: 1100px; }
  .main h2 { font-size: 18px; font-weight: 600; margin-bottom: 4px; }
  .main .pagesub { font-size: 13px; color: var(--text-secondary); margin-bottom: 18px; }

  .view { display: none; }
  .view.visible { display: block; }

  table.data { width: 100%; border-collapse: collapse; font-size: 13px; background: var(--surface-2); border-radius: var(--radius); overflow: hidden; }
  table.data th { text-align: left; background: var(--surface-1); color: var(--text-secondary); font-weight: 500; padding: 8px 10px; border-bottom: 0.5px solid var(--border); }
  table.data td { padding: 8px 10px; border-bottom: 0.5px solid var(--border); }
  table.data tr:last-child td { border-bottom: none; }

  /* ---------------- Tabs (2026-08-07) — splits a page with several large
     tables into one-at-a-time panels instead of stacking them all, so the
     page itself doesn't grow tall enough to need scrolling. First used on
     the PO Coordinator's Purchase Report (Pending Invoice / All Purchase
     Orders). ---------------- */
  .tab-row { display: flex; gap: 4px; border-bottom: 1.5px solid var(--border); margin-bottom: 14px; }
  .tab-btn { background: transparent; border: none; border-bottom: 2px solid transparent; border-radius: 0; padding: 9px 4px; margin-right: 18px; font-size: 13.5px; font-weight: 600; color: var(--text-muted); cursor: pointer; }
  .tab-btn:hover { color: var(--text-primary); }
  .tab-btn.active { color: var(--accent-fill); border-bottom-color: var(--accent-fill); }

  /* Lets a long table scroll within its own bounded area (sticky header
     included) instead of pushing the whole page taller — the page around it
     (Overview cards, tabs, filters) always stays in view. */
  .table-scroll-area { max-height: calc(100vh - 340px); min-height: 220px; overflow: auto; border-radius: var(--radius); }
  .table-scroll-area table.data { border-radius: 0; }
  .table-scroll-area table.data th { position: sticky; top: 0; z-index: 1; }

  .stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 22px; }
  /* Stat cards get a top accent bar (overlaid on the card's own border-radius
     via border-top + matching corner radii) so a screen full of otherwise
     identical white cards reads as a set of distinct, deliberately designed
     tiles instead of a flat, borderless wall of boxes. */
  .stat-card { border-top: 3px solid var(--accent-fill); border-radius: var(--radius); padding-top: 12px; }
  .stat-card p.label { font-size: 12.5px; font-weight: 600; color: var(--text-secondary); letter-spacing: 0.01em; }
  .stat-card p.value { font-size: 24px; font-weight: 700; margin-top: 4px; color: var(--text-primary); letter-spacing: -0.01em; }

  /* ---- Dashboard KPI cards (2026-08-29 redesign, revised after direct
     comparison against the actual reference design) ----
     Two card types per DASHBOARD_KPI_DEFS[].variant (core.js): 'hero' cards
     get a full tinted background + icon badge (the section's "things going
     well" card/s); 'bordered' cards stay white with a colored left accent +
     optionally colored value text (supporting/attention states). Both read
     their color from one of the --dash-tint/--dash-accent/--dash-accent-text
     custom properties set by the .color-* classes below, so adding a new
     KPI in a given color is just adding the class, not new rules. */
  .color-purple { --dash-tint: #F3EEFC; --dash-accent: #7C3AED; --dash-accent-text: #6D28D9; }
  .color-green  { --dash-tint: #E9F9EF; --dash-accent: #16A34A; --dash-accent-text: #15803D; }
  .color-red    { --dash-tint: #FCEBEC; --dash-accent: #DC2626; --dash-accent-text: #DC2626; }
  .color-blue   { --dash-tint: #EAF2FD; --dash-accent: #2563EB; --dash-accent-text: #1D4ED8; }
  .color-orange { --dash-tint: #FFF1E4; --dash-accent: #EA8A23; --dash-accent-text: #C2670F; }
  .color-yellow { --dash-tint: #FEF6DD; --dash-accent: #CA8A04; --dash-accent-text: #A16207; }
  .color-teal   { --dash-tint: #E6F7F6; --dash-accent: #0D9488; --dash-accent-text: #0F766E; }
  .color-gray   { --dash-tint: #F1F3F6; --dash-accent: #94A3B8; --dash-accent-text: #475569; }

  .dash-kpi { border-top-width: 0; }
  .dash-kpi-label { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-secondary); }
  .dash-kpi .value { font-size: 22px; font-weight: 700; margin-top: 4px; color: var(--text-primary); letter-spacing: -0.01em; }
  .dash-kpi-value { font-size: 24px; margin-top: 6px; }

  .dash-kpi-hero { background: var(--dash-tint); padding: 16px 18px; }
  .dash-kpi-icon {
    display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px;
    border-radius: 50%; background: rgba(255,255,255,0.75); font-size: 16px; margin-bottom: 10px;
  }
  .dash-kpi-trend {
    font-size: 11.5px; font-weight: 700; margin-top: 10px; display: inline-block;
    padding: 3px 8px; border-radius: 999px; background: rgba(255,255,255,0.6);
  }
  .dash-kpi-trend.trend-up { color: #15803D; }
  .dash-kpi-trend.trend-down { color: #DC2626; }

  .dash-kpi-bordered { background: var(--surface-1); border-left: 4px solid var(--dash-accent); padding: 13px 16px; }
  .dash-kpi-bordered.value-colored .value { color: var(--dash-accent-text); }

  /* 2026-09-28 fix: 'grid' sections (Logistics & Invoicing, AERB Status)
     used to mix 'hero'-variant cards (tinted background, icon) with
     'bordered'-variant cards (plain white, no icon) in the same row --
     reported live as looking unfinished, with the icon on some cards and
     not others and the value landing at a different height across the
     row. Every card in these two sections now renders through this one
     class instead (gridCardHtml(), dashboard.js): same tinted background,
     same icon slot, same label/value position, whatever card it is. */
  .dash-kpi-tile { background: var(--dash-tint); padding: 14px 16px; }
  .dash-kpi-tile .dash-kpi-icon { width: 30px; height: 30px; font-size: 14px; margin-bottom: 8px; }
  .dash-kpi-tile .value { color: var(--text-primary); }
  .dash-kpi-tile.value-colored .value { color: var(--dash-accent-text); }
  /* Same lift-on-hover affordance .stat-card already gives its clickable
     cards (see the Coordinator's "Needs attention" tiles, dashboard.js),
     generalized to any card that's actually clickable rather than
     re-declared per card style. */
  .card[data-quick-po]:hover { transform: translate3d(0, var(--motion-lift), 0); }
  .card[data-quick-po]:hover::before { opacity: 1; }

  /* ---- Dashboard sections ---- */
  .dash-section { margin-bottom: 26px; }
  .dash-section-title {
    display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 700;
    color: var(--text-primary); margin: 0 0 12px;
  }
  .dash-section-icon { font-size: 17px; line-height: 1; }
  .dash-section-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }

  /* 'quad' sections (Financials & Orders, Approval Flow): the hero card
     spans both rows of a 3-column grid on the left; the other three sit
     2-over-1 beside it -- matches the reference exactly (confirmed by
     direct comparison, not a guess). Falls back to dash-section-grid's
     plain equal-card layout (in sectionHtml(), dashboard.js) if the hero
     KPI itself is hidden for this viewer. */
  .dash-quad-grid { display: grid; grid-template-columns: 1.3fr 1fr 1fr; grid-template-rows: auto auto; gap: 12px; }
  .dash-quad-grid > *:nth-child(1) { grid-column: 1; grid-row: 1 / 3; display: flex; flex-direction: column; justify-content: center; }
  .dash-quad-grid > *:nth-child(2) { grid-column: 2; grid-row: 1; }
  .dash-quad-grid > *:nth-child(3) { grid-column: 3; grid-row: 1; }
  .dash-quad-grid > *:nth-child(4) { grid-column: 2 / 4; grid-row: 2; }

  /* Target Performance's two hero cards -- bigger, richer than a plain KPI
     card (a progress bar or ring, an icon badge, a colored pill), always
     exactly 2 columns regardless of the grids the other sections use,
     since there are only ever the two of them. */
  .dash-hero-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
  .dash-target-hero { padding: 18px 20px; }
  .dash-target-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
  .dash-target-top .dash-kpi-icon { margin-bottom: 0; }
  .dash-target-badge { font-size: 11px; font-weight: 700; padding: 3px 10px; border-radius: 999px; background: rgba(255,255,255,0.75); color: var(--dash-accent-text); }
  .dash-hero-progress-row {
    display: flex; justify-content: space-between; font-size: 12px; font-weight: 600;
    color: var(--text-secondary); margin-top: 16px;
  }
  .dash-target-progress { margin-top: 6px; }
  /* Extra specificity (ancestor class, not just the bar's own) needed to
     beat the base .progress > span rule further down this file at EQUAL
     specificity -- source order alone was letting that later rule win,
     leaving this bar the generic app-blue instead of the card's own green. */
  .dash-target-hero .dash-target-progress > span { background: var(--dash-accent); }
  .dash-hero-footnote { font-size: 12px; color: var(--text-secondary); margin-top: 10px; }
  .dash-hero-ring-row { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-top: 6px; }
  .dash-hero-ring-text { min-width: 0; }
  .dash-hero-remaining-label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-secondary); margin-top: 14px; }
  .dash-hero-remaining-value { font-size: 17px; font-weight: 700; color: var(--text-primary); margin-top: 2px; }
  .dash-target-hero-ring .gauge { width: 70px; height: 70px; flex-shrink: 0; }
  .dash-target-hero-ring .gauge-arc { stroke: var(--dash-accent); }
  .dash-target-hero-ring .gauge-pct { font-size: 15px; }

  /* Circular gauge — Target performance cards that have a % (Target
     completed, Remaining target). Replaces the old linear .progress bar
     with a ring + centered percentage. */
  .stat-card.with-gauge { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
  .stat-card.with-gauge .stat-card-text { min-width: 0; }
  .gauge { position: relative; width: 56px; height: 56px; flex-shrink: 0; }
  .gauge svg { width: 100%; height: 100%; }
  .gauge .gauge-arc { transition: stroke-dashoffset var(--motion-count) var(--motion-ease); }
  .gauge-pct { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; color: var(--text-primary); }

  .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 16px; }
  .form-grid .full { grid-column: 1 / -1; }

  .section-title {
    font-size: 13px; font-weight: 700; margin: 22px 0 10px; color: var(--text-secondary);
    text-transform: uppercase; letter-spacing: 0.04em; position: relative; padding-left: 12px;
  }
  .section-title::before {
    content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
    width: 4px; height: 13px; border-radius: 2px; background: var(--accent-fill);
  }

  .toast { position: fixed; top: 18px; right: 18px; background: var(--text-primary); color: #fff; padding: 10px 16px; border-radius: var(--radius); font-size: 13px; opacity: 0; transform: translateY(-8px); transition: all 0.2s; pointer-events: none; z-index: 50; }
  .toast.show { opacity: 1; transform: translateY(0); }

  .pill-row { display: flex; gap: 8px; flex-wrap: wrap; margin: 6px 0; }
  .pill { font-size: 12px; padding: 6px 10px; border-radius: 999px; border: 0.5px solid var(--border-strong); background: var(--surface-2); cursor: pointer; user-select: none; }
  .pill.selected { background: var(--accent-bg); border-color: var(--accent-fill); color: var(--accent-text); }

  /* Radio-backed pill toggle (2026-08-15, Purchase Order's "Generate PO"
     mode picker) — same .pill look as the accessory/option chips above,
     driven by a real <input type="radio"> wrapped inside instead of a
     JS-toggled .selected class, so it stays a plain native form control
     (submit handler keeps reading .checked, completely unchanged) while
     looking like a deliberate segmented choice instead of a bare pair of
     radio buttons. `label` defaults to display:block app-wide (see the
     global `label` rule above), so this overrides that to inline-flex to
     get the compact pill shape back. */
  .pill.toggle-pill { display: inline-flex; align-items: center; gap: 6px; }
  .pill.toggle-pill input[type="radio"] {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }
  .pill.toggle-pill:has(input:checked) { background: var(--accent-bg); border-color: var(--accent-fill); color: var(--accent-text); font-weight: 700; }

  /* Segmented single-choice control (DSR Report's All / Multiple Visits /
     Under-visited filter, 2026-08-26) — real radio buttons visually hidden,
     one continuous pill-shaped track with the active option rendered as a
     solid accent-filled inner chip. Only one of the three is ever meaningful
     at a time, so this is a genuine radio group, not independent toggles. */
  .segmented { display: inline-flex; align-items: center; gap: 2px; padding: 3px; background: var(--surface-2); border: 0.5px solid var(--border-strong); border-radius: 999px; }
  .segmented .segment { display: inline-flex; }
  .segmented input[type="radio"] {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  }
  .segmented .segment span {
    display: inline-block; padding: 6px 14px; border-radius: 999px; font-size: 13px; font-weight: 500;
    color: var(--text-secondary); cursor: pointer; white-space: nowrap;
    transition: background var(--motion-fast) ease, color var(--motion-fast) ease;
  }
  .segmented input:checked + span { background: var(--accent-fill); color: #fff; font-weight: 600; }
  .segmented input:focus-visible + span { outline: 2px solid var(--accent-fill); outline-offset: 2px; }

  .hint { font-size: 12px; color: var(--text-muted); margin-top: 6px; }

  /* Dashboard's filter dropdown(s) + PO-scope note, side by side (2026-08-29
     redesign — see dashboard.html). The note is a bordered/tinted callout,
     not a plain paragraph, and takes whatever width the filter box(es)
     don't use (or the full row, via flex-basis, when neither filter shows). */
  .dash-filter-row { display: flex; flex-wrap: wrap; align-items: stretch; gap: 12px; margin-bottom: 16px; }
  .dash-filter-note {
    flex: 1 1 260px; margin: 0; padding: 10px 14px; border-radius: var(--radius);
    background: var(--surface-2); border: 1px solid var(--border);
    font-size: 12px; color: var(--text-secondary); display: flex; align-items: center; gap: 8px;
  }
  .dash-filter-note::before { content: 'ⓘ'; flex-shrink: 0; font-size: 14px; color: var(--text-muted); }
  .locked-note { font-size: 12px; color: var(--warning-text); background: var(--warning-bg); padding: 8px 10px; border-radius: var(--radius); margin-top: 10px; }

  /* ================= Motion / animation =================
     PERFORMANCE RULE, no exceptions: only `transform` and `opacity` are
     ever animated. Those two are composited on the GPU, so a frame costs
     nothing extra. The first version of this block animated `filter` on
     every button, `box-shadow` on card hover, and `background-position`
     on an infinite loading shimmer — all three force a full repaint on
     the main thread every frame, and the shimmer never stopped. That is
     what made the UI feel heavy. Shadows and glows are still here, but
     they are now painted once on a pseudo-element and only its OPACITY
     is animated, which is free.

     Three intensity tiers, switchable from Settings -> Appearance and
     remembered per browser:
       body.motion-calm  — minimal, for anyone who finds motion busy
       body.motion-std   — fast and understated
       (default = max)   — full effects: 3D card flips, click ripples,
                           shine sweeps, confetti when a monthly target
                           is hit
     Note the durations get SHORTER, not longer, as the effects get
     bigger. "Dramatic" and "slow" are independent axes, and slow is the
     one that reads as laggy — so the biggest tier is also the snappiest
     it can be while still letting each effect register.

     Placed AFTER the base component styles on purpose: several rules
     here deliberately override earlier declarations (button hover,
     .toast), so source order matters. */
  :root {
    --motion-fast: 95ms;
    --motion: 155ms;
    --motion-slow: 240ms;
    --motion-count: 620ms;
    --motion-stagger: 34ms;
    --motion-ease: cubic-bezier(0.2, 0.9, 0.3, 1);
    --motion-bounce: cubic-bezier(0.34, 2.1, 0.5, 1);
    --motion-lift: -5px;
  }
  body.motion-std {
    --motion-fast: 90ms;
    --motion: 140ms;
    --motion-slow: 200ms;
    --motion-count: 450ms;
    --motion-stagger: 22ms;
    --motion-bounce: cubic-bezier(0.34, 1.5, 0.6, 1);
    --motion-lift: -2px;
  }
  body.motion-calm {
    --motion-fast: 70ms;
    --motion: 110ms;
    --motion-slow: 140ms;
    --motion-count: 260ms;
    --motion-stagger: 0ms;
    --motion-bounce: cubic-bezier(0.2, 0.9, 0.3, 1);
    --motion-lift: -1px;
  }

  /* Every keyframe below touches transform/opacity only. */
  @keyframes epFadeIn   { from { opacity: 0; } to { opacity: 1; } }
  @keyframes epRise     { from { opacity: 0; transform: translate3d(0, 10px, 0); } to { opacity: 1; transform: none; } }
  @keyframes epSlideL   { from { opacity: 0; transform: translate3d(-18px, 0, 0); } to { opacity: 1; transform: none; } }
  @keyframes epPop      { from { opacity: 0; transform: scale(0.93) translate3d(0, 10px, 0); } to { opacity: 1; transform: none; } }
  @keyframes epFlipIn   { from { opacity: 0; transform: perspective(760px) rotateX(-32deg) translate3d(0, 16px, 0); } to { opacity: 1; transform: perspective(760px) rotateX(0deg) translate3d(0, 0, 0); } }
  @keyframes epViewIn   { from { opacity: 0; transform: translate3d(18px, 0, 0); } to { opacity: 1; transform: none; } }
  @keyframes epSpin     { to { transform: rotate(360deg); } }
  @keyframes epSweep    { from { transform: translate3d(-100%, 0, 0); } to { transform: translate3d(220%, 0, 0); } }
  @keyframes epRipple   { to { transform: scale(2.8); opacity: 0; } }
  @keyframes epNumPop   { 0% { transform: none; } 45% { transform: scale(1.16); } 100% { transform: none; } }
  @keyframes epConfetti { to { transform: translate3d(var(--cx), var(--cy), 0) rotate(var(--cr)); opacity: 0; } }
  @keyframes epBadgeIn  { from { opacity: 0; transform: scale(0.4); } to { opacity: 1; transform: none; } }

  /* ---- Login entrance: box pops in, then logo/subtitle/fields/button
     cascade in on their own beat instead of appearing instantly the moment
     the box lands. ---- */
  #login-screen .login-box { animation: epPop var(--motion-slow) var(--motion-bounce) both; }
  #login-screen .login-logo { animation: epRise var(--motion-slow) var(--motion-ease) both; animation-delay: 60ms; }
  #login-screen .login-box .sub { animation: epRise var(--motion-slow) var(--motion-ease) both; animation-delay: 110ms; }
  #login-screen .login-box #login-form > label,
  #login-screen .login-box #login-form > input { animation: epRise var(--motion) var(--motion-ease) both; animation-delay: 160ms; }
  #login-screen .login-box #login-form > button { animation: epPop var(--motion-slow) var(--motion-bounce) both; animation-delay: 230ms; }

  /* ---- Sidebar entrance + sliding active indicator ---- */
  #app-shell.visible .sidebar { animation: epSlideL var(--motion-slow) var(--motion-ease) both; }
  #app-shell.visible .sidebar .navbtn { animation: epRise var(--motion) var(--motion-ease) both; }
  #app-shell.visible .sidebar .navbtn:nth-of-type(1) { animation-delay: calc(var(--motion-stagger) * 1); }
  #app-shell.visible .sidebar .navbtn:nth-of-type(2) { animation-delay: calc(var(--motion-stagger) * 2); }
  #app-shell.visible .sidebar .navbtn:nth-of-type(3) { animation-delay: calc(var(--motion-stagger) * 3); }
  #app-shell.visible .sidebar .navbtn:nth-of-type(4) { animation-delay: calc(var(--motion-stagger) * 4); }
  #app-shell.visible .sidebar .navbtn:nth-of-type(5) { animation-delay: calc(var(--motion-stagger) * 5); }
  #app-shell.visible .sidebar .navbtn:nth-of-type(n+6) { animation-delay: calc(var(--motion-stagger) * 6); }
  .sidebar .navbtn { position: relative; }
  .sidebar .navbtn::before {
    content: ''; position: absolute; left: 2px; top: 7px; bottom: 7px; width: 3px; border-radius: 3px;
    background: #fff; opacity: 0; transform: scaleY(0); transform-origin: center;
    transition: opacity var(--motion-fast) linear, transform var(--motion) var(--motion-bounce);
  }
  .sidebar .navbtn.active::before { opacity: 1; transform: scaleY(1); }

  /* ---- Page/view change ---- */
  .view.visible { animation: epViewIn var(--motion) var(--motion-ease) both; }

  /* ---- Buttons: press feedback + click ripple (no filter, no shadow) ---- */
  button {
    position: relative; overflow: hidden;
    transition: transform var(--motion-fast) var(--motion-bounce),
                background-color var(--motion-fast) linear,
                color var(--motion-fast) linear,
                border-color var(--motion-fast) linear;
  }
  /* Overrides the earlier `filter: brightness()` hover — filter promotes a
     layer and repaints; a flat background swap does not. */
  button:hover:not(:disabled) { filter: none; transform: translate3d(0, -1px, 0); }
  button:not(.primary):not(.success):not(.danger):hover:not(:disabled) { background: var(--border); }
  button.primary:hover:not(:disabled) { background: var(--accent-text); }
  button.success:hover:not(:disabled) { background: var(--success-text); }
  button.danger:hover:not(:disabled) { background: var(--danger-text); }
  button:active:not(:disabled) { transform: translate3d(0, 0, 0) scale(0.96); }
  .sidebar .navbtn:hover:not(:disabled) { transform: none; background: var(--border); }
  .sidebar .navbtn.active:hover:not(:disabled) { background: var(--accent-fill); }
  .ep-ripple {
    position: absolute; border-radius: 50%; background: currentColor; opacity: 0.3;
    transform: scale(0); pointer-events: none;
    animation: epRipple 460ms var(--motion-ease) forwards;
  }

  /* ---- Inputs: focus ring painted once, only opacity animates ---- */
  input, select, textarea { transition: border-color var(--motion-fast) linear; }
  input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent-fill); box-shadow: 0 0 0 3px var(--accent-bg); }

  /* ---- Tables ---- */
  table.data tbody tr { transition: background-color var(--motion-fast) linear; }
  table.data tbody tr:hover { background: var(--surface-1); }
  /* Row cascade, capped at the first 10 rows so a 200-row table doesn't
     schedule 200 animations. */
  table.data tbody tr:nth-child(-n+10) { animation: epRise var(--motion) var(--motion-ease) both; }
  table.data tbody tr:nth-child(1) { animation-delay: calc(var(--motion-stagger) * 0.5); }
  table.data tbody tr:nth-child(2) { animation-delay: calc(var(--motion-stagger) * 1); }
  table.data tbody tr:nth-child(3) { animation-delay: calc(var(--motion-stagger) * 1.5); }
  table.data tbody tr:nth-child(4) { animation-delay: calc(var(--motion-stagger) * 2); }
  table.data tbody tr:nth-child(5) { animation-delay: calc(var(--motion-stagger) * 2.5); }
  table.data tbody tr:nth-child(n+6):nth-child(-n+10) { animation-delay: calc(var(--motion-stagger) * 3); }
  body.motion-calm table.data tbody tr { animation: none; }

  /* ---- Cards: lift + glow. The shadow is painted once on ::before and
         only its opacity animates, so hovering never triggers a repaint. ---- */
  .card { position: relative; transition: transform var(--motion) var(--motion-bounce); }
  .card::before {
    content: ''; position: absolute; inset: 0; border-radius: var(--radius); pointer-events: none;
    box-shadow: 0 10px 26px rgba(22, 35, 61, 0.14); opacity: 0;
    transition: opacity var(--motion) linear;
  }
  .stat-card:hover { transform: translate3d(0, var(--motion-lift), 0); }
  .stat-card:hover::before { opacity: 1; }
  .stats-grid > * { animation: epRise var(--motion) var(--motion-ease) both; }
  /* Max tier flips the cards in on the X axis — pure 3D transform. */
  body:not(.motion-std):not(.motion-calm) .stats-grid > * { animation-name: epFlipIn; animation-timing-function: var(--motion-ease); }
  .stats-grid > *:nth-child(1) { animation-delay: calc(var(--motion-stagger) * 0); }
  .stats-grid > *:nth-child(2) { animation-delay: calc(var(--motion-stagger) * 1); }
  .stats-grid > *:nth-child(3) { animation-delay: calc(var(--motion-stagger) * 2); }
  .stats-grid > *:nth-child(4) { animation-delay: calc(var(--motion-stagger) * 3); }
  .stats-grid > *:nth-child(5) { animation-delay: calc(var(--motion-stagger) * 4); }
  .stats-grid > *:nth-child(n+6) { animation-delay: calc(var(--motion-stagger) * 5); }
  /* Applied for one beat by JS the moment a count-up lands. */
  .value.ep-pop { display: inline-block; animation: epNumPop 340ms var(--motion-bounce); }

  /* ---- Pills ---- */
  .pill { transition: background-color var(--motion-fast) linear, border-color var(--motion-fast) linear, transform var(--motion-fast) var(--motion-bounce); }
  .pill:hover { border-color: var(--accent-fill); transform: translate3d(0, -2px, 0); }
  .pill.selected { animation: epBadgeIn var(--motion) var(--motion-bounce); }

  /* ---- Modals ---- */
  #edit-overlay.visible, #print-overlay.visible { animation: epFadeIn var(--motion-fast) linear both; }
  #edit-overlay.visible #edit-sheet, #print-overlay.visible #print-sheet { animation: epPop var(--motion) var(--motion-bounce) both; }

  /* ---- Toast ---- */
  .toast { transition: opacity var(--motion) linear, transform var(--motion) var(--motion-bounce); transform: translate3d(0, -18px, 0) scale(0.9); }
  .toast.show { transform: none; }

  /* ---- Dashboard greeting ---- */
  /* 2026-08-31: bumped up from the shared .main h2 size (18px) and given
     its own row so it reads as a real page headline instead of a small
     section label — reported live as too small next to the Employee
     filter, which used to sit as its own bulky bordered card below it. */
  #dash-greeting { animation: epRise var(--motion-slow) var(--motion-ease) both; font-size: 26px; margin-bottom: 0; }
  .dash-header-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 16px; }
  .dash-header-row .dash-filter-row { margin-bottom: 0; }
  /* Compact inline filter (replaces the old .bordered card) — a plain
     label+select pair, right-aligned next to the greeting instead of
     stacking below as its own full-width box. */
  .dash-filter-compact { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
  .dash-filter-compact label { font-size: 13px; color: var(--text-secondary); white-space: nowrap; }
  .dash-filter-compact select { width: 170px; }
  .dash-filter-compact select:disabled { color: var(--text-muted); }

  /* ---- Target progress bars, with a travelling shine on the max tier ---- */
  .progress { height: 6px; border-radius: 999px; background: var(--border); margin-top: 9px; overflow: hidden; }
  .progress > span {
    display: block; height: 100%; width: 0; border-radius: 999px; background: var(--accent-fill);
    position: relative; overflow: hidden;
    transition: width var(--motion-count) var(--motion-ease);
  }
  .progress.remaining > span { background: var(--warning-fill); }
  body:not(.motion-std):not(.motion-calm) .progress > span::after {
    content: ''; position: absolute; inset: 0;
    background: linear-gradient(90deg, transparent, rgba(255,255,255,0.7), transparent);
    transform: translate3d(-100%, 0, 0);
    animation: epSweep 1.9s var(--motion-ease) infinite;
  }

  /* ---- Loading skeletons ----
     The sweep is a pseudo-element moved with translate3d. The original
     version animated background-position on the element itself, which
     repainted every frame, forever, for every skeleton on screen. */
  .skel { position: relative; overflow: hidden; background: var(--border); border-radius: var(--radius); }
  .skel::after {
    content: ''; position: absolute; inset: 0;
    background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent);
    transform: translate3d(-100%, 0, 0);
    animation: epSweep 1.15s linear infinite;
  }
  .skel-card { height: 62px; }
  .skel-line { height: 12px; border-radius: 6px; margin: 11px 0; }
  .skel-line.w70 { width: 70%; }
  .skel-line.w45 { width: 45%; }

  /* ---- Inline button spinner ---- */
  button.is-loading { color: transparent !important; pointer-events: none; }
  button.is-loading::after {
    content: ''; position: absolute; top: 50%; left: 50%; width: 14px; height: 14px; margin: -7px 0 0 -7px;
    border-radius: 50%; border: 2px solid rgba(95, 94, 90, 0.3); border-top-color: rgba(95, 94, 90, 0.95);
    animation: epSpin 0.6s linear infinite;
  }
  button.primary.is-loading::after, button.success.is-loading::after, button.danger.is-loading::after {
    border-color: rgba(255, 255, 255, 0.4); border-top-color: #fff;
  }

  /* ---- Confetti burst (max tier only) — fired when a monthly target is
         reached. Each piece is one absolutely-positioned element animating
         transform+opacity, removed from the DOM when it finishes. ---- */
  #ep-confetti { position: fixed; inset: 0; pointer-events: none; z-index: 200; overflow: hidden; }
  #ep-confetti i {
    position: absolute; width: 9px; height: 14px; border-radius: 2px; will-change: transform, opacity;
    animation: epConfetti var(--dur, 1500ms) cubic-bezier(0.12, 0.62, 0.36, 1) forwards;
  }

  /* ---- Accessibility: honour the OS "reduce motion" setting ---- */
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
    #ep-confetti { display: none !important; }
  }

  /* ---- Never animate anything on paper ----
     Critical, not cosmetic: the quotation/PO "download" prints
     #print-sheet, and an entrance animation caught mid-flight would
     commit a half-faded or slightly-scaled page to the customer's PDF.
     Chrome's print engine does not reliably run animations to completion
     first, so the whole motion layer is switched off here. */
  @media print {
    *, *::before, *::after {
      animation: none !important;
      transition: none !important;
      opacity: 1 !important;
      transform: none !important;
    }
    #ep-confetti, .ep-ripple { display: none !important; }
  }

  /* ---- Print preview modal (used for quotation / PO "download") ---- */
  #print-overlay {
    display: none; position: fixed; inset: 0; background: rgba(30,30,28,0.45); z-index: 100;
    align-items: flex-start; justify-content: center; padding: 30px 16px; overflow-y: auto;
  }
  #print-overlay.visible { display: flex; }
  #print-sheet { background: #fff; width: 680px; max-width: 100%; border-radius: 10px; padding: 30px; color: #222; font-size: 13px; }
  #print-sheet table { width: 100%; border-collapse: collapse; margin-top: 14px; }
  #print-sheet td, #print-sheet th { border: 1px solid #ccc; padding: 6px 8px; text-align: left; }
  #print-sheet th { background: #EEF2F8; color: #2B2A29; }
  #print-sheet h1 { font-size: 18px; }
  #print-sheet h2 { font-size: 15px; margin-top: 6px; }
  .print-toolbar { display: flex; justify-content: flex-end; gap: 8px; margin-bottom: 14px; }

  /* ---- Letterhead (real company header/footer banner images) ----
     NOTE: these render inline in normal document flow, NOT position:fixed.
     Chrome's print engine repeats position:fixed elements visually on every
     page but does not reserve margin space for them past page 1, which
     caused flowing content to overlap the header on page 2+ (see git history
     for that attempt). A later attempt forced a page break before every major
     section with a repeating header/footer pair, but that left large blank
     gaps for short sections and could spill a couple of lines onto their own
     near-blank page when a section happened to end near a natural page
     boundary. Current approach: only ONE header (top of page 1) and ONE
     footer (true end of the document); everything in between flows
     naturally with no forced breaks, so pages fill up before breaking and
     no page is ever mostly blank. Trade-off: on a quotation long enough to
     overflow past page 1 from natural flow, the letterhead won't repeat on
     those later pages — accepted as the more honest option vs. either bug
     above. */
  .letterhead-header { display: block; width: 100%; height: auto; margin: -30px -30px 18px; }
  .letterhead-footer { display: block; width: 100%; height: auto; margin: 24px -30px -30px; }
  .doc-section-title {
    text-decoration: none; font-weight: 700; color: #4D71AE; border-bottom: 2px solid #4D71AE;
    padding-bottom: 4px; margin-top: 22px; display: block;
  }
  /* Plain black-on-white by request (2026-08-05) -- the bold/blue section-row
     styling this table used to carry read as "unprofessional" in the actual
     downloaded PDF, so every row (section or detail) now shares one plain style. */
  .techspecs-table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12.5px; }
  .techspecs-table td, .techspecs-table th { border: 1px solid #ccc; padding: 5px 8px; text-align: left; vertical-align: top; font-weight: normal; background: #fff; color: #2B2A29; }

  @page { size: A4; margin: 12mm 10mm; }
  @media print {
    /* #print-sheet's white background only covers its own (auto-height) content, not
       the full physical page — on any page where content doesn't reach the bottom
       (typically the last page), the app's page background (--bg-page, a light beige)
       would otherwise show through below it instead of plain white. */
    html, body { background: #fff !important; }
    body > *:not(#print-overlay) { display: none !important; }
    #print-overlay { position: static !important; background: none !important; display: block !important; padding: 0 !important; }
    #print-sheet { box-shadow: none !important; width: 100% !important; padding: 26px 30px 30px !important; }
    .no-print { display: none !important; }
  }

  /* ---- Org tree (collapsible drill-down accordion) ---- */
  .org-tree { display: flex; flex-direction: column; gap: 6px; }
  .org-node { }
  .org-row {
    display: flex; align-items: center; gap: 10px; background: var(--surface-2);
    border: 0.5px solid var(--border-strong); border-radius: var(--radius);
    padding: 10px 12px; cursor: pointer; user-select: none;
  }
  .org-row:hover { border-color: var(--accent-fill); }
  .org-row.open { border-color: var(--accent-fill); background: var(--accent-bg); }
  .org-caret {
    display: inline-flex; align-items: center; justify-content: center; width: 16px; color: var(--text-muted);
    transition: transform 0.15s ease; flex-shrink: 0;
  }
  .org-row.open .org-caret { transform: rotate(90deg); color: var(--accent-text); }
  .org-info { display: flex; flex-direction: column; flex: 1; min-width: 0; }
  .org-info b { font-size: 13px; }
  .org-info span { font-size: 11px; color: var(--text-secondary); }
  .org-count { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
  .org-stats { font-size: 12px; padding: 8px 12px 8px 38px; display: flex; gap: 6px; flex-wrap: wrap; }
  .org-children { margin: 6px 0 0 26px; padding-left: 14px; border-left: 1px solid var(--border); display: flex; flex-direction: column; gap: 6px; }

  /* ---- Generic edit modal (DSR edit, approval override) ---- */
  #edit-overlay { display: none; position: fixed; inset: 0; background: rgba(30,30,28,0.45); z-index: 110; align-items: center; justify-content: center; padding: 20px; }
  #edit-overlay.visible { display: flex; }
  #edit-sheet { background: var(--surface-2); width: 560px; max-width: 100%; border-radius: 12px; padding: 24px; max-height: 90vh; overflow-y: auto; }
  #edit-sheet h3 { font-size: 15px; margin-bottom: 12px; }

  /* Two-column checkbox lists (Page access / Dashboard KPIs / bulk pickers,
     admin.js). Grid row height stretches to fit the taller of each row's two
     items -- top-aligning every checkbox+label pair (rather than centering)
     keeps each one flush with its own text instead of floating in whatever
     extra height its unrelated column neighbor forced onto the row (2026-08-06
     fix: paired items don't relate to each other's length, so centering made
     short single-line labels drift down against a long two-line neighbor).
     minmax(0, 1fr) -- not plain 1fr (2026-09-14 fix): a bare 1fr column is
     really minmax(auto, 1fr), so it refuses to shrink below its widest row's
     min-content -- and a flex row's text span carries the same "won't shrink
     below my content" rule by default. Together, a long label (e.g. the
     Super Developer per-person grant hint text) blew the whole grid wider
     than the modal instead of wrapping, forcing the modal itself into
     horizontal scroll and leaving the far column's checkbox stranded at the
     cut-off edge -- reported live as looking like every row had two
     checkboxes. minmax(0, ...) plus min-width: 0 on both the flex row and
     its text let the column actually shrink to the modal's width and wrap
     the text like everywhere else instead. */
  .checkbox-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 10px 16px; }
  .checkbox-grid label { display: flex; align-items: flex-start; gap: 8px; line-height: 1.35; cursor: pointer; min-width: 0; }
  .checkbox-grid label > span { min-width: 0; }
  .checkbox-grid input[type="checkbox"] { flex-shrink: 0; margin-top: 2px; }

  @media (max-width: 800px) {
    #app-shell.visible { flex-direction: column; }
    .sidebar { width: 100%; min-width: 0; flex-direction: row; overflow-x: auto; }
    .sidebar .userinfo { display: none; }
    .stats-grid { grid-template-columns: repeat(2, 1fr); }
    .dash-section-grid { grid-template-columns: repeat(2, 1fr); }
    .dash-hero-grid { grid-template-columns: 1fr; }
    .dash-quad-grid { grid-template-columns: 1fr; }
    .dash-quad-grid > * { grid-column: auto !important; grid-row: auto !important; }
    .form-grid { grid-template-columns: 1fr; }
    .main { padding: 18px; }
  }
.readonly-field { background: var(--surface-1); color: var(--text-muted); }

/* ============================================================================
   Simple horizontal bar charts (DSR Report — City/Department/Purpose panels).
   Plain HTML/CSS, no charting library: a label column, a track with a single-
   hue fill sized by percentage of the group's max value, and a value at the
   tip. Single series per chart, so one accent hue throughout is correct —
   identity isn't in question, only magnitude.
   ============================================================================ */
/* DSR Report's 4-chart row (City/Department/Purpose/Category, 2026-08-26).
   Was an inline 4-equal-column grid with no responsive rule at all, which on
   a phone-width screen produced four UNEVEN columns (the browser distorts
   1fr tracks around each chart's own content minimum) rather than one clean
   collapse — worse than not having a breakpoint on .stats-grid, since that
   at least degrades evenly. min-width:0 on each track lets a track actually
   shrink below its bar-label content's natural width instead of fighting it. */
.dsrr-chart-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; margin-bottom: 16px; }
@media (max-width: 900px) {
  .dsrr-chart-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 480px) {
  .dsrr-chart-grid { grid-template-columns: 1fr; }
}

.barchart { max-height: 280px; overflow-y: auto; padding-right: 4px; }
.barchart-row { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
.barchart-row:last-child { margin-bottom: 0; }
.barchart-label {
  width: 38%; flex-shrink: 0; font-size: 12px; color: var(--text-secondary);
  text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.barchart-track {
  flex: 1; height: 16px; background: var(--bg-page); border: 1px solid var(--border);
  border-radius: 4px; position: relative; overflow: hidden;
}
.barchart-fill { height: 100%; background: var(--accent-fill); border-radius: 0 4px 4px 0; min-width: 3px; }
.barchart-value {
  width: 34px; flex-shrink: 0; font-size: 12px; color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.barchart-empty { font-size: 12px; color: var(--text-muted); padding: 8px 0; }

/* Month filter chips (DSR Report) — toggle buttons, active state uses the
   same accent as the bar fills above for visual continuity. */
.month-chip {
  padding: 6px 14px; border-radius: 999px; border: 1px solid var(--border-strong);
  background: var(--surface-2); color: var(--text-secondary); font-size: 13px; cursor: pointer;
}
.month-chip.active { background: var(--accent-fill); border-color: var(--accent-fill); color: #fff; }

/* Highlighted table rows (DSR Report / Quotation Report flags) */
tr.row-warning td { background: var(--warning-bg); }
tr.row-danger td { background: var(--danger-bg); }

/* ============================================================================
   Mobile sidebar dropdown menu (2026-07-31, ported alongside the matching JS
   in core.js — see the comment there). Pure addition, doesn't touch anything
   above this line.
   ============================================================================ */
/* ================= RESPONSIVE FIX================= */
/* =========================================================
   MOBILE LEFT DROPDOWN MENU
   ========================================================= */

/* =========================================================
   MOBILE VERTICAL DROPDOWN MENU
   ========================================================= */

.mobile-menu-toggle {
    display: none;
}

/* ================= DESKTOP ================= */

@media (min-width: 801px) {

    .sidebar {
        width: 200px;
        min-width: 200px;
        flex-direction: column;
    }

    .sidebar .brand {
        display: block;
    }

    .sidebar .navbtn {
        display: block;
    }
}

/* ================= MOBILE / TABLET ================= */

@media (max-width: 800px) {

    #app-shell.visible {
        display: block;
    }

    /* Sidebar mobile par top mein full width rahegi,
       lekin items vertical/portrait honge */
    .sidebar {
        width: 100%;
        min-width: 0;
        height: auto;

        display: flex;
        flex-direction: column !important;
        align-items: stretch;

        padding: 12px;
        gap: 4px;

        overflow: visible !important;

        position: relative;
        top: auto;
        left: auto;

        background: var(--surface-1);
        border-right: 0;
        border-bottom: 1px solid var(--border);
        z-index: 100;
    }

    /* Epsilon logo mobile par visible rahe */
    .sidebar .brand {
        display: block !important;
        width: 100%;
        padding: 6px 8px 12px;
        text-align: left;
    }

    .sidebar .brand img {
        display: block;
        width: 145px;
        max-width: 70%;
        height: auto;
        margin: 0;
    }

    .sidebar .brand .brand-caption {
        display: block;
        margin-top: 3px;
        font-size: 10px;
    }

    /* Mobile Menu button */
    .mobile-menu-toggle {
        display: flex !important;
        width: 100%;
        min-height: 44px;

        align-items: center;
        justify-content: flex-start;

        padding: 10px 12px;
        gap: 10px;

        border: 1px solid var(--border-strong);
        border-radius: var(--radius);

        background: var(--surface-2);
        color: var(--text-primary);

        font-size: 14px;
        font-weight: 600;
        text-align: left;
    }

    .mobile-menu-toggle .mobile-menu-icon {
        font-size: 20px;
        line-height: 1;
    }

    .mobile-menu-toggle .mobile-menu-text {
        flex: 1;
        text-align: left;
    }

    .mobile-menu-toggle .mobile-menu-arrow {
        margin-left: auto;
        font-size: 11px;
        transition: transform 0.2s ease;
    }

    .sidebar.mobile-menu-open
    .mobile-menu-toggle
    .mobile-menu-arrow {
        transform: rotate(180deg);
    }

    /* Menu closed hone par pages hide */
    .sidebar .navbtn {
        display: none !important;
    }

    .sidebar .notif-pill {
        display: none !important;
    }

    .sidebar .notif-bell-wrap {
        display: none !important;
    }

    .sidebar .userinfo {
        display: none !important;
    }

    /* Menu open hone par pages one-by-one vertical dikhaye */
    /* This !important genuinely is needed here -- it's overriding a
       SEPARATE !important two rules up (`@media (max-width:800px) .sidebar
       .navbtn { display:none !important }`, the "collapsed by default"
       rule), not the JS's inline hiding. The real 2026-08-27 bug: this rule
       being !important meant it ALSO beat applyNavVisibility()'s inline
       display:none for any nav button a person's role or a Super
       Developer's per-person hiddenNav doesn't allow, so opening the mobile
       hamburger menu showed literally every page to everyone regardless of
       permissions. The actual fix is on the JS side (applyNavVisibility()
       now hides with important-priority via setProperty(...,'important') so
       it can beat this rule too, exactly as an inline style is specified to
       beat any external stylesheet rule of equal importance) -- removing
       !important here instead just made EVERY item (including ones the
       role does allow) fall back to the collapsed-by-default rule, i.e.
       nothing ever showed at all. Keep this !important; don't remove it
       again without also reverting the JS side. */
    .sidebar.mobile-menu-open .navbtn {
        display: block !important;

        width: 100%;
        min-width: 100%;
        flex: 0 0 auto;

        margin: 0;
        padding: 11px 12px;

        text-align: left;
        white-space: normal;

        border-radius: var(--radius);
    }

    /* Har option separate line mein */
    .sidebar.mobile-menu-open .navbtn + .navbtn {
        margin-top: 2px;
    }

    .sidebar.mobile-menu-open .notif-pill {
        display: flex !important;
        width: 100%;
        margin: 4px 0;
    }

    .sidebar.mobile-menu-open .notif-bell-wrap {
        display: block !important;
        width: 100%;
        margin: 4px 0 8px;
    }

    .sidebar.mobile-menu-open .notif-panel {
        position: static;
        width: 100%;
        max-width: none;
        margin-top: 6px;
        box-shadow: none;
        border: 0.5px solid var(--border);
    }

    /* User information menu ke end mein */
    .sidebar.mobile-menu-open .userinfo {
        display: block !important;
        width: 100%;
        margin-top: 8px;
        padding: 10px 8px;
    }

    /* Main content */
    .main {
        width: 100%;
        max-width: none;
        padding: 16px;
        overflow-x: hidden;
    }

    .view.visible {
        width: 100%;
        min-width: 0;
    }

    /* Large report table horizontally scroll ho sake */
    table.data {
        display: block;
        width: 100%;
        overflow-x: auto;
        white-space: nowrap;
    }
}

/* ================= SMALL MOBILE ================= */

@media (max-width: 480px) {

    .sidebar {
        padding: 10px;
    }

    .sidebar .brand {
        padding: 4px 6px 10px;
    }

    .sidebar .brand img {
        width: 135px;
    }

    .mobile-menu-toggle {
        min-height: 42px;
        padding: 9px 11px;
    }

    .sidebar.mobile-menu-open .navbtn {
        padding: 10px 11px;
        font-size: 13px;
    }

    .main {
        padding: 12px;
    }
}
/* Desktop and large laptop.
   max-width caps the content column so it doesn't stretch edge-to-edge (and
   look sparse/gappy) on wide desktop monitors -- 1440px comfortably fits the
   4-column stat grids, 2-column forms, and the 1200px-wide Purchase Report
   tables without needing to scroll on most screens. margin:auto centers the
   leftover space evenly rather than leaving one big blank strip on the right.
   Below this breakpoint's effective width (~1630px viewport, once the 190px
   sidebar is subtracted) the cap never engages, so laptop/tablet/mobile
   sizing below is unaffected. */
.main {
    flex: 1;
    width: 100%;
    max-width: 1440px;
    margin: 0 auto;
    min-width: 0;
    overflow-x: auto;
    padding: 28px 34px;
}

/* Purchase Report tables */
#po-table,
#po-all-table,
#po-pending-table,
#po-surya-all-table,
#po-revoke-table {
    width: 100%;
    min-width: 1200px;
}

table.data th,
table.data td {
    white-space: nowrap;
    vertical-align: middle;
}

/* Small laptop */
@media (max-width: 1366px) {

    .sidebar {
        width: 190px;
        min-width: 190px;
    }

    .main {
        padding: 24px;
    }

    table.data {
        font-size: 12px;
    }

    table.data th,
    table.data td {
        padding: 7px 8px;
    }
}

/* Tablet */
@media (max-width: 1024px) {

    .sidebar {
        width: 180px;
        min-width: 180px;
    }

    .main {
        padding: 20px;
    }

    .stats-grid {
        grid-template-columns: repeat(2, 1fr);
    }

    .dash-section-grid {
        grid-template-columns: repeat(2, 1fr);
    }

    .dash-hero-grid {
        grid-template-columns: 1fr;
    }

    .dash-quad-grid {
        grid-template-columns: 1fr;
    }

    .dash-quad-grid > * {
        grid-column: auto !important;
        grid-row: auto !important;
    }

    #po-table,
    #po-all-table,
    #po-pending-table,
    #po-surya-all-table,
    #po-revoke-table {
        min-width: 1050px;
    }
}

/* Mobile and small tablet for all page */
@media (max-width: 800px) {

    #app-shell.visible {
        flex-direction: column;
    }

    .sidebar {
        width: 100%;
        min-width: 0;
        height: auto;
        flex-direction: row;
        overflow-x: auto;
        overflow-y: visible;
        padding: 10px;
        border-right: 0;
        border-bottom: 1px solid var(--border);
        position: sticky;
        top: 0;
        z-index: 40;
    }

    .sidebar .brand {
        display: none;
    }

    .sidebar .userinfo {
        display: none;
    }

    .sidebar .navbtn {
        flex: 0 0 auto;
        white-space: nowrap;
        padding: 8px 12px;
    }

    .main {
        width: 100%;
        padding: 16px;
        overflow-x: hidden;
    }

    .view.visible {
        width: 100%;
        overflow-x: hidden;
    }

    table.data {
        display: block;
        width: 100%;
        overflow-x: auto;
        white-space: nowrap;
        border-radius: 8px;
    }

    #po-table,
    #po-all-table,
    #po-pending-table,
    #po-surya-all-table,
    #po-revoke-table {
        min-width: 1000px;
    }

    .stats-grid {
        grid-template-columns: repeat(2, 1fr);
    }

    .dash-section-grid {
        grid-template-columns: repeat(2, 1fr);
    }

    .dash-hero-grid {
        grid-template-columns: 1fr;
    }

    .dash-quad-grid {
        grid-template-columns: 1fr;
    }

    .dash-quad-grid > * {
        grid-column: auto !important;
        grid-row: auto !important;
    }

    .form-grid {
        grid-template-columns: 1fr;
    }

    #po-table-title-row {
        flex-wrap: wrap;
        align-items: flex-start !important;
    }

    #po-table-title-row > div:last-child {
        width: 100%;
        margin-left: 0 !important;
    }

    #po-export-excel {
        width: 100%;
    }
}

/* Small mobile */
@media (max-width: 480px) {

    .main {
        padding: 12px;
    }

    .main h2 {
        font-size: 17px;
    }

    #dash-greeting {
        font-size: 20px;
    }

    .main .pagesub {
        font-size: 12px;
    }

    .stats-grid {
        grid-template-columns: 1fr;
    }

    .dash-section-grid {
        grid-template-columns: 1fr;
    }

    /* DSR Report's own stat cards (2026-08-26) — only 4 cards with short
       labels ("Working Day", "Total Visit"...), unlike Dashboard's 20 cards
       with much longer ones, so the general .stats-grid rule above (1 column
       below this width) wastes space here instead of helping. Scoped to this
       one grid by id so Dashboard's already-tuned mobile layout is untouched. */
    #dsrr-stats {
        grid-template-columns: repeat(2, 1fr);
    }

    .bordered {
        padding: 12px;
    }

    button {
        min-height: 40px;
    }

    #edit-sheet {
        padding: 16px;
    }

    #print-overlay {
        padding: 10px;
    }

    #print-sheet {
        padding: 18px;
        font-size: 11px;
    }
}

/* =========================================
   EPSILON DEVELOPER CREDIT (2026-08-03)
   Search "EPSILON DEVELOPER CREDIT" (here) or "addDeveloperCredit" (in
   core.js) to find/edit this. See claude/code-map.md for the full list of
   sitewide elements and where each one lives.
   ========================================= */
.developer-credit {
  position: fixed;
  right: 18px;
  bottom: 10px;
  max-width: calc(100vw - 210px);
  padding: 4px 8px;
  font-family: Inter, Arial, sans-serif;
  font-size: 10px;
  font-weight: 400;
  line-height: 1.2;
  color: #7f7f7f;
  background: rgba(250, 249, 246, 0.88);
  border-top: 1px solid #dedbd2;
  letter-spacing: 0.2px;
  text-align: right;
  opacity: 0.85;
  pointer-events: none;
  user-select: none;
  z-index: 900;
}
.main-content,
.page-content,
.app-main,
main {
  padding-bottom: 40px !important;
}
@media (max-width: 768px) {
  .developer-credit {
    right: 8px;
    bottom: 6px;
    max-width: calc(100vw - 16px);
    font-size: 9px;
    text-align: center;
  }
}
@media print {
  .developer-credit {
    display: none !important;
  }
}