/* One UI family for both scripts and both directions (D1). The pin is this URL
   — Google's build carries its own numbering (/vazirmatn/v16/), which maps to
   no upstream release number. `swap` keeps text readable on a slow link; the
   cost is one reflow when Arabic's line box settles from the fallback's to
   Vazirmatn's 1.57em. */
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700&display=swap');

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  /* E1: brand tap feedback, NOT transparent. main.css has only 7 :active rules
     in 205 KB and .btn has no pressed state at all, so zeroing the highlight
     would leave most buttons with no tap feedback whatsoever. */
  -webkit-tap-highlight-color: rgba(79, 70, 229, 0.12);
  /* Stop WebKit inflating text on rotation. This is also the future home of the
     frontend text-size path (the replacement for the WKWebView pageZoom that
     DynamicType.maxZoom = 1.0 rules out): whoever builds it overrides THIS
     value rather than adding a second rule. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

:root {
  --primary: #4f46e5;
  --primary-light: #818cf8;
  --primary-dark: #3730a3;
  --primary-50: #eef2ff;
  --primary-100: #e0e7ff;
  --success: #059669;
  --success-light: #d1fae5;
  --success-text: #065f46;
  --danger: #dc2626;
  --danger-light: #fee2e2;
  --danger-text: #991b1b;
  --warning: #d97706;
  --warning-light: #fef3c7;
  --warning-bg: #fffbeb;
  --warning-text: #92400e;
  --gray-50: #f9fafb;
  --gray-100: #f3f4f6;
  --gray-200: #e5e7eb;
  --gray-300: #d1d5db;
  --gray-400: #9ca3af;
  --gray-500: #6b7280;
  --gray-600: #4b5563;
  --gray-700: #374151;
  --gray-800: #1f2937;
  --gray-900: #111827;
  --sidebar-bg: #0f172a;
  /* Raised from 0.2: at that alpha the active row's indigo barely separated from
     the hover row's white wash, so "where am I" and "what is under the cursor"
     read as the same state. */
  --sidebar-active: rgba(79, 70, 229, 0.35);
  --sidebar-border: rgba(255,255,255,0.08);
  /* The sidebar is a fixed dark surface in BOTH themes, so it cannot use
     `--danger` (#dc2626, tuned for light surfaces and too dark on #0f172a).
     These join the `--sidebar-*` family for the same reason it exists, and need
     no [data-theme="dark"] counterpart: both sidebar backgrounds are near-black.
     The value is the dark theme's own `--danger`/`--danger-light`. */
  /* Resting sidebar text. Fixed for the same reason as `--sidebar-muted` below:
     as `--gray-400` this read 7.0:1 in the light theme but 3.9:1 in the dark one,
     so the nav labels, the company name and the footer controls all quietly
     dimmed below AA when the app theme flipped, on a surface that barely moved.
     #9ca3af is the light theme's own value, so light mode is unchanged and dark
     mode is repaired to 7.3:1. Keeps the sidebar's three-step hierarchy honest:
     muted labels ~5:1 < resting items ~7:1 < active/hover #fff. */
  --sidebar-text: #9ca3af;
  /* Muted sidebar text. Fixed, not `--gray-500`: the gray ramp INVERTS between
     themes (light #6b7280 / dark #94a3b8) while this surface stays near-black,
     so the section labels rendered at 3.8:1 in the light theme and 7.2:1 in the
     dark one — the same text, failing AA in only one of them. This value is
     ~5.0:1 on both sidebar backgrounds: clear of the 4.5:1 floor for 10px text,
     still visibly dimmer than the nav items (~7:1) so the grouping stays quiet. */
  --sidebar-muted: #7c8899;
  --sidebar-danger: #f87171;
  --sidebar-danger-bg: rgba(248, 113, 113, 0.12);
  --sidebar-danger-border: rgba(248, 113, 113, 0.3);
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
  --shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
  --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
  --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
  --radius: 10px;
  --radius-lg: 14px;
  --transition: 0.2s ease;
  /* Pill fills. Deliberately paler than the matching `--*-light`, which also
     backs stat/KPI icon chips and status-dot halos where a single instance
     should read as a tint. A pill column is the opposite case: ten stacked
     `--success-light` (#d1fae5, 90% lightness) pills merged into a green stripe
     down the table. These are the 50-step of the same ramps (~95-97% lightness),
     so the fill nearly disappears and the text carries the hue. Scoped to pills
     on purpose — widening `--*-light` itself would also wash out the icon chips
     and the info-banner borders, which are not the thing being fixed. */
  --badge-success-bg: #ecfdf5;
  --badge-danger-bg: #fef2f2;
  --badge-warning-bg: #fffbeb;
  /* Semantic surface tokens */
  --surface: #ffffff;
  --surface-alt: var(--gray-50);
  --body-bg: var(--gray-100);
  --text-primary: var(--gray-900);
  --text-secondary: var(--gray-600);
  --text-muted: var(--gray-500);
  --border-color: var(--gray-200);
  --border-light: var(--gray-100);
  --overlay-bg: rgba(15, 23, 42, 0.6);
  /* Purple / manual accent (flags, QA leave tile, purple stat-icon) */
  --purple-light: #f3e8ff;
  --purple-text: #7e22ce;
  --purple: #7c3aed;

  /* ─── Type scale ───
     --fs-2xs is a FLOOR, not just a step: 12px is the smallest size any
     on-screen text may use. It is what badges, table headers and card
     sub-labels take, and those carry meaning (a status, a column name, an
     employee count), not decoration. Nothing on screen goes below it.

     Sizing is script-neutral and must stay that way. Vazirmatn's Arabic draws
     ~30% taller than its Latin lowercase at the same px (measured ink: 8px vs
     6px at 11px, 11px vs 8px at 14px), so Arabic is not disadvantaged here and
     needs NO per-language bump — an html[lang="ar"] size override would only
     drag the rem paddings against the fixed-px layout constraints (the 180px
     KPI band, the 800px table min-width, the 40px icons).

     Print is a separate universe: @media print sizes in `pt` are tuned for
     paper and are deliberately below these values. Do not fold them in. */
  --fs-2xs: 0.75rem;    /* 12px — badges, th, meta. FLOOR: never go smaller */
  --fs-xs: 0.8125rem;   /* 13px — captions, sub-labels, dense chrome */
  --fs-sm: 0.875rem;    /* 14px — body, table cells, inputs, buttons */
  --fs-md: 1rem;        /* 16px — card titles */
  --fs-lg: 1.25rem;     /* 20px — section headings */
  --fs-xl: 1.625rem;    /* 26px — page titles */
  --fs-2xl: 2.25rem;    /* 36px — hero KPI value */

  /* ─── Layer 1 — Dawam brand primitives ───
     Raw brand ramps. Nothing consumes these yet: the semantic tokens above
     (--primary, --surface, --sidebar-*, …) are layer 2 and are repointed at
     these in a later commit, so adding them here is a no-op by construction.

     THE LAYER RULE: layer 1 is defined ONCE, in :root, and the
     [data-theme="dark"] block never remaps it. A primitive names a colour, not
     a role — #203650 is the same navy in either theme — so a dark override
     here would make "--navy-900" mean two different colours and leave layer 2
     with no fixed thing to point at. Dark mode belongs entirely to the
     semantic layer, which is where it already lives.

     Steps sit on an OKLab lightness ladder and are strictly monotonic from 50
     to 950. The two ratified brand values are reproduced VERBATIM (kept in
     their ratified uppercase so they are greppable as the source values) at
     the step their own lightness puts them in, rather than being re-derived
     and rounded into a neighbour: navy #203650 measures L=0.328, which is the
     900 slot, and teal #4A8583 measures L=0.576, which is the 600 slot.

     Teal is an accent only, and the anchor itself reads 4.22:1 on white — just
     under the 4.5:1 floor. --teal-700 (6.60:1) is the darker cut that
     interactive text and fills take; do not reach for --teal-600 there.
     See DECISIONS.md, D3 and D4 (2026-08-16). */
  --navy-50: #f2f6fb;
  --navy-100: #e3ebf4;
  --navy-200: #ccdaea;
  --navy-300: #b1c4db;
  --navy-400: #90a8c5;
  --navy-500: #728eaf;
  --navy-600: #587699;
  --navy-700: #415d7f;
  --navy-800: #304968;
  --navy-900: #203650; /* brand anchor — ratified navy, verbatim */
  --navy-950: #14253a;
  --teal-50: #f0f7f7;
  --teal-100: #dfedec;
  --teal-200: #c6dedd;
  --teal-300: #a7cac8;
  --teal-400: #83b0ae;
  --teal-500: #619795;
  --teal-600: #4A8583; /* brand anchor — ratified teal, verbatim */
  --teal-700: #2a6664; /* the AA cut: 6.60:1 on white */
  --teal-800: #185250;
  --teal-900: #0b3f3e;
  --teal-950: #022b2a;
  /* A single brand surface, not a ramp: cream backs login, empty states and
     onboarding only — the app canvas keeps the slate neutrals and printed
     paper stays white (D4), so there is no second value to interpolate to. */
  --cream: #F3EFE8;
}

/* ─── Dark Theme ─── */
[data-theme="dark"] {
  --primary: #818cf8;
  --primary-light: #a5b4fc;
  --primary-dark: #6366f1;
  --primary-50: rgba(99, 102, 241, 0.12);
  --primary-100: rgba(99, 102, 241, 0.18);
  --success: #34d399;
  --success-light: rgba(52, 211, 153, 0.15);
  --success-text: #6ee7b7;
  --danger: #f87171;
  --danger-light: rgba(248, 113, 113, 0.15);
  --danger-text: #fca5a5;
  --warning: #fbbf24;
  --warning-light: rgba(251, 191, 36, 0.15);
  --warning-bg: rgba(251, 191, 36, 0.08);
  --warning-text: #fcd34d;
  /* The dark theme's `--*-light` are already 0.15-alpha washes, which is what
     the pale-fill rule is asking for on a dark surface — so pills track them
     directly here rather than carrying a second set of values. */
  --badge-success-bg: var(--success-light);
  --badge-danger-bg: var(--danger-light);
  --badge-warning-bg: var(--warning-light);
  --gray-50: #1e293b;
  --gray-100: #1e293b;
  --gray-200: #334155;
  --gray-300: #475569;
  --gray-400: #64748b;
  --gray-500: #94a3b8;
  --gray-600: #cbd5e1;
  --gray-700: #e2e8f0;
  --gray-800: #f1f5f9;
  --gray-900: #f8fafc;
  --sidebar-bg: #0c1222;
  --shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
  --shadow: 0 1px 3px rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.3);
  --shadow-md: 0 4px 6px -1px rgba(0,0,0,0.4), 0 2px 4px -2px rgba(0,0,0,0.3);
  --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.5), 0 4px 6px -4px rgba(0,0,0,0.4);
  --surface: #1e293b;
  --surface-alt: #162032;
  --body-bg: #0f172a;
  --text-primary: #f8fafc;
  --text-secondary: #cbd5e1;
  --text-muted: #94a3b8;
  --border-color: #334155;
  --border-light: #1e293b;
  --overlay-bg: rgba(0, 0, 0, 0.7);
  --purple-light: rgba(124, 58, 237, 0.18);
  --purple-text: #c4b5fd;
  --purple: #a78bfa;
}

/* Smooth theme transition */
html[data-theme] body,
html[data-theme] .card,
html[data-theme] .stat-card,
html[data-theme] .filter-bar,
html[data-theme] .modal,
html[data-theme] .toast {
  transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}

body {
  font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  font-size: var(--fs-sm);
  background: var(--body-bg);
  color: var(--text-primary);
  line-height: 1.6;
  overflow-x: hidden; /* L4: safety net against stray wide elements causing horizontal page scroll */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* `body` had no font-size at all, so everything that did not name one inherited
   the UA's 16px. Declaring 14px here makes the scale explicit, but it also drags
   every INHERITING element down by the same 2px — and `<small>` is 0.83em of its
   parent, so the ~36 bare ones across settings/shifts/employees/payroll/
   punch-requests would land at 11.67px, i.e. under the floor this commit exists
   to establish. Pin them to the floor instead of letting them ride the parent.
   (The only other inheritors are `.sr-only` captions, which are never painted.) */
small {
  font-size: var(--fs-2xs);
}

/* ─── Layout ─── */
.app {
  display: flex;
  min-height: 100vh;
}

/* ─── Sidebar ─── */
.sidebar {
  width: 222px;
  background: var(--sidebar-bg);
  color: #fff;
  padding: 0;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  z-index: 50;
  overflow-y: auto;
  overflow-x: hidden;
}

.sidebar.transitions-enabled {
  transition: width 0.25s ease;
}

.sidebar-brand {
  padding: 1.5rem 1.5rem 1.25rem;
  border-bottom: 1px solid var(--sidebar-border);
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 72px;
}

.sidebar-brand .brand-icon {
  width: 36px;
  height: 36px;
  background: var(--primary);
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1rem;
  flex-shrink: 0;
}

.sidebar-brand .brand-text {
  overflow: hidden;
  white-space: nowrap;
  transition: opacity 0.2s ease, width 0.25s ease;
}

.sidebar-brand h1 {
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.2;
}

.sidebar-brand .brand-sub {
  font-size: var(--fs-2xs);
  color: var(--sidebar-text);
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin-top: 1px;
}

.sidebar nav {
  padding: 0.75rem 0;
  flex: 1;
}

.sidebar-section {
  padding: 0.5rem 1.5rem 0.4rem;
  font-size: var(--fs-2xs);
  font-weight: 600;
  text-transform: uppercase;
  /* Widened from 0.08em: uppercase at 10px needs the extra tracking to stay
     readable, and it is the cheapest legibility gain that does not make the
     label louder — the size step down from the 0.875rem items is deliberate. */
  letter-spacing: 0.1em;
  color: var(--sidebar-muted);
  margin-top: 0.75rem;
  white-space: nowrap;
  overflow: hidden;
  transition: opacity 0.2s ease;
}

.sidebar nav a {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 1.5rem;
  color: var(--sidebar-text);
  text-decoration: none;
  font-size: 0.875rem;
  font-weight: 500;
  transition: all var(--transition);
  /* Logical, not `border-left` + a mirrored `[dir="rtl"]` override: the accent
     bar belongs on the inline-start edge, which is the right edge in Arabic. */
  border-inline-start: 3px solid transparent;
  margin: 1px 0;
  white-space: nowrap;
  overflow: hidden;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;   /* E1: chrome, not data — no long-press callout */
}

/* The label is the ONLY item on this line allowed to give ground. A flex item's
   automatic minimum size is its min-content width, and `white-space: nowrap`
   above makes that the whole unbroken label — so without `min-width: 0` nothing
   on the line can shrink, and the 180px mobile drawer cannot fit
   icon + gap + label + gap + badge for the longer labels (`Punch Requests`
   needs 164px against a 129px content box). The line then overflows, which
   silently costs the badge BOTH of its guarantees: `margin-inline-start: auto`
   has no free space left to distribute so it resolves to 0, and the badge is
   pushed past the flex container's main-end edge, where the `overflow: hidden`
   above clips it — measured at 10.8px of the pill cut off in English, and the
   Arabic row crowded to 12.8px from the edge where an uncrowded row sits at 24.
   Ellipsis rather than a smaller font: the count must stay legible, and a
   truncated nav label is the conventional, translation-proof answer. */
.sidebar nav a > span:not(.sidebar-nav-badge) {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sidebar nav a svg {
  width: 18px;
  height: 18px;
  opacity: 0.7;
  flex-shrink: 0;
  transition: opacity var(--transition);
}

/* No accent bar on hover. It used to take a `--gray-600` bar, which meant hover
   and active differed only in bar HUE — the bar is now the exclusive "you are
   here" signal, and hover stays white text + a faint wash. */
.sidebar nav a:hover {
  color: #fff;
  background: rgba(255,255,255,0.05);
}

.sidebar nav a:hover svg {
  opacity: 1;
}

.sidebar nav a.active {
  color: #fff;
  /* Weight is the one signal hover cannot borrow: the label was already #fff at
     rest-hover, so white alone never distinguished the two. */
  font-weight: 600;
  background: var(--sidebar-active);
  border-inline-start-color: var(--primary-light);
}

.sidebar nav a.active svg {
  opacity: 1;
  color: var(--primary-light);
}

/* A real circle, with the digits really centred in it. It used to be a box sized
   purely by `line-height: 1.4` plus horizontal padding — 22.4 × 17.3, so never
   round — and the digits' ink sat 1.84px ABOVE the box centre (10.7% of its
   height): flow layout anchors text to the BASELINE, so vertical centring was
   never expressed at all and `text-align` only ever handled x.
   `align-items: center` alone does NOT finish the job: it centres the LINE BOX,
   and the ink is not centred inside the line box — digits carry no descender,
   and Vazirmatn's ascent exceeds its descent by ~0.43em to make room for Arabic.
   The `padding-top` is the correction: with `box-sizing: border-box` and a fixed
   height it shrinks the content box from the top, sliding the centred line box
   down by half the padding. 0.29em lands the ink within 0.02px of centre in
   Vazirmatn and still beats the old figure under a fallback font (0.52px vs
   1.34px). It is MEASURED, not derived — re-measure if the font or size moves.
   Width stays free above `min-width` so 2- and 3-digit counts grow to a pill. */
.sidebar-nav-badge {
  margin-inline-start: auto;
  /* Never the item that absorbs a tight line — a shrunken count pill is
     unreadable, and it is the label that carries the ellipsis instead. */
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  min-width: 1.25rem;
  height: 1.25rem;
  padding: 0.29em 0.5em 0;
  background: var(--primary, #4f46e5);
  color: #fff;
  font-size: var(--fs-2xs);
  font-weight: 600;
  line-height: 1;
  border-radius: 999px;
  /* So a count ticking 1 → 2 → … cannot jitter the pill's width. */
  font-variant-numeric: tabular-nums;
}

/* ─── Sidebar collapse toggle ─── */
.sidebar-footer {
  padding: 0.75rem;
  border-top: 1px solid var(--sidebar-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: auto;
  gap: 0.5rem;
}

.sidebar.collapsed .sidebar-footer {
  flex-direction: column;
  gap: 0.5rem;
}

.sidebar.collapsed .sidebar-footer > div {
  flex-direction: column;
  gap: 0.5rem;
}

.sidebar-theme-btn,
.sidebar-collapse-btn {
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 6px;
  color: var(--sidebar-text);
  cursor: pointer;
  transition: all var(--transition);
  padding: 0;
  flex-shrink: 0;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;   /* E1: chrome, not data — no long-press callout */
  text-decoration: none;
}

.sidebar-theme-btn:hover,
.sidebar-collapse-btn:hover {
  background: rgba(255,255,255,0.12);
  color: #fff;
}

.sidebar-collapse-btn svg {
  transition: transform 0.25s ease;
}

/* Theme toggle icon visibility */
.sidebar-theme-btn .icon-moon { display: none; }
[data-theme="dark"] .sidebar-theme-btn .icon-sun { display: none; }
[data-theme="dark"] .sidebar-theme-btn .icon-moon { display: block; }

/* ─── Collapsed state ─── */
.sidebar.collapsed {
  width: 72px;
}

.sidebar.collapsed .sidebar-brand {
  padding: 1.5rem 0 1.25rem;
  justify-content: center;
  gap: 0;
}

.sidebar.collapsed .brand-text {
  opacity: 0;
  width: 0;
  overflow: hidden;
  position: absolute;
  pointer-events: none;
}

.sidebar.collapsed .sidebar-section {
  opacity: 0;
  height: 0;
  padding: 0;
  margin: 0;
  pointer-events: none;
}

.sidebar.collapsed nav a {
  justify-content: center;
  padding: 0.7rem 0;
  border-left-color: transparent;
  gap: 0;
}

.sidebar.collapsed nav a span {
  display: none;
}

.sidebar.collapsed nav a.active {
  border-inline-start-color: transparent;
  border-radius: 0;
}

.sidebar.collapsed .sidebar-collapse-btn svg {
  transform: rotate(180deg);
}

.app.sidebar-collapsed .main-content {
  margin-left: 72px;
}

/* ─── Main Content ─── */
.main-content {
  flex: 1;
  /* Flex items default to min-width:auto (= max-content), so one wide table
     would inflate the whole page past the viewport — and body{overflow-x:
     hidden} makes that overflow unreachable. min-width:0 keeps the column at
     the viewport and lets inner .table-responsive wrappers do the scrolling. */
  min-width: 0;
  padding: 2rem 2.5rem;
  margin-left: 222px;
  min-height: 100vh;
}

.page-header {
  margin-bottom: 2rem;
}

.page-header h2 {
  font-size: 1.6rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--gray-900);
  margin-bottom: 0.25rem;
}

.page-header p {
  color: var(--gray-500);
  font-size: 0.9rem;
  margin-top: 0;
}

.main-content h2 {
  margin-bottom: 1.5rem;
  font-size: 1.6rem;
  font-weight: 700;
  letter-spacing: -0.02em;
}
.main-content .page-header h2 {
  margin-bottom: 0.25rem;
}

/* ─── Cards ─── */
.card {
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: var(--radius-lg);
  padding: 1.5rem;
  margin-bottom: 1.5rem;
  box-shadow: var(--shadow-sm);
  transition: box-shadow var(--transition);
}

.card:hover {
  box-shadow: var(--shadow);
}

.card h3 {
  margin-bottom: 1rem;
  font-size: 1rem;
  font-weight: 600;
  color: var(--gray-800);
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.stats-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 1rem;
  margin-bottom: 1.5rem;
}

/* Holiday banner */
.holiday-banner {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--purple-light);
  color: var(--purple-text);
  border: 1px solid var(--purple);
  border-radius: var(--radius);
  padding: 0.75rem 1.25rem;
  margin-bottom: 1rem;
  font-weight: 500;
  font-size: 0.92rem;
}

/* Admin notice banner — set by KronexApp admins via the admin panel.
   Rendered above the dashboard stat cards on the tenant dashboard.
   Uses the same primary-tinted palette as the reports accuracy note. */
.admin-notice-banner {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  background: color-mix(in srgb, var(--primary) 7%, transparent);
  color: var(--text-secondary, #4b5563);
  border: 1px solid color-mix(in srgb, var(--primary) 22%, transparent);
  border-inline-start: 3px solid var(--primary);
  border-radius: var(--radius);
  padding: 0.85rem 1.1rem;
  margin-bottom: 1rem;
  font-size: 0.92rem;
}
.admin-notice-banner .admin-notice-icon {
  flex: 0 0 auto;
  color: var(--primary);
  opacity: 0.85;
  margin-top: 0.1rem;
}
.admin-notice-banner .admin-notice-body {
  flex: 1 1 auto;
  min-width: 0;
}
.admin-notice-banner .admin-notice-text {
  white-space: pre-wrap;
  word-wrap: break-word;
  overflow-wrap: anywhere;
  line-height: 1.45;
}

.stat-card {
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: var(--radius);
  padding: 1.25rem 1.5rem;
  box-shadow: var(--shadow-sm);
  transition: transform 0.25s cubic-bezier(.4,0,.2,1), box-shadow 0.25s cubic-bezier(.4,0,.2,1), border-color 0.25s ease;
  position: relative;
  overflow: hidden;
}

.stat-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--primary);
  opacity: 0;
  transition: opacity 0.25s ease;
}

/* Color-matched top bars */
.stat-card.green::before  { background: var(--success); }
.stat-card.orange::before { background: var(--warning); }
.stat-card.red::before    { background: var(--danger); }
.stat-card.blue::before   { background: var(--primary); }
.stat-card.purple::before { background: var(--purple); }

.stat-card:hover {
  box-shadow: var(--shadow-lg);
  transform: translateY(-3px);
  border-color: var(--gray-300);
}

.stat-card:hover::before {
  opacity: 1;
}

/* Clickable stat-card link styles */
a.stat-card-link {
  display: block;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
}
a.stat-card-link:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* Arrow indicator */
.stat-card-arrow {
  position: absolute;
  bottom: 1rem;
  inset-inline-end: 1rem;
  color: var(--gray-300);
  opacity: 0;
  transform: translateX(calc(var(--dir-mult, 1) * -4px));
  transition: opacity 0.25s ease, transform 0.25s ease, color 0.25s ease;
}
[dir="rtl"] .stat-card-arrow {
  transform: translateX(4px) scaleX(-1);
}
.stat-card:hover .stat-card-arrow {
  opacity: 1;
  transform: translateX(0);
}
[dir="rtl"] .stat-card:hover .stat-card-arrow {
  transform: translateX(0) scaleX(-1);
}
.stat-card.green:hover .stat-card-arrow  { color: var(--success); }
.stat-card.orange:hover .stat-card-arrow { color: var(--warning); }
.stat-card.red:hover .stat-card-arrow    { color: var(--danger); }
.stat-card.blue:hover .stat-card-arrow   { color: var(--primary); }

/* Icon lift on hover */
.stat-card:hover .stat-icon {
  transform: scale(1.08);
  transition: transform 0.25s cubic-bezier(.4,0,.2,1);
}

.stat-card .stat-icon {
  width: 40px;
  height: 40px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 0.75rem;
  font-size: 1.1rem;
}

.stat-card .stat-icon.blue   { background: var(--primary-50); color: var(--primary); }
.stat-card .stat-icon.green  { background: var(--success-light); color: var(--success); }
.stat-card .stat-icon.orange { background: var(--warning-light); color: var(--warning); }
.stat-card .stat-icon.red    { background: var(--danger-light); color: var(--danger); }
.stat-card .stat-icon.purple { background: var(--purple-light); color: var(--purple); }

.stat-card .label {
  font-size: 0.78rem;
  color: var(--gray-500);
  margin-bottom: 0.35rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.stat-card .value {
  font-size: 1.85rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--gray-900);
  line-height: 1.1;
}

.stat-card .stat-sub {
  font-size: 0.78rem;
  color: var(--text-muted);
  margin-top: 0.35rem;
}
.stat-card .stat-note {
  display: flex;
  align-items: flex-start;
  gap: 0.35rem;
  position: absolute;
  top: 0.75rem;
  inset-inline-end: 0.75rem;
  font-size: var(--fs-2xs);
  line-height: 1.4;
  color: var(--text-muted);
  font-style: italic;
}

/* Compact stat cards inside reports */
#report-summary {
  gap: 0.6rem;
  margin-bottom: 0.75rem;
}
#report-summary .stat-card {
  padding: 0.5rem 0.75rem;
}
#report-summary .stat-card .stat-icon {
  width: 24px;
  height: 24px;
  border-radius: 6px;
  margin-bottom: 0.3rem;
}
#report-summary .stat-card .stat-icon svg {
  width: 12px;
  height: 12px;
}
#report-summary .stat-card .label {
  font-size: var(--fs-2xs);
  margin-bottom: 0.15rem;
}
#report-summary .stat-card .value {
  font-size: 1.05rem;
}
#report-summary .stat-card .stat-sub {
  font-size: var(--fs-2xs);
  margin-top: 0.15rem;
}

/* Compact report-output card — less padding than a standard card */
#report-output .card {
  padding: 0.75rem 1.25rem;
  margin-bottom: 0.6rem;
}

/* Tighter gap between the generate-report card and the output below */
.page-header + .card {
  margin-bottom: 0.75rem;
}

/* Suppress the bottom margin on the stats-row when it has no children
   (e.g. summary/leave report types that clear innerHTML but don't hide it) */
#report-summary:empty {
  display: none !important;
  margin: 0 !important;
}

/* ─── Dashboard Layout ─── */
.dashboard-greeting {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.dashboard-greeting h2 {
  font-size: 1.6rem;
  font-weight: 700;
  margin: 0;
}
.dashboard-greeting p {
  color: var(--gray-500);
  margin: 0;
  font-size: 0.9rem;
}

/* Quick-settings strip on dashboard */
.quick-settings {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.4rem 0.75rem;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 0.5rem;
}
.quick-toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  cursor: pointer;
  color: var(--text);
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;   /* E1: chrome, not data — no long-press callout */
  font-size: 0.8rem;
  white-space: nowrap;
}
.quick-toggle svg {
  flex-shrink: 0;
  color: var(--gray-500);
}
.quick-toggle .toggle-switch {
  transform: scale(0.8);
}
@media (max-width: 600px) {
  .quick-settings { width: 100%; justify-content: space-around; }
}

.stat-card .stat-value-group {
  display: flex;
  align-items: baseline;
  gap: 0.35rem;
}
.stat-card .stat-value-group .value {
  margin: 0;
}
.stat-card .stat-value-group .stat-total {
  font-size: 1rem;
  color: var(--text-muted);
  font-weight: 500;
}

.stat-progress {
  height: 6px;
  background: var(--gray-200);
  border-radius: 3px;
  margin-top: 0.6rem;
  overflow: hidden;
}
.stat-progress-fill {
  height: 100%;
  border-radius: 3px;
  background: var(--primary);
  transition: width 0.8s ease;
}

.dashboard-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1rem;
  margin-bottom: 1rem;
}

.chart-container {
  position: relative;
  width: 100%;
  max-width: 260px;
  margin: 0 auto;
}

.activity-feed {
  list-style: none;
  padding: 0;
  margin: 0;
  max-height: 400px;
  overflow-y: auto;
}
.activity-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--gray-100);
}
.activity-item:last-child {
  border-bottom: none;
}
.activity-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.activity-dot.on-time { background: var(--success); }
.activity-dot.late { background: var(--warning); }
.activity-dot.neutral { background: var(--gray-300); }
.activity-info {
  flex: 1;
  min-width: 0;
}
.activity-name {
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--gray-800);
}
.activity-detail {
  font-size: 0.78rem;
  color: var(--gray-500);
}
.activity-time {
  font-size: 0.8rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.activity-badge {
  font-size: var(--fs-2xs);
  padding: 0.15rem 0.45rem;
  border-radius: 4px;
  font-weight: 600;
  white-space: nowrap;
}
.activity-badge.on-time {
  background: var(--success-light);
  color: var(--success);
}
.activity-badge.late {
  background: var(--warning-light);
  color: var(--warning);
}

.weekly-chart-container {
  position: relative;
  width: 100%;
  height: 200px;
}

.sync-controls .section-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
}
.sync-status-bar {
  font-size: 0.75rem;
  color: var(--gray-500);
  font-weight: 400;
}

/* Sync progress indicator */
.sync-progress {
  display: none;
  margin-top: 0.75rem;
}
.sync-progress.active {
  display: block;
}
.sync-progress-bar {
  height: 4px;
  border-radius: 2px;
  background: var(--gray-200);
  overflow: hidden;
  margin-bottom: 0.5rem;
}
.sync-progress-bar .bar-fill {
  height: 100%;
  width: 30%;
  border-radius: 2px;
  background: var(--primary);
  animation: sync-indeterminate 1.5s ease-in-out infinite;
}
@keyframes sync-indeterminate {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(430%); }
}
@keyframes sync-indeterminate-rtl {
  0%   { transform: translateX(100%); }
  100% { transform: translateX(-430%); }
}
[dir="rtl"] .sync-progress-bar .bar-fill {
  animation-name: sync-indeterminate-rtl;
}
/* Determinate state: JS drives bar-fill width from a history-based ETA.
   Defined after the indeterminate rules so it overrides them (incl. RTL) at
   equal specificity. Width-based fill grows from the inline-start edge, so RTL
   fills from the right automatically — no direction-specific rule needed. */
.sync-progress.is-determinate .bar-fill {
  animation: none;
  transform: none;
  width: 0;
  transition: width 0.3s ease;
}
.sync-progress-info {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 0.8rem;
  color: var(--gray-600);
}
.sync-progress-phase {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}
.sync-progress-phase .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--primary);
  animation: pulse-dot 1s ease-in-out infinite;
}
@keyframes pulse-dot {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}
.sync-progress-timer {
  font-variant-numeric: tabular-nums;
  color: var(--gray-500);
}
/* Shell-owned SPA navigation progress bar (router.js). A thin fixed top bar
   shown after a short delay while a page swap is in flight. z-index 2100 clears
   headers/dropdowns (50–1000) and modal/confirm layers (≤2000); the sole higher
   layer, .app-tooltip-el (99999), is hover-transient so no tooltip is live
   during a click-started nav. Reuses the sync-indeterminate keyframes above as a
   clean standalone rule (NOT the .sync-progress-bar .bar-fill selector, which
   carries a determinate-state animation override). overflow:hidden clips the
   translating fill so it can't extend the viewport (same as .sync-progress-bar). */
.nav-progress {
  display: none;
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  height: 3px;
  z-index: 2100;
  overflow: hidden;
  pointer-events: none;
}
.nav-progress.active {
  display: block;
}
.nav-progress .bar-fill {
  height: 100%;
  width: 30%;
  background: var(--primary);
  animation: sync-indeterminate 1.5s ease-in-out infinite;
}
[dir="rtl"] .nav-progress .bar-fill {
  animation-name: sync-indeterminate-rtl;
}
.sync-controls details {
  margin-top: 0.75rem;
}
.sync-controls summary {
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--gray-600);
  padding: 0.35rem 0;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;   /* E1: chrome, not data — no long-press callout */
}
.sync-controls summary:hover {
  color: var(--primary);
}

.no-activity {
  text-align: center;
  color: var(--text-muted);
  font-size: 0.875rem;
  padding: 2rem 0;
}

@media (max-width: 768px) {
  .dashboard-grid {
    grid-template-columns: 1fr;
  }
}

/* ─── Tables ─── */
.table-container {
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}

table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}

th, td {
  text-align: left;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--gray-100);
}

th {
  background: var(--gray-50);
  font-weight: 600;
  font-size: var(--fs-2xs);
  color: var(--gray-500);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
}

/* A column header is set apart from its body rows by FOUR devices here: size,
   colour, `text-transform: uppercase` and `letter-spacing`. The last two do
   nothing in Arabic — the script is caseless, and Chrome suppresses tracking
   inside cursive joins (measured: `الاسم` is 23.67px at both `normal` and
   `0.05em`, while `Name` grows 28 -> 30.23px). So an Arabic header keeps only
   size and colour, and at the floor size it reads as small grey text rather
   than as a header. Restore the rank with weight, the one device that does
   carry across scripts. Applies to every table, since `th` is the base rule. */
[dir="rtl"] th {
  font-weight: 700;
  color: var(--gray-600);
}

tr:last-child td {
  border-bottom: none;
}

tbody tr:nth-child(even) td {
  background: var(--gray-50);
}

tfoot .report-totals-row td {
  background: var(--gray-100);
  border-top: 2px solid var(--gray-300);
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--gray-800);
}

tr:hover td {
  background: color-mix(in srgb, var(--primary) 6%, var(--surface));
}

td {
  font-size: 0.875rem;
  color: var(--gray-700);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ─── Table responsive wrapper ─── */
.table-responsive {
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  border-radius: var(--radius-lg);
  border: 1px solid var(--gray-200);
  box-shadow: var(--shadow-sm);
  position: relative;
  min-height: 120px;
  /* Scroll shadow hints */
  background:
    linear-gradient(to right, var(--surface) 30%, transparent),
    linear-gradient(to left, var(--surface) 30%, transparent),
    linear-gradient(to right, rgba(0,0,0,0.1), transparent 15px),
    linear-gradient(to left, rgba(0,0,0,0.1), transparent 15px);
  background-position: left center, right center, left center, right center;
  background-repeat: no-repeat;
  background-size: 20px 100%, 20px 100%, 15px 100%, 15px 100%;
  background-attachment: local, local, scroll, scroll;
}
.table-responsive table {
  border: none;
  border-radius: 0;
  box-shadow: none;
  min-width: 800px;
  min-height: 100%;
}

/* The debt statement renders inside a modal, so it must fit the modal rather
   than the 800px page-table minimum above - otherwise its Actions column is
   clipped and the row buttons sit behind a horizontal scrollbar. */
.table-responsive table.debt-statement-table {
  min-width: 0;
}
.debt-statement-table td,
.debt-statement-table th {
  white-space: nowrap;
}
.debt-statement-table td:nth-child(2) {
  white-space: normal;
}

/* The debtors table's employee cell carries the name plus up to two badges
   (inactive, pending-only). The global `td` rule is nowrap + ellipsis, so once
   the Pending column narrowed every other column the badge fell outside the
   cell and the ellipsis ate it - the badge that EXPLAINS a zero-balance row was
   the part that disappeared. Wrapping is the same escape the statement's
   description column takes above; the badge drops to a second line only when it
   would not otherwise fit. */
#debts-body td:first-child {
  white-space: normal;
}

/* ─── ESS "My Debt" panel ──────────────────────────────────────────────────
   The employee-facing half of the same data, and deliberately NOT the table
   above. Three things forced the split:

   • The reader is not an accountant. They arrived from a payslip that printed
     a debt figure they could not explain, so the current amount owed is the
     headline, not a footer total under a ledger.
   • "Amount" and "Remaining after this" are not peers. In a four-column table
     they render as two near-identical money columns and the eye cannot tell
     which one moved; here the amount is the value and the remainder is its
     caption, so the relationship is carried by the layout instead of by a
     column header nobody reads.
   • Four narrow columns stretch across a desk browser with a mostly empty
     description, and overflow a phone. A feed has nothing to stretch and
     nothing to scroll - it just reflows.

   Colour convention: NO green on a movement. Green on a payroll deduction
   reads as "money I received", which is the opposite of what happened. New
   debt is amber, everything that reduces the debt is plain neutral text, and
   the +/- sign plus the wording carry the direction. Green is reserved for the
   one place it is unambiguous: the headline when nothing is owed. */
/* A reading width, not the page width. Unconstrained, the panel stretched to
   1138px on a desk browser and put a movement's amount a thousand pixels away
   from the title it belongs to - the same density complaint the table had, just
   without the columns. Capped, the row is one glance wide. It stays at the
   inline-start edge (no auto margin) like every other page's content, so it
   mirrors with the rest of the layout in RTL. Applied to the whole panel, so
   the subtitle above the cards shares the same measure. */
#panel-my-debt {
  max-width: 46rem;
}

.my-debt-hero {
  position: relative;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 1.25rem 1.4rem;
}
.my-debt-hero::before {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  height: 3px;
  background: var(--warning);
}
.my-debt-hero.is-clear::before { background: var(--success); }

.my-debt-hero-label {
  display: block;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.my-debt-hero-value {
  margin-block-start: 0.3rem;
  font-size: clamp(1.85rem, 6vw, 2.35rem);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.my-debt-hero.is-clear .my-debt-hero-value { color: var(--success); }

.my-debt-meter-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-block-start: 1rem;
}
.my-debt-meter {
  flex: 1 1 auto;
  min-width: 0;
  height: 8px;
  border-radius: 999px;
  background: var(--gray-200);
  overflow: hidden;
}
.my-debt-meter-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--primary);
}
.my-debt-meter-pct {
  flex: 0 0 auto;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-secondary);
}

.my-debt-hero-totals {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.2rem 0.85rem;
  margin-block-start: 0.6rem;
  font-size: 0.82rem;
  color: var(--text-muted);
}
.my-debt-hero-totals b {
  font-weight: 600;
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

/* The history itself. A <ol>, not a <table>: these are events in order, and
   the surrounding card is what supplies the visual frame the table used to. */
.my-debt-feed-card {
  margin-block-start: 1rem;
  background: var(--surface);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding-inline: 1.25rem;
}
.my-debt-feed {
  list-style: none;
  margin: 0;
  padding: 0;
}
.my-debt-move {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  padding-block: 0.9rem;
  border-block-end: 1px solid var(--border-light);
}
.my-debt-move:last-child { border-block-end: 0; }

.my-debt-icon {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--gray-200);
  color: var(--gray-600);
}
.my-debt-move.is-charge .my-debt-icon {
  background: var(--warning-light);
  color: var(--warning-text);
}

.my-debt-main {
  flex: 1 1 auto;
  min-width: 0;
}
.my-debt-title {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text-primary);
  line-height: 1.35;
}
.my-debt-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.15rem 0.4rem;
  margin-block-start: 0.2rem;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--text-muted);
}
/* The separator belongs to the part that follows it, so it can never be left
   dangling at the end of a wrapped line. */
.my-debt-meta > span + span::before {
  content: '·';
  margin-inline-end: 0.4rem;
  color: var(--gray-300);
}

.my-debt-figures {
  flex: 0 0 auto;
  max-width: 45%;
  text-align: end;
}
.my-debt-amount {
  display: block;
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}
.my-debt-move.is-charge .my-debt-amount { color: var(--warning-text); }
.my-debt-left {
  display: block;
  margin-block-start: 0.15rem;
  font-size: var(--fs-2xs);
  line-height: 1.4;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* The one interactive thing on a read-only panel: a repayment names the locked
   payslip that collected it, and opening it is how the employee reconciles the
   figure they came here about. */
.my-debt-link {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--primary);
  cursor: pointer;
  text-decoration: underline;
}
.my-debt-link:hover { color: var(--primary-dark); }
.my-debt-link:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
  border-radius: 4px;
}

.my-debt-empty {
  padding: 2.25rem 1rem;
  text-align: center;
  color: var(--text-muted);
  font-size: 0.9rem;
}
.my-debt-note {
  margin-block-start: 0.8rem;
  font-size: 0.78rem;
  line-height: 1.55;
  color: var(--text-muted);
}
.my-debt-note + .alert { margin-block-start: 0.8rem; }

@media (max-width: 560px) {
  .my-debt-hero { padding: 1.1rem 1rem; }
  .my-debt-feed-card { padding-inline: 1rem; }
  .my-debt-move { gap: 0.65rem; }
  .my-debt-icon { width: 28px; height: 28px; }
  .my-debt-figures { max-width: 42%; }
  /* One meta part per line, and no separators at all. At this width the parts
     wrap anyway, and a separator that belongs to the following part then opens
     a line with a stray "·" - the mirror of the dangling one it was moved to
     avoid. Stacked, there is nothing to separate. */
  .my-debt-meta {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.1rem;
  }
  .my-debt-meta > span + span::before { content: none; }
}

/* ─── Sortable headers ─── */
th.sortable {
  cursor: pointer;
  /* The `-webkit-` copy is NOT redundant. Measured in the iOS shell on
     2026-08-20 (WKWebView, iOS 26.5): `CSS.supports('user-select','none')` is
     FALSE and the unprefixed property computes to `text`, while the prefixed
     one computes to `none`. Ten of the twelve chrome selectors in this file
     declared only the unprefixed form, so they were never actually
     unselectable in Safari or in either shell — a long-press selected the
     header text and iOS raised Copy / Look Up / Translate over it. Drop the
     prefix and the callout comes back. */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;   /* E1: chrome, not data — no long-press callout */
  position: relative;
  padding-inline-end: 1.5rem;
  transition: color 0.15s;
}
th.sortable:hover {
  color: var(--primary);
}
th.sortable::after {
  content: '⇅';
  position: absolute;
  inset-inline-end: 0.4rem;
  top: 50%;
  transform: translateY(-50%);
  font-size: var(--fs-2xs);
  opacity: 0.35;
  transition: opacity 0.15s;
}
th.sortable:hover::after {
  opacity: 0.65;
}
th.sort-asc::after {
  content: '▲';
  opacity: 0.85;
  color: var(--primary);
}
th.sort-desc::after {
  content: '▼';
  opacity: 0.85;
  color: var(--primary);
}

/* ─── Column resize handles ─── */
.col-resize-handle {
  position: absolute;
  top: 0;
  right: -3px;
  width: 6px;
  height: 100%;
  cursor: col-resize;
  z-index: 2;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;   /* E1: chrome, not data — no long-press callout */
}
.col-resize-handle::after {
  content: '';
  position: absolute;
  top: 25%;
  right: 2px;
  width: 2px;
  height: 50%;
  border-radius: 1px;
  background: var(--gray-300);
  opacity: 0;
  transition: opacity 0.15s;
}
th:hover .col-resize-handle::after,
.col-resize-handle.active::after {
  opacity: 1;
}
.col-resize-handle.active::after {
  background: var(--primary);
}
[dir="rtl"] .col-resize-handle {
  right: auto;
  left: -3px;
}

/* ─── Pagination ─── */
.table-pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.65rem 1rem;
  font-size: 0.8rem;
  color: var(--gray-500);
  border-top: 1px solid var(--gray-100);
  background: var(--gray-50);
  flex-wrap: wrap;
  gap: 0.5rem;
}
.pager-controls {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}
.pager-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.9rem;
  height: 1.9rem;
  padding: 0 0.45rem;
  border: 1px solid var(--gray-200);
  border-radius: 6px;
  background: var(--surface);
  color: var(--gray-600);
  font-size: 0.8rem;
  cursor: pointer;
  transition: all 0.15s;
}
.pager-btn:hover:not(:disabled) {
  border-color: var(--primary-light);
  color: var(--primary);
  background: var(--primary-50);
}
.pager-btn.active {
  background: var(--primary);
  color: #fff;
  border-color: var(--primary);
  font-weight: 600;
}
.pager-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.pager-ellipsis {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  color: var(--gray-400);
  font-size: 0.85rem;
}

/* RTL: flip pagination arrow icons */
[dir="rtl"] .pager-prev svg,
[dir="rtl"] .pager-next svg {
  transform: scaleX(-1);
}

/* ─── Buttons ─── */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.55rem 1.15rem;
  border: none;
  border-radius: 8px;
  font-size: 0.85rem;
  font-weight: 500;
  cursor: pointer;
  transition: all var(--transition);
  font-family: inherit;
  white-space: nowrap;
  text-decoration: none;
}

/* Prevent display:inline-flex from overriding the [hidden] attribute */
.btn[hidden] { display: none; }

.btn svg {
  width: 16px;
  height: 16px;
}

.btn-primary {
  background: var(--primary);
  color: #fff;
  box-shadow: 0 1px 2px rgba(79, 70, 229, 0.3);
}
.btn-primary:hover {
  background: var(--primary-dark);
  box-shadow: 0 2px 8px rgba(79, 70, 229, 0.35);
  transform: translateY(-1px);
}

.btn-success {
  background: var(--success);
  color: #fff;
  box-shadow: 0 1px 2px rgba(5, 150, 105, 0.3);
}
.btn-success:hover {
  background: #047857;
  box-shadow: 0 2px 8px rgba(5, 150, 105, 0.35);
  transform: translateY(-1px);
}

/* Cautionary action (payroll Unlock, and the confirm button of its dialogs).
   Tinted rather than solid: --warning is a light amber in the dark theme, so
   white-on-warning would fail contrast there, while --warning-light/-text are
   both theme-aware. An admin-only .btn-warning already exists in
   frontend/admin/admin.css, which tenant pages do NOT load — without this rule
   the class matched nothing and the button fell back to the UA grey
   `buttonface`, reading as disabled. */
.btn-warning {
  background: var(--warning-light);
  color: var(--warning-text);
  border: 1px solid transparent;
}
.btn-warning:hover {
  border-color: var(--warning);
  box-shadow: 0 2px 8px rgba(217, 119, 6, 0.25);
}

.btn-danger {
  background: var(--surface);
  color: var(--danger);
  border: 1px solid var(--gray-200);
}
.btn-danger:hover {
  background: var(--danger-light);
  border-color: var(--danger);
}

.btn-danger-outline {
  background: var(--surface);
  color: var(--danger);
  border: 1px solid var(--danger);
}
.btn-danger-outline:hover {
  background: var(--danger);
  color: #fff;
}

.btn-secondary {
  background: var(--surface);
  color: var(--gray-700);
  border: 1px solid var(--gray-200);
}
.btn-secondary:hover {
  background: var(--gray-50);
  border-color: var(--gray-300);
}

.btn-outline {
  background: var(--surface);
  color: var(--gray-700);
  border: 1px solid var(--gray-300);
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.btn-outline:hover {
  background: var(--gray-50);
  color: var(--gray-900);
  border-color: var(--gray-400);
}

.btn-ghost {
  background: transparent;
  color: var(--gray-600);
  padding: 0.4rem 0.6rem;
}
.btn-ghost:hover {
  background: var(--gray-100);
  color: var(--gray-900);
}
.btn-ghost:active,
.btn-ghost:focus,
.btn-ghost:focus-visible {
  background: transparent;
  outline: none;
}

/* ─── Segmented Control ─── */
.setting-row:first-of-type { border-top: none; padding-top: 0; }
.setting-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1rem 0;
}
.setting-row + .setting-row {
  border-top: 1px solid var(--gray-200);
}
.setting-row-info {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.setting-row-label {
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--text-primary);
}
.setting-row-hint {
  font-size: 0.78rem;
  color: var(--gray-500);
}
/* EQUAL-WIDTH columns, and that is load-bearing rather than cosmetic: the
   sliding indicator below is one FRACTION of the control (`100% / --seg-count`)
   offset by whole multiples of itself, so it only lands on its button when
   every segment is the same width. As `inline-flex` the buttons were
   content-sized (`flex: 1` cannot equalise them - `min-width: auto` floors each
   at its own label), so the pill drifted from the label it was highlighting:
   measured on the overtime card, segments of 45.5 / 53.6 / 61.6px under a
   53.6px indicator, which overhung "1 day" by 8px into its neighbour and left
   "30 days" 8px uncovered. Worst wherever one label is much shorter than
   another, which is why the Arabic "يوم" made it obvious.
   `grid-auto-columns: 1fr` sizes every track to the widest segment, so the
   arithmetic the indicator already assumed becomes true. `.seg-wrap` below
   re-declares `display: flex` and hides the indicator, so it is unaffected. */
.seg-control {
  display: inline-grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  background: var(--gray-100);
  border-radius: 10px;
  padding: 3px;
  gap: 0px;
  flex-shrink: 0;
  position: relative;
}
.seg-control::before {
  content: '';
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 3px;
  width: calc((100% - 6px) / var(--seg-count, 2));
  border-radius: 8px;
  background: var(--seg-color, var(--surface));
  box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), background 0.25s ease;
  transform: translateX(calc(var(--seg-index, 0) * 100%));
  z-index: 0;
}
.seg-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.45rem 0.9rem;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--gray-600);
  font-size: 0.8rem;
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  transition: color 0.2s ease;
  white-space: nowrap;
  position: relative;
  z-index: 1;
  flex: 1;
  justify-content: center;
}
.seg-btn:hover {
  color: var(--text-primary);
}
.seg-btn.active {
  color: var(--seg-active-text, var(--primary));
}
.seg-btn svg {
  flex-shrink: 0;
}
.seg-example {
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  opacity: 0.7;
}
.seg-btn.active .seg-example {
  opacity: 1;
}
[data-theme="dark"] .seg-control {
  background: var(--gray-200);
}
[data-theme="dark"] .seg-btn.active {
  background: var(--surface);
}
[dir="rtl"] .seg-control::before {
  left: auto;
  right: 3px;
  transform: translateX(calc(var(--seg-index, 0) * -100%));
}
/* Wrap variant — used when there are too many segments to fit in one row
   (e.g. punch-type picker with break_in/break_out/ot_in/ot_out enabled).
   Drops the sliding indicator and instead paints the active button with a
   solid background so the layout still looks clean across two rows. */
.seg-control.seg-wrap {
  display: flex;
  flex-wrap: wrap;
  width: 100%;
  gap: 3px;
}
.seg-control.seg-wrap::before { display: none; }
.seg-control.seg-wrap .seg-btn {
  flex: 1 1 calc(50% - 3px);
  min-width: 0;
  white-space: normal;
  text-align: center;
  padding: 0.45rem 0.55rem;
  border-radius: 8px;
}
.seg-control.seg-wrap .seg-btn.active {
  background: var(--seg-color, var(--surface));
  color: var(--seg-active-text, var(--primary));
  box-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
}
.info-banner {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.85rem;
  margin-bottom: 0.75rem;
  background: #eff6ff;
  border: 1px solid #bfdbfe;
  border-radius: var(--radius, 8px);
  font-size: 0.82rem;
  color: #1d4ed8;
}
[data-theme="dark"] .info-banner {
  background: rgba(59, 130, 246, 0.1);
  border-color: rgba(59, 130, 246, 0.25);
  color: #93c5fd;
}

/* Warning variant of .info-banner — used by the raw-punches sync freshness
   notice on the Attendance page. Reuses the existing warning palette so it
   visually aligns with badges/flag pills. */
.info-banner.info-banner-warning {
  background: var(--warning-bg);
  border-color: var(--warning-light);
  color: var(--warning-text);
}
.info-banner.info-banner-warning svg { color: var(--warning); }
[data-theme="dark"] .info-banner.info-banner-warning {
  background: var(--warning-bg);
  border-color: var(--warning-light);
  color: var(--warning-text);
}

/* ─── App Tooltip (JS-powered, rendered in body) ─── */
.app-tooltip-el {
  position: fixed;
  z-index: 99999;
  max-width: min(16rem, calc(100vw - 2rem));
  background: var(--surface, #ffffff);
  color: var(--text-primary, #111827);
  border: 1px solid var(--border-color, #e5e7eb);
  padding: 0.45rem 0.65rem;
  border-radius: 8px;
  font-size: 0.78rem;
  font-weight: 500;
  line-height: 1.35;
  white-space: normal;
  word-break: break-word;
  overflow-wrap: anywhere;
  text-align: start;
  box-shadow: var(--shadow-lg);
  pointer-events: none;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.app-tooltip-el.visible {
  opacity: 1;
  transform: translateY(0);
}
[data-theme="dark"] .app-tooltip-el {
  background: var(--surface, #1e293b);
  color: var(--text-primary, #f8fafc);
  border-color: var(--gray-300, #475569);
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none !important;
  box-shadow: none !important;
}

.btn-sm {
  padding: 0.3rem 0.6rem;
  font-size: 0.75rem;
  border-radius: 6px;
}

.btn-group {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

/* Device config action bar */
.device-config-actions {
  margin-top: 0.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
}
.device-config-buttons {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}

/* Action cell buttons inline */
td .btn-group {
  display: inline-flex;
}

/* Icon-only action buttons */
.btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border-radius: 8px;
  border: 1px solid var(--gray-200);
  background: var(--surface);
  cursor: pointer;
  transition: all var(--transition);
  color: var(--gray-500);
}

.btn-icon svg {
  width: 15px;
  height: 15px;
}

.btn-icon:hover {
  background: var(--gray-50);
  border-color: var(--gray-300);
  color: var(--gray-700);
  transform: translateY(-1px);
  box-shadow: var(--shadow-sm);
}

.btn-icon.btn-icon-danger:hover {
  background: var(--danger-light);
  border-color: var(--danger);
  color: var(--danger);
}

/* Disabled icon buttons (e.g. a team-punch manager's own raw-punch rows, which
 * remain visible so the restriction is understandable but cannot be actioned).
 * Native `disabled` is the source of truth; this is the visual treatment. */
.btn-icon:disabled,
.btn-icon[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}
.btn-icon:disabled:hover,
.btn-icon[disabled]:hover {
  background: var(--surface);
  border-color: var(--gray-200);
  color: var(--gray-500);
  transform: none;
  box-shadow: none;
}
/* The enclosing wrapper carries the explanatory title and stays hover-able even
 * though the disabled buttons inside it swallow pointer events. */
.raw-actions-disabled {
  display: inline-flex;
  gap: 0.25rem;
  cursor: not-allowed;
}

/* ─── Forms ─── */
.form-group {
  margin-bottom: 1.15rem;
}

.form-group label {
  display: block;
  font-weight: 500;
  margin-bottom: 0.4rem;
  font-size: 0.835rem;
  color: var(--gray-700);
}

.form-group input:not([type="radio"]),
.form-group select,
.form-group textarea {
  width: 100%;
  max-width: 100%;
  padding: 0.55rem 0.85rem;
  border: 1px solid var(--gray-300);
  border-radius: 8px;
  font-size: 0.875rem;
  font-family: inherit;
  color: var(--gray-900);
  background: var(--surface);
  transition: all var(--transition);
  height: 2.5rem;
  box-sizing: border-box;
}

/* Radios are native controls, not text fields — the box styling above must
   never stretch them into full-width bordered boxes (which broke the leave-type
   eligibility options in RTL). Render them natively beside their label and keep
   the dot on-brand in both themes. Checkboxes are intentionally untouched here:
   every checkbox is already wrapped in a self-styled toggle/custom component. */
.form-group input[type="radio"] {
  width: auto;
  height: auto;
  flex-shrink: 0;
  accent-color: var(--primary);
}

.form-group input[type="date"],
.form-group input[type="time"],
.form-group input[type="datetime-local"] {
  -webkit-appearance: none;
  appearance: none;
}

.form-group input:focus:not([type="radio"]),
.form-group select:focus,
.form-group textarea:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-50);
}

/* ─── Inline field errors (field-error.js) ─── */
/* Tokenized (dark-safe) and free of directional properties (RTL-safe).
   .has-error covers fields inside a .form-group; [aria-invalid] also catches
   id-less/ungrouped inputs (e.g. the per-day shift time inputs). */
.field-error {
  color: var(--danger-text);
  font-size: 0.8rem;
  margin-top: 0.25rem;
}

/* Generalized from the .filter-bar treatment further down. In normal flow an
   error grows only its OWN column, so a sibling sharing a .form-row is left
   visibly out of line with it. Out of flow, the group's box never changes and
   only the erroring group reserves the space it needs; content below shifts
   once, which is honest feedback rather than a defect.

   Safe to make every group a containing block: .ss-container and .ms-container
   declare position:relative themselves, so their dropdowns still anchor to the
   widget, and every other absolutely-positioned control that can sit inside a
   group (.password-toggle-btn, .custom-check, .seg-control::before,
   .toggle-slider, .sync-interval-menu, .split-dropdown-menu) has its own
   positioned parent. All properties are logical, so RTL needs no override.

   The 7-day shift grid deliberately keeps the in-flow default: a
   .shift-day-time-cell is neither of these, and an absolutely positioned
   message inside a ~140px cell would overlap the row beneath. */
.form-group,
.qa-field {
  position: relative;
}

/* Reserve only where the message really is a direct child - i.e. only where the
   rule below actually takes it out of flow. Those same 7-day cells are nested
   inside #shift-day-schedules-group, so closest() marks THAT .form-group with
   .has-error while the message stays in the cell; without this guard the group
   would grow a phantom 1.35rem the message never occupies. */
.form-group.has-error:has(> .field-error),
.qa-field.has-error:has(> .field-error) {
  padding-block-end: 1.35rem;
}

/* `> ` on purpose: the message is inserted as the control's next sibling, so it
   is a direct child exactly when the control is. */
.form-group.has-error > .field-error,
.qa-field.has-error > .field-error {
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  inset-inline-end: 0;
  margin-block-start: 0;
}
.form-group.has-error input,
.form-group.has-error select,
.form-group.has-error textarea,
input[aria-invalid="true"],
select[aria-invalid="true"],
textarea[aria-invalid="true"] {
  border-color: var(--danger);
}

.form-group textarea {
  height: auto;
}

.form-group select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.85rem center;
  padding-right: 2.25rem;
  padding-top: 0;
  padding-bottom: 0;
  line-height: calc(2.5rem - 2px);
}

[dir="rtl"] .form-group select {
  background-position: left 0.85rem center;
  padding-right: 0.85rem;
  padding-left: 2.25rem;
}

/* Native date/time pickers display HH:MM / Y-M-D segments in document order,
   so under dir="rtl" Safari reverses them (19:30 renders as 30:19). The value
   is unchanged — only the visual order is wrong. Force the segments LTR while
   keeping the field aligned to the start (right) edge of the RTL layout. */
[dir="rtl"] input[type="time"],
[dir="rtl"] input[type="date"] {
  direction: ltr;
  text-align: right;
}

.form-group input::placeholder {
  color: var(--gray-400);
}

.form-row {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
}

.form-row .form-group {
  flex: 1;
  min-width: 180px;
}

/* ─── Attendance Processing card — top section (scoped) ─── */
.ap-card .ap-subhead {
  display: block;
  font-weight: 500;
  font-size: 0.835rem;
  color: var(--gray-700);
  margin-bottom: 0.5rem;
  text-align: start;
}

.ap-card .ap-settings-list {
  display: flex;
  flex-direction: column;
}

.ap-card .ap-setting-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 0.9rem 0;
  border-top: 1px solid var(--border-color);
}

.ap-card .ap-setting-row:first-child {
  border-top: none;
  padding-top: 0.35rem;
}

.ap-card .ap-setting-info {
  min-width: 0;
  text-align: start;
}

.ap-card .ap-setting-info label {
  display: block;
  font-weight: 500;
  font-size: 0.835rem;
  color: var(--gray-700);
  margin-bottom: 0.2rem;
  cursor: pointer;
}

.ap-card .ap-setting-info small {
  display: block;
  font-size: 0.78rem;
  color: var(--gray-500);
  line-height: 1.35;
}

/* Each control is shrink-wrapped and pushed to the row's far edge by
   space-between (the left edge in RTL), so every control's left edge lands
   on one vertical axis regardless of the control's own width. */
.ap-card .ap-setting-control {
  flex: 0 0 auto;
  display: flex;
}

.ap-card .ap-setting-control input[type="number"] {
  width: 120px;
  text-align: start;
  padding: 0.55rem 0.85rem;
  height: 2.5rem;
  border: 1px solid var(--gray-300);
  border-radius: 8px;
  font-size: 0.875rem;
  font-family: inherit;
  color: var(--gray-900);
  background: var(--surface);
  box-sizing: border-box;
  transition: all var(--transition);
}

.ap-card .ap-setting-control input[type="number"]:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-50);
}

.ap-card .ap-setting-control input[type="number"]::placeholder {
  color: var(--gray-400);
}

.ap-card .ap-toggle {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  margin: 0;
}

/* Toggle state is conveyed by the switch position; hide the redundant label. */
.ap-card .ap-toggle .ap-toggle-state {
  display: none;
}

/* ─── Payroll generate section ─── */
.payroll-generate-layout {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

.payroll-generate-fields {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 1rem;
  align-items: end;
}

.payroll-generate-fields .form-group {
  min-width: 0;
  margin-bottom: 0;
}

/* Inline field errors here take the .filter-bar treatment, not the general D1
 * one, and for the same reason: this is a horizontal row of controls, so a
 * group that grows its own box changes the row. The grid is `align-items: end`,
 * so the general rule's 1.35rem reserve on the erroring groups grew the row and
 * pushed the (perfectly valid) Pay Group control 22px down - measured. Keep the
 * group's box fixed, hang the message below it, and let the CONTAINER reserve
 * the space: padding sits outside the grid area, so no item moves.
 *
 * These out-specify the general rules by one class each - (0,4,0) vs (0,3,0)
 * for the message, (0,4,0) vs (0,3,0) for the reserve - which is what lets the
 * `inset-block-end: auto` reset survive.
 *
 * Unlike the filter bar the message is NOT allowed to overrun its column: these
 * are 260px wide, wide enough for every message on this panel in both
 * languages, and two adjacent columns can be in error at once - an overrun
 * would have them overlap each other. */
.payroll-generate-fields .form-group.has-error:has(> .field-error) {
  padding-block-end: 0;
}

.payroll-generate-fields .form-group.has-error > .field-error {
  position: absolute;
  top: 100%;
  inset-block-end: auto;
  inset-inline-start: 0;
  inset-inline-end: 0;
  margin-top: 0.3rem;
}

.payroll-generate-fields:has(.form-group.has-error) {
  padding-block-end: 1.6rem;
}

.payroll-generate-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

.payroll-generate-actions .btn {
  min-height: 2.5rem;
}

/* ─── Generate Payroll card collapse toggle ─── */
#generate-payroll-card .btn-card-toggle {
  margin-inline-start: auto;
  flex-shrink: 0;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.2rem;
  color: var(--gray-400);
  border-radius: var(--radius-sm, 4px);
  display: flex;
  align-items: center;
  transition: color 0.15s, background 0.15s;
}
#generate-payroll-card .btn-card-toggle:hover {
  color: var(--gray-700);
  background: var(--gray-100);
}
#generate-payroll-card .btn-card-toggle:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
#generate-payroll-card .btn-card-toggle svg {
  transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
  transform: rotate(0deg);
}
#generate-payroll-card.is-collapsed .btn-card-toggle svg {
  transform: rotate(-180deg);
}
#generate-payroll-card .payroll-generate-layout {
  overflow: hidden;
  max-height: 400px;
  opacity: 1;
  transition: max-height 0.32s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease;
}
#generate-payroll-card.is-collapsed .payroll-generate-layout {
  max-height: 0;
  opacity: 0;
  pointer-events: none;
}
/* The collapse clip would cut off the pay-group dropdown, which escapes the
   layout box. Lift it only while a picker is open — collapsing closes the
   picker first (document click), so the animation still clips. */
#generate-payroll-card .payroll-generate-layout:has(.ms-container.ms-open),
#generate-payroll-card .payroll-generate-layout:has(.ss-container.ss-open) {
  overflow: visible;
}
#generate-payroll-card h3 {
  transition: margin-bottom 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
#generate-payroll-card.is-collapsed h3 {
  margin-bottom: 0;
}

/* ─── Split-button dropdown (e.g. Export PDF / Word) ─── */
.split-dropdown {
  position: relative;
  display: inline-flex;
  align-items: center;
} 
.split-dropdown .btn-export-main {
  border-radius: 8px;
}

.split-dropdown .btn-export-caret {
  border-radius: 0 8px 8px 0;
  padding: 0 0.45rem;
  border-left: 1px solid var(--gray-300);
}
[dir="rtl"] .split-dropdown .btn-export-caret {
  border-radius: 8px 0 0 8px;
  border-left: none;
  border-right: 1px solid var(--gray-300);
}
.split-dropdown .btn-export-caret svg {
  transition: transform 0.18s ease;
}
.split-dropdown.open .btn-export-caret svg {
  transform: rotate(180deg);
}
.split-dropdown-menu {
  display: none;
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 200;
  min-width: 170px;
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: 8px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.10);
  overflow: hidden;
}
[dir="rtl"] .split-dropdown-menu {
  left: auto;
  right: 0;
}
.split-dropdown.open .split-dropdown-menu {
  display: block;
}
.split-dropdown-menu button {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.55rem 1rem;
  background: none;
  border: none;
  font-size: 0.85rem;
  font-family: inherit;
  color: var(--gray-700);
  cursor: pointer;
  text-align: start;
  transition: background 0.15s;
}
.split-dropdown-menu button:hover {
  background: var(--gray-50);
}
.split-dropdown-menu button:disabled {
  color: var(--gray-400);
  cursor: not-allowed;
}

/* ─── Payroll selected card meta bar ─── */
.payroll-selected-card {
  transition: box-shadow 0.2s ease;
}

.payroll-meta-empty {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--text-muted);
  font-size: 0.85rem;
  padding: 0.35rem 0;
}

.payroll-meta-bar {
  display: flex;
  align-items: stretch;
  gap: 0;
  flex-wrap: wrap;
  background: var(--gray-50);
  border: 1px solid var(--gray-200);
  border-radius: var(--radius);
}

.payroll-meta-item {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.6rem 1.1rem;
  flex: 1;
  min-width: 150px;
}

.payroll-meta-divider {
  width: 1px;
  background: var(--gray-200);
  align-self: stretch;
}

.payroll-meta-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--primary);
  flex-shrink: 0;
}

.payroll-meta-content {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}

.payroll-meta-label {
  font-size: var(--fs-2xs);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  white-space: nowrap;
}

.payroll-meta-value {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--gray-800);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.payroll-meta-item--selector {
  flex: 1.2;
}

/* Override SearchableSelect styles when embedded in the meta bar */
.payroll-meta-item--selector .ss-container {
  width: auto;
}

.payroll-meta-item--selector .ss-trigger {
  border: none;
  background: transparent;
  padding: 0;
  min-height: unset;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--gray-800);
  box-shadow: none;
  gap: 0.3rem;
  width: auto;
}

.payroll-meta-item--selector .ss-trigger:hover,
.payroll-meta-item--selector .ss-trigger:focus,
.payroll-meta-item--selector .ss-container.ss-open .ss-trigger {
  border: none;
  box-shadow: none;
  color: var(--primary);
}

.payroll-meta-item--selector .ss-display {
  font-size: 0.9rem;
  font-weight: 600;
}

.payroll-meta-item--selector .ss-arrow {
  color: var(--primary);
  opacity: 0.75;
}

.payroll-meta-item--selector .ss-arrow svg {
  width: 16px;
  height: 16px;
  stroke-width: 3;
}

/* ─── Badges ─── */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.15rem 0.65rem;
  border-radius: 999px;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.badge-success { background: var(--badge-success-bg); color: var(--success-text); }
.badge-danger  { background: var(--badge-danger-bg); color: var(--danger-text); }
.badge-warning { background: var(--badge-warning-bg); color: var(--warning-text); }
.badge-info    { background: var(--primary-50); color: var(--primary-dark); }
.badge-muted   { background: var(--gray-100); color: var(--gray-500); }

/* A badge that is a real <button> (e.g. the "Debt repayment" jump on an
   adjustment row): strip the UA button chrome, keep the badge look, and
   signal clickability on hover/focus. */
button.badge { border: 0; cursor: pointer; font-family: inherit; }
button.badge:hover,
button.badge:focus-visible { text-decoration: underline; }

/* ─── Callout (.alert) ───
   Ten surfaces already ship `class="alert alert-warning"` - the three
   quick-action bulk-result panels, the punch-request draft-recovery warning,
   #pr-submit-error, #add-punch-bulk-result, #adj-bulk-result, the two leave
   panels and the debt carry-over disclosure - and NONE of them had a rule to
   match, so every one rendered as bare body text indistinguishable from the
   prose around it. Tinted block with a leading rule; logical properties, so the
   rule sits on the correct edge in RTL. */
.alert {
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--border-color);
  border-inline-start: 3px solid var(--gray-400);
  border-radius: var(--radius);
  background: var(--surface-alt);
  color: var(--text-secondary);
  font-size: 0.85rem;
  line-height: 1.55;
}
.alert > * + * { margin-block-start: 0.35rem; }

.alert-warning {
  background: var(--warning-light);
  border-color: transparent;
  border-inline-start-color: var(--warning);
  color: var(--warning-text);
}

/* Inactive employee row */
tr.row-inactive td {
  opacity: 0.55;
}

/* ─── Status indicators ─── */
.status-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-inline-end: 0.4rem;
}

.status-dot.online  { background: var(--success); box-shadow: 0 0 0 3px var(--success-light); }
.status-dot.offline { background: var(--gray-400); }
.status-dot.warning { background: var(--warning); box-shadow: 0 0 0 3px var(--warning-light); }

/* ─── Auto-filled cell highlight ─── */
.auto-filled {
  background: var(--warning-bg);
  font-style: italic;
}

/* ─── Flag Pills ─── */
.flag-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

.flag-pill {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.flag-danger  { background: var(--badge-danger-bg); color: var(--danger-text); }
.flag-warning { background: var(--badge-warning-bg); color: var(--warning-text); }
.flag-info    { background: var(--primary-50); color: var(--primary-dark); }
.flag-success { background: var(--badge-success-bg); color: var(--success-text); }
.flag-default { background: var(--gray-100); color: var(--gray-600); }
.flag-manual  { background: var(--purple-light); color: var(--purple-text); }

.next-day-punch-marker {
  display: inline-block;
  margin-inline-start: 0.25rem;
  color: var(--warning-text);
  font-size: 0.82rem;
  font-weight: 700;
  line-height: 1;
  cursor: help;
  vertical-align: 0.08em;
}

.next-day-punch-legend {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.45rem;
  color: var(--text-secondary);
  font-size: 0.78rem;
}

.next-day-punch-legend .next-day-punch-marker {
  margin-inline-start: 0;
}

/* ─── Spinner ─── */
.spinner {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  border: 2px solid var(--gray-200);
  border-top-color: var(--primary);
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}

.spinner-lg {
  width: 2rem;
  height: 2rem;
  border-width: 3px;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* Mutation-pending state for buttons (withPending in api.js). The spinner is a ::after so
   it never clobbers button content; logical properties keep it RTL-safe. Reuses @keyframes
   spin above. Not .spinner — a later white-on-color .spinner override breaks on secondary buttons. */
.btn.is-pending {
  pointer-events: none;
}
.btn.is-pending::after {
  content: '';
  display: inline-block;
  width: 1em;
  height: 1em;
  border: 2px solid currentColor;
  border-inline-start-color: transparent;
  border-radius: 50%;
  margin-inline-start: 0.5em;
  animation: spin 0.6s linear infinite;
}

/* Page spinner */
.page-loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 200px;
  color: var(--text-muted);
  gap: 1rem;
  font-size: 0.875rem;
}

/* ─── Skeleton / Shimmer Loading ─── */
.skeleton {
  background: var(--gray-200);
  background: linear-gradient(90deg, var(--gray-200) 25%, var(--gray-100) 50%, var(--gray-200) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s ease-in-out infinite;
  border-radius: 6px;
  display: inline-block;
}

@keyframes shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

.skeleton-text {
  height: 0.875rem;
  width: 100%;
  margin-bottom: 0.5rem;
}

.skeleton-text-sm {
  height: 0.7rem;
  width: 60%;
}

.skeleton-text-lg {
  height: 1.5rem;
  width: 40%;
}

.skeleton-badge {
  height: 1.4rem;
  width: 4.5rem;
  border-radius: 999px;
}

.skeleton-circle {
  border-radius: 50%;
}

.skeleton-btn {
  height: 2rem;
  width: 2rem;
  border-radius: 8px;
}

/* Skeleton stat card - replaces stat-card content while loading */
.stat-card .skeleton-value {
  height: 1.75rem;
  width: 3rem;
  margin: 0.35rem 0;
}

.stat-card .skeleton-sub {
  height: 0.7rem;
  width: 5rem;
}

/* Skeleton table rows */
.skeleton-row td {
  padding: 0.85rem 1rem;
}

.skeleton-row .skeleton {
  animation-delay: calc(var(--row) * 0.08s);
}

/* DataTable fetch-error row — visually distinct from the muted empty state. */
.dt-error-row td {
  color: var(--danger);
  text-align: center;
  padding: 2rem;
}

/* ─── View tabs ─── */
.view-tabs {
  display: flex;
  gap: 0;
  margin-block-end: 1rem;
  /* --gray-300, not --gray-200: the row sits directly on --body-bg (= --gray-100
     in light), against which --gray-200 is invisible. */
  border-block-end: 1px solid var(--gray-300);
}

.view-tab {
  padding: 0.65rem 1.3rem;
  background: none;
  border: none;
  border-block-end: 3px solid transparent;
  /* Pulls the tab's own indicator over the 1px track so the active underline
     sits on top of it rather than beside it. */
  margin-block-end: -1px;
  cursor: pointer;
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--text-secondary);
  transition: color 0.15s, border-color 0.15s;
}

/* Zero-shift active state. `.view-tab.active` goes to weight 600, which widens
   the tab and nudges every tab after it on each switch: measured at 19px on the
   settings row in English and 25px in Arabic, plainly visible as the trailing
   tabs sliding. This ghost reserves the BOLD width at all times, so the box
   never changes size and only the ink does. It is fed by data-label because CSS
   cannot read an element's own text; applyLanguage() in i18n.js mirrors the
   translated label into that attribute, so it stays correct in both languages.
   A tab with no data-label simply reserves nothing and behaves as before. */
.view-tab::after {
  content: attr(data-label);
  display: block;
  height: 0;
  overflow: hidden;
  /* visibility, not just zero height: it also keeps the ghost text out of the
     accessibility tree, so the tab's accessible name stays un-doubled. */
  visibility: hidden;
  font-weight: 600;
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;   /* E1: chrome, not data — no long-press callout */
}

.view-tab:not(.active):hover {
  color: var(--text-primary);
  border-block-end-color: var(--gray-300);
}

.view-tab:focus-visible {
  outline: 2px solid var(--primary);
  /* Negative, unlike the house +2px: on mobile .view-tabs is overflow-x:auto,
     which clips a positive offset on the first and last tab. */
  outline-offset: -2px;
}

.view-tab.active {
  color: var(--primary);
  border-block-end-color: var(--primary);
  font-weight: 600;
}

/* A row with exactly one tab (employee-settings) has nothing to switch to, and
   that tab is always the active one, so the pointer cursor promises a click
   that can never do anything. Now that the track makes the row read clearly as
   a control, that false affordance is worth removing. Structural, not
   visibility-based: rows whose extra tabs are hidden per-permission still have
   several children and keep the pointer. */
.view-tabs .view-tab:only-child {
  cursor: default;
}

.pager-controls {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.75rem 0;
  flex-wrap: wrap;
}

.pager-info {
  margin-inline-start: 0.75rem;
  font-size: 0.82rem;
  color: var(--gray-500);
}

/* ─── Filter bar ─── */
.filter-bar {
  display: flex;
  gap: 1rem;
  align-items: flex-end;
  flex-wrap: wrap;
  margin-bottom: 1.5rem;
  padding: 1.25rem;
  padding-top: 4.25rem;
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  position: relative;
}

.filter-bar-header {
  position: absolute;
  top: 0.6rem;
  inset-inline-end: 0.75rem;
  z-index: 1;
}

.filter-bar .form-group {
  margin-bottom: 0;
}

.filter-bar .form-group:has(#filter-employee-ms),
.filter-bar .form-group:has(#adj-employee-filter) {
  min-width: 260px;
}

.filter-bar .form-group:has(#filter-flags-ms) {
  min-width: 200px;
}

.filter-bar .form-group:has(input[type="date"]) {
  min-width: 180px;
}

.filter-bar input,
.filter-bar select,
.filter-bar .ms-trigger {
  height: 2.375rem;
}

.filter-bar > .btn {
  height: 2.375rem;
  padding-top: 0;
  padding-bottom: 0;
}

/* Inline field errors inside a filter bar (the auto-applying filters show one
 * when a date range is backwards). The bar is `align-items: flex-end`, so an
 * error left in normal flow makes its own group taller and shoves that group's
 * label and input UPWARD relative to its neighbours - the erroring field ends up
 * visibly out of line with every other control. Take the message out of flow so
 * the group's box never changes size, and grow only the bar's bottom padding to
 * make room for it. Nothing moves: the flex line's cross size is unaffected, so
 * even the `align-self: center` Reset button stays put.
 *
 * These are the SPECIALISATION of the general .form-group.has-error rules near
 * the .field-error block above, and they have to out-specify them to survive:
 * `.form-group.has-error > .field-error` (0,3,0) would otherwise beat a plain
 * `.filter-bar .field-error` (0,2,0) and pin the message to the group's block
 * end with both `top` and `inset-block-end` set - which stretches the box - and
 * kill the `width: max-content` overrun below. The bar reserves the space here,
 * so the group itself must not. */
.filter-bar .form-group {
  position: relative;
}

.filter-bar .form-group.has-error:has(> .field-error) {
  padding-block-end: 0;
}

.filter-bar .form-group.has-error > .field-error {
  position: absolute;
  top: 100%;
  inset-block-end: auto;
  inset-inline-start: 0;
  /* Allowed to run past its own group toward the bar's inline end rather than
     wrap: these groups are only ~180px wide and a wrapped message would need
     twice the reserved padding below. */
  inset-inline-end: auto;
  min-width: 100%;
  width: max-content;
  max-width: 24rem;
  margin-top: 0.3rem;
}

/* `has-error` is toggled on the .form-group by setFieldError/clearFieldError,
 * so the reserve appears with the message and goes away with it. */
.filter-bar:has(.form-group.has-error) {
  padding-bottom: 2.5rem;
}

body.modal-open {
  overflow: hidden;
}

/* ─── Modal ─── */
.modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: var(--overlay-bg);
  backdrop-filter: blur(4px);
  z-index: 100;
  justify-content: center;
  align-items: center;
  animation: fadeIn 0.15s ease;
}

.modal-overlay.active {
  display: flex;
}

/* Confirm / alert / prompt dialogs (confirm.js) must sit above every other
   overlay - standard modals and the dashboard quick-action modals (z-index
   1000) - or their buttons render behind and become unclickable (e.g. the
   bulk confirm-with-count step). confirm.js builds its overlays outside the
   appModal stack, so the per-stack-depth inline z-index modal.js stamps on
   stacked modals (100, 110, 120, …) never reaches this. */
.modal-overlay.confirm-dialog-overlay {
  z-index: 2000;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes slideUp {
  from { opacity: 0; transform: translateY(12px) scale(0.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

.modal {
  background: var(--surface);
  border-radius: var(--radius-lg);
  padding: 0;
  width: 90%;
  max-width: 520px;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  box-shadow: var(--shadow-lg);
  animation: slideUp 0.2s ease;
}

.modal:focus {
  outline: none;
}

.modal-header {
  padding: 1rem 1.5rem;
  border-bottom: 1px solid var(--gray-100);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
}

.modal-header h3 {
  margin: 0;
  font-size: 1.05rem;
  font-weight: 600;
}

/* Minimal icon-only modal close button (no background, balanced spacing) */
.modal-close-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.25rem;
  margin: -0.25rem;
  background: none;
  border: none;
  border-radius: var(--radius);
  color: var(--gray-500);
  line-height: 1;
  cursor: pointer;
  transition: color var(--transition);
}
.modal-close-btn:hover,
.modal-close-btn:focus-visible {
  color: var(--gray-900);
  background: none;
  outline: none;
}

.modal-body {
  padding: 1rem 1.5rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.modal h3 {
  margin-bottom: 1rem;
  font-size: 1.05rem;
  font-weight: 600;
}

.accordion-header {
  width: 100%;
  border: 0;
  background: transparent;
  color: var(--gray-500);
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;   /* E1: chrome, not data — no long-press callout */
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0;
  margin: 0 0 0.65rem;
  font: inherit;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.05em;
  text-align: start;
  text-transform: uppercase;
}

.accordion-header:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 3px;
  border-radius: 4px;
}

.accordion-title {
  min-width: 0;
}

.accordion-badge {
  margin-inline-start: 0.25rem;
  letter-spacing: 0;
  text-transform: none;
}

.accordion-chevron {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 0.75rem;
  block-size: 0.75rem;
  flex: 0 0 auto;
  color: var(--gray-400);
  font-size: 0.75rem;
  line-height: 1;
  transition: transform 200ms ease;
  transform-origin: center;
}

.accordion-open > .accordion-header .accordion-chevron {
  transform: rotate(90deg);
}

.accordion-content {
  display: none;
}

.accordion-open > .accordion-content {
  display: block;
}

.modal-actions {
  display: flex;
  gap: 0.75rem;
  justify-content: flex-end;
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--gray-100);
}

/* ─── Custom Confirm / Alert Dialog ─── */
.confirm-dialog {
  max-width: 420px;
}

.confirm-dialog-body {
  padding: 1.75rem 1.5rem 0.5rem;
  display: flex;
  gap: 1rem;
  align-items: flex-start;
}

.confirm-dialog-icon {
  width: 44px;
  height: 44px;
  min-width: 44px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.confirm-dialog-content {
  flex: 1;
  min-width: 0;
}

.confirm-dialog-title {
  font-size: 1rem;
  font-weight: 600;
  margin: 0 0 0.35rem;
  color: var(--gray-900);
}

.confirm-dialog-message {
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--gray-600);
  margin: 0;
  white-space: pre-line;
}

/* Inline error shown inside a confirm dialog when its async action rejects
   (P4 async-confirm actions). Reuses the .btn.is-pending spinner for the
   confirm button; this is the only P4-specific confirm CSS. */
.confirm-dialog-error {
  color: var(--danger);
  margin-block-start: 0.5rem;
  font-size: 0.85rem;
}

.confirm-dialog .modal-actions {
  padding: 1rem 1.5rem 1.25rem;
  margin-top: 1rem;
}

.confirm-dialog-input-section {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid var(--gray-200);
  width: 100%;
}

/* ── Retroactive-shift preflight dialog ──────────────────────────────────────
   Reuses the confirm-dialog shell (and its class name, which modal.js keys its
   Escape layering off) but carries lists and a radio choice, so it needs more
   width and its own vertical rhythm. Directional properties are logical
   throughout: this dialog renders in Arabic RTL as often as in English. */
.retro-shift-dialog {
  max-width: 560px;
}

.retro-shift-dialog .confirm-dialog-content {
  max-height: min(62vh, 34rem);
  overflow-y: auto;
}

.retro-lead {
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--gray-700);
  margin: 0 0 0.75rem;
}

.retro-facts {
  margin: 0 0 0.9rem;
  padding-inline-start: 1.1rem;
  font-size: 0.84rem;
  line-height: 1.65;
  color: var(--gray-600);
}

/* The one fact in the list that describes a LOSS rather than a recalculation.
   Weighted, not coloured red: it is an expected consequence of the correction
   HR asked for, not an error, and the dialog already carries a warning icon. */
.retro-facts .retro-fact-loss {
  color: var(--gray-800);
  font-weight: 600;
}

.retro-hint {
  font-size: 0.84rem;
  line-height: 1.6;
  color: var(--gray-600);
  margin: 0.75rem 0 0;
}

.retro-choice {
  border: 1px solid var(--gray-200);
  border-radius: var(--radius-md, 8px);
  padding: 0.85rem;
  margin-bottom: 0.9rem;
  background: var(--gray-50);
}

.retro-choice-lead {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--gray-800);
  margin: 0 0 0.65rem;
}

.retro-radio {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  padding: 0.45rem 0;
  cursor: pointer;
}

.retro-radio input {
  margin-block-start: 0.2rem;
  flex: 0 0 auto;
}

.retro-radio-title {
  display: block;
  font-size: 0.87rem;
  color: var(--gray-800);
}

.retro-radio-note {
  display: block;
  font-size: 0.78rem;
  color: var(--gray-500);
  margin-block-start: 0.15rem;
}

.retro-warn {
  border-inline-start: 3px solid var(--warning, #d97706);
  background: var(--warning-light);
  border-radius: var(--radius-sm, 6px);
  padding: 0.6rem 0.75rem;
  margin-bottom: 0.75rem;
}

.retro-warn-blocked {
  border-inline-start-color: var(--danger, #dc2626);
  background: var(--danger-light);
}

.retro-warn-title {
  font-size: 0.83rem;
  font-weight: 600;
  color: var(--gray-800);
}

.retro-warn-list {
  margin: 0.35rem 0 0;
  padding-inline-start: 1.1rem;
  font-size: 0.8rem;
  line-height: 1.6;
  color: var(--gray-700);
}

.retro-emps {
  margin-block-start: 0.5rem;
}

.retro-emp {
  border-top: 1px solid var(--gray-200);
  padding: 0.4rem 0;
}

.retro-emp > summary {
  cursor: pointer;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--gray-800);
}

.retro-emp-line {
  font-size: 0.79rem;
  color: var(--gray-600);
  padding-inline-start: 1rem;
  margin-block-start: 0.2rem;
}

.retro-emp-spans {
  /* Date spans are LTR content; the isolate keeps them from reordering inside
     an RTL run, and the overflow keeps a long list from widening the dialog. */
  overflow-x: auto;
  white-space: nowrap;
}

.retro-emp-blocked {
  color: var(--danger);
}

.confirm-dialog-input-section label {
  display: block;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--gray-700);
  margin-bottom: 0.4rem;
}

.confirm-input {
  width: 100%;
  box-sizing: border-box;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--gray-300);
  border-radius: var(--radius);
  font-size: 0.875rem;
  color: var(--gray-900);
  background: var(--surface);
  transition: border-color 0.15s, box-shadow 0.15s;
  outline: none;
}

.confirm-input:focus {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-50);
}

.confirm-input::placeholder {
  color: var(--gray-400);
}

/* ─── Page Transition ─── */
.main-content {
  animation: pageFadeIn 0.18s ease-out;
}

@keyframes pageFadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ─── Toast notifications ─── */
.toast-container {
  position: fixed;
  top: 1.5rem;
  right: 1.5rem;
  left: auto;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.85rem 1.25rem;
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  font-size: 0.875rem;
  font-weight: 500;
  min-width: 280px;
  max-width: 420px;
  animation: toastIn 0.3s ease;
  pointer-events: auto;
}

.toast.toast-success { border-left: 4px solid var(--success); }
.toast.toast-error   { border-left: 4px solid var(--danger); }
.toast.toast-warning { border-left: 4px solid var(--warning); }
.toast.toast-info    { border-left: 4px solid var(--primary); }

.toast-icon {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
}

/* Optional trailing action button (showToast opts.action). Logical margins so
   it sits after the message in both LTR and RTL. */
.toast-action {
  flex-shrink: 0;
  margin-inline-start: auto;
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--gray-300);
  border-radius: var(--radius-sm, 6px);
  background: transparent;
  color: var(--primary);
  font-family: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

.toast-action:hover {
  background: color-mix(in srgb, var(--primary) 10%, transparent);
  border-color: var(--primary);
}

.toast-action:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

.toast.hiding {
  animation: toastOut 0.3s ease forwards;
}

@keyframes toastIn {
  from { opacity: 0; transform: translateX(30px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes toastOut {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(30px); }
}

/* ─── Empty state ─── */
.empty-state {
  text-align: center;
  padding: 3rem 2rem;
  color: var(--text-muted);
}

.empty-state svg {
  width: 48px;
  height: 48px;
  margin-bottom: 1rem;
  opacity: 0.5;
}

.empty-state h4 {
  font-size: 1rem;
  font-weight: 600;
  color: var(--gray-600);
  margin-bottom: 0.35rem;
}

.empty-state p {
  font-size: 0.85rem;
  color: var(--text-muted);
}

/* ─── Section toolbar ─── */
.section-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 1rem;
}

.section-toolbar .btn {
  height: 2.375rem;
  padding-top: 0;
  padding-bottom: 0;
}

.section-toolbar h3 {
  font-size: 1rem;
  font-weight: 600;
  color: var(--gray-800);
}
.leaves-records-toolbar {
  justify-content: flex-start;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.leaves-status-filter {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.leaves-records-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-inline-start: auto;
}

.search-box {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--white);
  /* gray-300 + a resting shadow: this box sits on the gray page, not inside a
     white card, so the gray-200 the card-bound inputs use has no edge here. */
  border: 1px solid var(--gray-300);
  border-radius: 0.5rem;
  padding: 0.5rem 0.75rem;
  box-shadow: var(--shadow-sm);
  transition: border-color 0.15s, box-shadow 0.15s;
}
.search-box:focus-within {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-50);
}
.search-box svg { color: var(--gray-400); flex-shrink: 0; }
.search-box:focus-within svg { color: var(--primary); }
.search-box input {
  border: none;
  outline: none;
  background: transparent;
  font-size: 0.875rem;
  color: var(--gray-800);
  width: 200px;
}
.search-box input::placeholder { color: var(--gray-400); }

/* ─── Utility ─── */
.text-muted { color: var(--gray-500); }
.text-right { text-align: right; }
.mt-1 { margin-top: 0.5rem; }
.mb-1 { margin-bottom: 0.5rem; }
.hidden { display: none !important; }
.flex { display: flex; }
.items-center { align-items: center; }
.gap-2 { gap: 0.5rem; }

/* ─── Multi-Select & Bulk Edit ─── */
.col-check { width: 3rem; text-align: center; }

/* ─── Custom Checkbox ─── */
.custom-check {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  cursor: pointer;
  vertical-align: middle;
}
.custom-check input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}
.custom-check .check-box {
  width: 1rem;
  height: 1rem;
  border: 1.5px solid var(--gray-300);
  border-radius: 4px;
  background: var(--surface);
  transition: all 0.15s ease;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.custom-check input:checked + .check-box {
  background: var(--gray-500);
  border-color: var(--gray-500);
}
.custom-check input:checked + .check-box::after {
  content: '';
  display: block;
  width: 4px;
  height: 8px;
  border: solid #fff;
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(45deg) translateY(-0.5px);
}
.custom-check input:indeterminate + .check-box {
  background: var(--gray-500);
  border-color: var(--gray-500);
}
.custom-check input:indeterminate + .check-box::after {
  content: '';
  display: block;
  width: 7px;
  height: 0;
  border-bottom: 1.5px solid #fff;
}
.custom-check input:focus-visible + .check-box {
  box-shadow: 0 0 0 3px rgba(107,114,128,0.15);
}
.custom-check:hover .check-box {
  border-color: var(--gray-500);
}
[data-theme="dark"] .custom-check .check-box {
  border-color: var(--gray-600);
}
[data-theme="dark"] .custom-check input:checked + .check-box {
  background: var(--gray-400);
  border-color: var(--gray-400);
}
[data-theme="dark"] .custom-check:hover .check-box {
  border-color: var(--gray-400);
}

tr.row-selected { background: rgba(99,102,241,0.06) !important; }
[data-theme="dark"] tr.row-selected { background: rgba(129,140,248,0.08) !important; }

/* ─── Bulk Toolbar ─── */
.bulk-toolbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  animation: fadeIn 0.15s ease;
}

/* Bulk Edit Modal */
.bulk-edit-hint {
  font-size: 0.85rem;
  color: var(--gray-500);
  margin-bottom: 1rem;
}
.bulk-field-group {
  border-bottom: 1px solid var(--gray-100);
  padding: 0.75rem 0;
}
.bulk-field-group:last-of-type { border-bottom: none; }
.bulk-field-toggle {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
  font-weight: 500;
  font-size: 0.9rem;
  color: var(--gray-700);
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;   /* E1: chrome, not data — no long-press callout */
}
.bulk-field-input {
  margin-top: 0.5rem;
  padding-inline-start: 1.75rem;
}
.bulk-field-input select,
.bulk-field-input input,
.bulk-field-input textarea {
  width: 100%;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--gray-300);
  border-radius: 8px;
  font-size: 0.875rem;
  font-family: inherit;
  color: var(--gray-900);
  background: var(--surface);
  height: 2.4rem;
  box-sizing: border-box;
  transition: all 0.15s ease;
}
.bulk-field-input select:focus,
.bulk-field-input input:focus,
.bulk-field-input textarea:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-50);
}
.bulk-field-input textarea {
  height: auto;
}
.bulk-field-input select {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.75rem center;
  padding-right: 2.25rem;
  padding-top: 0;
  padding-bottom: 0;
  line-height: calc(2.4rem - 2px);
  cursor: pointer;
}
[dir="rtl"] .bulk-field-input select {
  background-position: left 0.75rem center;
  padding-right: 0.75rem;
  padding-left: 2.25rem;
}

/* ─── Utilities ─── */
.gap-3 { gap: 0.75rem; }

/* ─── Responsive ─── */
/* ─── Mobile Top Bar ─── */
.mobile-topbar {
  display: none;
  position: sticky;
  top: 0;
  z-index: 60;
  height: 52px;
  background: var(--sidebar-bg);
  color: #fff;
  align-items: center;
  justify-content: space-between;
  padding: 0 1rem;
  gap: 0.75rem;
}
.mobile-topbar-title {
  font-weight: 700;
  font-size: 1.05rem;
  flex: 1;
  text-align: center;
}
.mobile-hamburger,
.mobile-topbar-theme {
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: #fff;
  cursor: pointer;
  border-radius: 8px;
  padding: 0;
  flex-shrink: 0;
  text-decoration: none;
}
.mobile-hamburger:hover,
.mobile-topbar-theme:hover {
  background: rgba(255,255,255,0.1);
}
/* show sun in light mode, moon in dark mode */
.mobile-topbar-theme .topbar-icon-moon { display: none; }
[data-theme="dark"] .mobile-topbar-theme .topbar-icon-sun  { display: none; }
[data-theme="dark"] .mobile-topbar-theme .topbar-icon-moon { display: block; }

/* ─── Sidebar Backdrop ─── */
.sidebar-backdrop {
  display: none;
  position: fixed;
  inset: 52px 0 0 0;
  background: rgba(0,0,0,0.5);
  z-index: 49;
  opacity: 0;
  transition: opacity 0.25s ease;
}
.sidebar-backdrop.active {
  display: block;
  opacity: 1;
}

/* L1: icon-only rail retired. Below 768px = mobile topbar + off-canvas
   drawer with full labels (see the max-width:768px block below); at/above
   768px = full sidebar. No unlabeled 72px rail state. */

@media (max-width: 768px) {
  .mobile-topbar {
    display: flex;
  }
  .sidebar {
    display: flex;
    position: fixed;
    top: 52px;
    inset-inline-start: 0;
    bottom: 0;
    width: 180px;
    transform: translateX(-100%);
    transition: transform 0.3s ease;
    z-index: 55;
  }
  .sidebar.mobile-open {
    transform: translateX(0);
    width: 180px;
  }
  /* Drawer, opened: topbar already shows the brand, so hide the in-drawer
     brand and lay nav out with full labels + touch spacing. */
  .sidebar.mobile-open .sidebar-brand {
    display: none;
  }
  .sidebar.mobile-open .sidebar-section {
    display: block;
    opacity: 1;
    height: auto;
    padding: 0.6rem 1.25rem 0.3rem;
    pointer-events: auto;
  }
  /* 1rem, not 1.25rem: the drawer is 180px and a badged row spends 62px of it on
     icon + two gaps + the count pill, so at 20px of inline padding the Arabic
     `طلبات البصمة` misses by 3px and the guard above ellipsises a label that
     almost fits — trading a clipped badge for a truncated label on the primary
     language. 16px leaves it 4.8px of slack and matches `.main-content`'s own
     mobile padding. The ellipsis stays as the guard for longer translations. */
  .sidebar.mobile-open nav a {
    justify-content: flex-start;
    padding: 0.85rem 1rem;
    min-height: 48px;
    gap: 0.75rem;
    border-left-color: transparent;
  }
  /* The LABEL span only — `:not()` because the nav badge is a `span` too, and it
     is an `inline-flex` centring box. This rule is (0,2,3) and the badge's own
     rule is (0,1,0), so an unqualified `span` here re-displayed the badge as
     `inline` (blockified to `block`, since it is a flex item of the anchor),
     which threw away `align-items: center` and dropped the digit 3px above the
     circle's centre — 15% of its height — in the mobile drawer ONLY, while the
     desktop sidebar stayed correct. */
  .sidebar.mobile-open nav a span:not(.sidebar-nav-badge) {
    display: inline;
  }
  .sidebar.mobile-open .sidebar-footer {
    display: flex;
  }
  .sidebar.mobile-open .sidebar-collapse-btn {
    display: none;
  }
  .app {
    flex-direction: column;
  }
  .main-content,
  .app.sidebar-collapsed .main-content {
    margin-inline-start: 0;
    padding: 1rem;
  }
  .stats-row {
    grid-template-columns: 1fr;
  }
  .filter-bar {
    flex-direction: column;
    align-items: stretch;
  }
  /* Keep the page label, drop the subtitle: on a phone the explainer line
     costs more than it says. The child selectors spare nested section headers
     (settings' Audit Log tab reuses the class). */
  .main-content > .page-header {
    margin-bottom: 1rem;
  }
  .main-content > .page-header > p {
    display: none;
  }
  .page-header h2 {
    font-size: 1.25rem;
  }
  .page-header p {
    font-size: 0.8rem;
  }

  /* Phase 2: Touch targets */
  .btn {
    min-height: 44px;
    padding: 0.6rem 1rem;
    font-size: 0.875rem;
  }
  .btn-sm {
    min-height: 38px;
    padding: 0.45rem 0.85rem;
  }
  .btn-icon {
    width: 44px;
    height: 44px;
  }
  .form-group input:not([type="radio"]),
  .form-group select,
  .form-group textarea {
    height: 44px;
    font-size: 16px; /* prevents iOS zoom */
    max-width: 100%;
  }
  .form-group input[type="date"],
  .form-group input[type="time"],
  .form-group input[type="datetime-local"] {
    -webkit-appearance: none;
    appearance: none;
    max-width: 100%;
    min-width: 0;
  }
  .form-group textarea {
    height: auto;
  }
  .form-row .form-group {
    min-width: 0;
  }
  .payroll-generate-fields {
    grid-template-columns: 1fr;
  }
  .payroll-generate-actions .btn {
    width: 100%;
  }
  .filter-bar .form-group:has(input[type="date"]) {
    min-width: 0;
  }
  .filter-bar .form-group:has(#filter-employee-ms),
  .filter-bar .form-group:has(#filter-flags-ms),
  .filter-bar .form-group:has(#adj-employee-filter) {
    min-width: 0;
  }

  /* Phase 3: Section toolbars */
  .section-toolbar {
    flex-direction: column;
    align-items: stretch;
    gap: 0.75rem;
  }
  .section-toolbar .btn-group {
    flex-wrap: wrap;
  }
  .toast-container {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
    width: 90%;
    max-width: 360px;
    align-items: center;
  }
  [dir="rtl"] .toast-container {
    left: 50%;
    right: auto;
    transform: translateX(-50%);
  }
  .search-box {
    width: 100%;
  }
  .search-box input {
    width: 100%;
  }
  /* Form rows & button groups stack on mobile */
  .form-row {
    flex-direction: column;
    align-items: stretch !important;
  }
  .btn-group {
    flex-direction: column;
    width: 100%;
  }
  .btn-group .btn {
    width: 100%;
  }
  /* Attendance toolbar, stacked: the primary action leads. In a ROW the eye
     lands on the rightmost/last button and "Add Punch" already read as the
     endpoint; once `.btn-group` turns into a column that same DOM order buries
     it under the Warnings toggle (and Fill Missing, when the feature is on), so
     the page's main verb sat third. `order` rather than a DOM move, because the
     row order above 768px is correct as authored. */
  #toolbar-raw #add-punch { order: -1; }

  /* Device config actions: stack sync picker above buttons on mobile */
  .device-config-actions {
    flex-direction: column-reverse;
    align-items: stretch;
  }
  .device-config-actions .device-config-buttons {
    flex-direction: column-reverse;
  }
  .device-config-actions .device-config-buttons .btn {
    width: 100%;
  }
  .device-config-actions .sync-interval-picker {
    align-self: center;
  }

  /* Phase 4: Tables */
  .table-responsive {
    position: relative;
    margin-left: -1rem;
    margin-right: -1rem;
    border-radius: 0;
    border-left: none;
    border-right: none;
  }
  th, td {
    padding: 0.65rem 0.75rem;
  }
  td {
    white-space: normal;
    word-break: break-word;
  }
  th {
    white-space: nowrap;
  }
  /* Atomic-value cells (dates, times, H:MM, day counts — renderers emit .num)
     must never fragment across lines: "13:46" split as "13:4"/"6" misreads. */
  td.num {
    white-space: nowrap;
    word-break: normal;
  }

  /* Phase 5: Dashboard */
  .stat-card {
    padding: 1rem 1.1rem;
  }
  .stat-card .value {
    font-size: 1.5rem;
  }
  .card {
    padding: 1.15rem;
  }
  .sync-controls .section-toolbar {
    flex-direction: column;
    align-items: stretch;
  }
  .sync-controls .btn-group {
    flex-direction: column;
  }
  .sync-controls .btn-group .btn {
    width: 100%;
  }
  .dashboard-grid {
    grid-template-columns: 1fr;
  }

  /* Phase 6: Modals */
  .modal {
    width: 92%;
    max-width: 420px;
    max-height: 85vh;
    border-radius: var(--radius-lg);
  }
  .modal-body {
    overflow-y: auto;
    padding: 1.25rem;
  }
  .modal-header {
    padding: 1rem 1.25rem;
  }

  /* Phase 7: Login */
  .login-card {
    padding: 1.75rem 1.25rem 1.5rem;
  }
  .login-card .form-group input {
    font-size: 16px;
    padding: 0.7rem 0.85rem;
  }

  /* Phase 8: Searchable selects */
  .ss-trigger,
  .ms-trigger {
    min-height: 44px;
    font-size: 16px;
  }
  .ss-dropdown {
    max-height: 50vh;
  }
  .ss-list {
    max-height: 45vh;
  }
  .ss-option,
  .ms-option {
    padding: 0.65rem 0.85rem;
    min-height: 44px;
    display: flex;
    align-items: center;
  }

  /* Filter bar tighter on mobile */
  .filter-bar {
    padding: 1rem;
    gap: 0.75rem;
  }
  .filter-bar-header {
    top: 0;
  }

  /* Pagination mobile */
  .pager-btn {
    min-width: 2.2rem;
    min-height: 2.2rem;
  }

  /* Allowed punch-type pills: scrollable on mobile */
  #allowed-punch-types-list {
    max-width: 100%;
    overflow-x: auto;
    overflow-y: hidden;
  }

  /* Payroll meta bar: 2-column grid on mobile */
  .payroll-meta-bar {
    display: grid;
    grid-template-columns: 1fr 1fr;
  }
  .payroll-meta-divider {
    display: none;
  }
  .payroll-meta-item {
    min-width: 0;
    border-bottom: 1px solid var(--gray-200);
  }
  /* children 3 and 7 are the right-column items — add column divider */
  .payroll-meta-bar .payroll-meta-item:nth-child(3),
  .payroll-meta-bar .payroll-meta-item:nth-child(7) {
    border-inline-start: 1px solid var(--gray-200);
  }
  /* children 5 and 7 are the bottom-row items — remove bottom border */
  .payroll-meta-bar .payroll-meta-item:nth-child(5),
  .payroll-meta-bar .payroll-meta-item:nth-child(7) {
    border-bottom: none;
  }
  .payroll-meta-label {
    white-space: normal;
  }
  .payroll-meta-value {
    white-space: normal;
    overflow-wrap: break-word;
  }

  /* Tabs: horizontally scrollable on mobile */
  .view-tabs {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    flex-wrap: nowrap;
    /* `overflow-x: auto` makes overflow-y compute to auto too, and a scroll port
       clips at its PADDING box, outside of which the container's own
       border-block-end is painted. So the desktop trick (the tab overlapping the
       track via margin-block-end: -1px) cannot work here: the indicator's last
       pixel is clipped away and the track resurfaces as a second line under the
       active tab. Draw the track as an inset shadow instead: it sits inside the
       padding box, stays pinned while the strip scrolls exactly as the border
       did, and the tabs paint over it. */
    border-block-end-color: transparent;
    box-shadow: inset 0 -1px 0 var(--gray-300);
  }
  .view-tabs::-webkit-scrollbar {
    display: none;
  }
  .view-tab {
    white-space: nowrap;
    flex-shrink: 0;
    padding: 0.6rem 0.9rem;
    font-size: 0.85rem;
    /* Nothing to pull over any more, and a negative margin here is exactly what
       the scroll port would clip. */
    margin-block-end: 0;
  }
}

@media (max-width: 1024px) {
  .payroll-generate-fields {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* ─── Day Picker Pills ─── */
.day-picker {
  display: flex;
  border: 1.5px solid var(--gray-300);
  border-radius: 0.5rem;
  overflow: hidden;
  background: var(--gray-300);
  gap: 1px;
}

.day-picker .shift-weekend-cb,
.day-picker .weekend-day-cb,
.day-picker .apt-cb,
.day-picker .rule-wd-cb,
.day-picker .sw-day-cb {
  display: none !important;
}

.day-picker .day-pill {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1 1 0%;
  min-width: 0;
  padding: 0.07rem 0;
  margin: 0;
  border: none;
  border-radius: 0;
  background: var(--surface);
  color: var(--text-secondary, var(--gray-600));
  font-size: 0.82rem;
  font-weight: 500;
  cursor: pointer;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;   /* E1: chrome, not data — no long-press callout */
  text-align: center;
  transition: background 0.15s ease, color 0.15s ease;
}

.day-pill:hover {
  color: var(--primary);
  background: color-mix(in srgb, var(--primary) 8%, var(--surface));
}

.shift-weekend-cb:checked + .day-pill,
.weekend-day-cb:checked + .day-pill,
.apt-cb:checked + .day-pill,
.rule-wd-cb:checked + .day-pill,
.sw-day-cb:checked + .day-pill {
  background: var(--primary);
  color: #fff;
  box-shadow: none;
}

.shift-weekend-cb:disabled + .day-pill,
.weekend-day-cb:disabled + .day-pill,
.apt-cb:disabled + .day-pill,
.rule-wd-cb:disabled + .day-pill,
.sw-day-cb:disabled + .day-pill {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}

/* Allowed punch-type picker — fixed-size pills, not stretched */
.apt-cb + .day-pill {
  flex: 0 0 auto;
  padding: 0.4rem 1.1rem;
  font-size: 0.82rem;
}

/* ─── Sync windows (settings → Device Configuration) ─── */
.sync-windows-block {
  margin-top: 1rem;
}

.sync-windows-block .sync-windows-help {
  display: block;
  margin-bottom: 0.6rem;
  color: var(--gray-600);
  font-size: 0.8rem;
}

/* Sync windows only apply while interval sync is on, so the block is HIDDEN
   (global .hidden utility, toggled by _applySyncWindowsDisabled) when the
   picker reads "off" rather than greyed out. It sits directly above the
   Save / Test Connection row. Hiding never clears the stored windows. */

.sync-window-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}

.sync-window-row input[type="time"] {
  width: auto;
  min-width: 7.5rem;
  flex: 0 0 auto;
}

/* An en dash between the two times. Not directional, so it needs no RTL flip. */
.sync-window-row .sw-dash {
  color: var(--gray-500);
  flex: 0 0 auto;
}

/* The pills take the remaining width. min-width keeps seven pills legible in
   Arabic, whose weekday abbreviations are wider than the English ones. */
.sync-window-row .day-picker {
  flex: 1 1 16rem;
  min-width: 16rem;
}

.sync-window-row .sw-remove {
  flex: 0 0 auto;
  color: var(--danger);
}

@media (max-width: 640px) {
  .sync-window-row .day-picker {
    flex-basis: 100%;
  }
}

#auto-sync-status {
  display: block;
  margin-top: 0.5rem;
  color: var(--gray-600);
  font-size: 0.82rem;
}

/* Paused is the state HR came to this screen to diagnose — give it weight
   without dressing an intentional configuration up as an error. */
#auto-sync-status.is-paused {
  color: var(--warning, var(--gray-700));
  font-weight: 500;
}

/* ─── Toggle Switch ─── */
.toggle-switch {
  position: relative;
  display: inline-block;
  width: 44px;
  height: 24px;
  flex-shrink: 0;
}

.toggle-switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.toggle-slider {
  position: absolute;
  cursor: pointer;
  top: 0; left: 0; right: 0; bottom: 0;
  background: var(--gray-300);
  border-radius: 24px;
  transition: background 0.25s ease;
}

.toggle-slider::before {
  content: '';
  position: absolute;
  height: 18px;
  width: 18px;
  left: 3px;
  bottom: 3px;
  background: var(--surface);
  border-radius: 50%;
  transition: transform 0.25s ease;
  box-shadow: var(--shadow-sm);
}

.toggle-switch input:checked + .toggle-slider {
  background: var(--primary);
}

.toggle-switch input:checked + .toggle-slider::before {
  transform: translateX(20px);
}

/* RTL: a switch conveys its state by knob position along the reading axis, so
   off must sit at the inline-start (right) edge and travel leftwards. The base
   rules above are physical (`left` + translateX) and don't mirror on their own;
   CSS transforms are always physical, hence the explicit negative override.
   Scoped to the employee retroactive-shift toggle for now — the other switches
   sharing .toggle-slider still use the LTR geometry in Arabic. */
[dir="rtl"] #emp-shift-retro-toggle + .toggle-slider::before {
  left: auto;
  right: 3px;
}

[dir="rtl"] #emp-shift-retro-toggle:checked + .toggle-slider::before {
  transform: translateX(-20px);
}

/* ─── Auto-sync status indicator ─── */
.auto-sync-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.2rem 0.65rem;
  border-radius: 999px;
  font-size: var(--fs-2xs);
  font-weight: 600;
}

.auto-sync-badge.active {
  background: var(--success-light);
  color: var(--success-text);
}

.auto-sync-badge.inactive {
  background: var(--gray-100);
  color: var(--gray-500);
}

.auto-sync-badge .pulse-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.auto-sync-badge.active .pulse-dot {
  animation: pulse-dot 2s infinite;
}

@keyframes pulse-dot {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}

/* ─── Payroll: Slip detail modal ─── */
.slip-stats-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
}
.slip-stat {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  background: var(--surface-alt);
  border-radius: var(--radius);
  padding: 0.3rem 0.6rem;
  font-size: 0.82rem;
}
.slip-stat-val {
  font-weight: 700;
  font-size: 0.9rem;
}
.slip-stat-label {
  color: var(--text-secondary);
  font-size: 0.75rem;
}
.slip-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}
.slip-employee-name {
  font-size: 1.1rem;
  font-weight: 700;
}
.slip-period-row {
  color: var(--gray-500);
  font-size: 0.85rem;
  margin-bottom: 0.5rem;
}
.slip-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 0.5rem;
}
.slip-grid-item {
  display: flex;
  flex-direction: column;
  background: var(--surface-alt);
  border-radius: var(--radius);
  padding: 0.5rem 0.65rem;
}
.slip-label {
  font-size: var(--fs-2xs);
  color: var(--text-secondary);
  margin-bottom: 0.15rem;
}
.slip-value {
  font-size: 1rem;
  font-weight: 600;
}
.slip-breakdown-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}
.slip-breakdown-table td {
  padding: 0.35rem 0.5rem;
  border-bottom: 1px solid var(--gray-100);
}
.slip-breakdown-table td:last-child {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
[dir="rtl"] .slip-breakdown-table td:last-child {
  text-align: left;
}
/* Sub-group caption inside the single slip breakdown table (Hours only -
   the day rows open the section unlabeled).
   Must come AFTER the td:last-child rules above: the colspan cell is also the
   last child, and these selectors tie on specificity, so source order decides
   that the caption stays start-aligned in LTR and RTL. Its own background is
   deliberate - the print/PDF/Word surfaces stripe rows by index, and an
   inherited stripe would tint the caption. */
.slip-breakdown-table .slip-subgroup-row td {
  padding: 0.5rem 0.5rem 0.15rem;
  border-top: 1px solid var(--gray-200);
  border-bottom: none;
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: start;
  white-space: normal;
}
.slip-breakdown-table .slip-subgroup-row:first-child td {
  padding-top: 0.15rem;
  border-top: none;
}
/* The colspan caption cell is ALSO :last-child, and the [dir="rtl"] rule above
   out-specifies the block - re-assert start alignment for it. */
[dir="rtl"] .slip-breakdown-table tr.slip-subgroup-row td {
  text-align: start;
}
.slip-offset-note {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  margin-inline-start: 0.35rem;
  border-radius: 999px;
  background: var(--warning-light);
  color: var(--warning);
  font-size: var(--fs-2xs);
  font-weight: 700;
  line-height: 1;
  cursor: help;
  vertical-align: middle;
}
.slip-offset-note:focus-visible {
  outline: 2px solid var(--warning);
  outline-offset: 2px;
}
/* Qualifies a figure in the table above it (e.g. worked hours that include
   unpaid weekend/holiday time). Deliberately NOT print-hidden — a printed slip
   needs the explanation more than the screen does, where a tooltip could serve. */
.slip-footnote {
  margin: 0.35rem 0 0;
  font-size: var(--fs-2xs);
  line-height: 1.4;
  color: var(--gray-600);
  text-align: start;
}
.slip-total-row td {
  border-top: 2px solid var(--gray-200);
  border-bottom: none;
}
.slip-net-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 1rem;
  padding: 0.75rem 1rem;
  background: var(--primary-50);
  border-radius: var(--radius);
  font-weight: 600;
  font-size: 1.05rem;
}
.slip-net-value {
  color: var(--primary);
  font-size: 1.15rem;
}
/* Standing debt balance - the running total the employee owes the company,
   frozen onto the slip at generation and printed immediately above the
   postponed panel. Amber rather than the postponed panel's red: the two sit
   next to each other and must read as two facts, and this is a standing
   balance, not a deduction from this slip. */
.slip-debt {
  margin-top: 0.75rem;
  border: 1px solid var(--warning);
  border-inline-start-width: 4px;
  border-radius: var(--radius);
  background: var(--warning-light);
  overflow: hidden;
}
.slip-debt-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem;
  padding: 0.7rem 1rem;
  color: var(--warning-text);
  font-size: 0.95rem;
  font-weight: 600;
}
.slip-debt-total {
  color: var(--warning-text);
  font-size: 1.1rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Postponed advances — the balance the employee still owes, so it carries the
   same weight as the Net Payable row above it (filled tint, large tabular
   amount) in the deductions colour rather than the primary one. Small uppercase
   micro-type on a near-white strip made a five-figure balance read as a
   footnote. */
.slip-postponed {
  margin-top: 0.75rem;
  border: 1px solid var(--danger);
  border-inline-start-width: 4px;
  border-radius: var(--radius);
  background: var(--danger-light);
  overflow: hidden;
}
.slip-postponed-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem;
  padding: 0.7rem 1rem;
  color: var(--danger-text);
  font-size: 0.95rem;
  font-weight: 600;
}
.slip-postponed-total {
  color: var(--danger);
  font-size: 1.1rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.slip-postponed-note {
  padding: 0 1rem 0.6rem;
  color: var(--danger-text);
  font-size: 0.8rem;
}
/* Salary Slip Layout settings card — grouped toggle sections */
.slip-settings-group {
  margin-bottom: 1.5rem;
}
.slip-settings-group:last-child {
  margin-bottom: 0;
}
.slip-settings-group-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--gray-700);
  margin-bottom: 0.5rem;
}
.slip-settings-group-header svg {
  color: var(--primary);
  flex-shrink: 0;
}
.slip-settings-group-hint {
  font-size: 0.8rem;
  color: var(--gray-500);
  margin: 0 0 0.6rem;
}
.slip-toggle-list {
  border: 1px solid var(--gray-200);
  border-radius: var(--radius);
  background: var(--gray-50);
  overflow: hidden;
}
.slip-toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin: 0;
  padding: 0.7rem 0.9rem;
  font-size: 0.875rem;
  color: var(--gray-700);
  cursor: pointer;
  transition: background 0.15s ease;
}
.slip-toggle-row:not(:last-child) {
  border-bottom: 1px solid var(--gray-200);
}
.slip-toggle-row:hover {
  background: var(--gray-100);
}
.slip-toggle-row .toggle-switch {
  flex-shrink: 0;
}
.slip-settings-status {
  display: block;
  font-size: 0.85rem;
  margin-top: 0.5rem;
}

.slip-signatures {
  display: none;
}
.print-only {
  display: none;
}
.slip-sig-block {
  display: flex;
  flex-direction: column;
  align-items: center;
}
.slip-sig-label {
  font-size: 0.8rem;
  color: var(--gray-500);
}

/* ---------- Print Styles ---------- */
@media print {
  html, body { overflow: visible !important; width: 100% !important; margin: 0 !important; padding: 0 !important; }
  body { background: #fff; font-size: 8pt; line-height: 1.3; }
  .sidebar, .page-header, .toast-container,
  .btn-group, .form-row, .card > h3:first-child,
  .pager-controls, .table-pager,
  .main-content > .card,
  .report-accuracy-note,
  .ms-container, .filter-bar { display: none !important; }
  .stats-row { display: flex !important; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.35rem; }
  .stats-row:empty { display: none !important; }
  .stat-card { box-shadow: none; border: 1px solid #ccc; padding: 0.2rem 0.4rem; flex: 1 1 0; min-width: 80px; }
  .stat-card .stat-icon { display: none; }
  .stat-card .label { font-size: 7pt; margin-bottom: 0; }
  .stat-card .value { font-size: 11pt; margin: 0; line-height: 1.2; }
  .stat-card .stat-sub { font-size: 6.5pt; margin: 0; }
  .stat-card .stat-card-arrow { display: none; }
  .app { display: block; }
  .main-content { margin: 0 !important; padding: 0; width: 100%; }
  .card { box-shadow: none; border: none; padding: 0; margin: 0; }
  #report-output { display: block !important; width: 100%; }
  #report-output .card { margin-bottom: 0; }
  #report-title { display: block !important; font-size: 13pt; font-weight: 700; margin-bottom: 0.3rem; text-align: center; }
  .table-responsive { overflow: visible !important; width: 100%; }
  table, .table-responsive table { width: 100%; min-width: 0 !important; border-collapse: collapse; font-size: 7.5pt; table-layout: fixed !important; }
  thead { display: table-header-group; }
  tfoot { display: table-row-group; }
  th, td { border: 1px solid #bbb; padding: 2px 3px; width: auto !important; word-break: break-word; overflow-wrap: break-word; white-space: normal; }
  th { background: #f0f0f0 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; text-align: center; font-size: 7pt; font-weight: 600; }
  td { text-align: center; }
  tr { page-break-inside: avoid; }
  [dir="rtl"] th, [dir="rtl"] td { text-align: right; }
  [dir="rtl"] .main-content { margin: 0 !important; }

  /* ── Payroll slip print mode ── */
  .printing-slip .app,
  .printing-slip .sidebar,
  .printing-slip .mobile-topbar,
  .printing-slip .main-content,
  .printing-slip .toast-container,
  .printing-slip .view-tabs,
  .printing-slip .section-toolbar,
  .printing-slip .no-print { display: none !important; }

  .printing-slip .modal-overlay.active {
    position: static !important;
    display: block !important;
    background: none !important;
    backdrop-filter: none !important;
  }
  .printing-slip .modal {
    box-shadow: none !important;
    max-width: 100% !important;
    width: 100% !important;
    max-height: none !important;
    overflow: visible !important;
    animation: none !important;
    border-radius: 0 !important;
    padding: 0 !important;
  }
  .printing-slip .modal-header { border-bottom: 2px solid #333; padding: 0.5rem 0; }
  .printing-slip .modal-header h3 { font-size: 14pt; }
  .printing-slip .modal-body { padding: 0.5rem 0; font-size: 9pt; }
  .printing-slip .slip-employee-name { font-size: 16pt; font-weight: 700; text-align: center; width: 100%; margin-bottom: 0.25rem; }
  .printing-slip .slip-header-row { justify-content: center; }
  .printing-slip .slip-period-row { text-align: center; font-size: 10pt; margin-bottom: 0.75rem; border-bottom: 1px solid #ddd; padding-bottom: 0.5rem; }
  .printing-slip .slip-grid { gap: 0.25rem; }
  .printing-slip .slip-grid-item { padding: 0.25rem 0.4rem; border: 1px solid #ddd; }
  .printing-slip .slip-offset-note { display: none !important; }
  .printing-slip .slip-breakdown-table { font-size: 8.5pt; }
  .printing-slip .slip-breakdown-table td { padding: 2px 4px; border-bottom: 1px solid #ddd; }
  /* Re-assert the sub-group caption over the print td rule above (same
     specificity, later source wins) and keep it with its first data row. */
  .printing-slip .slip-breakdown-table .slip-subgroup-row td {
    padding: 5px 4px 1px;
    border-top: 1px solid #ddd;
    border-bottom: none;
    background: transparent !important;
    color: #6b7280;
    font-size: 7.5pt;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    text-align: start;
  }
  .printing-slip .slip-breakdown-table .slip-subgroup-row:first-child td {
    padding-top: 1px;
    border-top: none;
  }
  .printing-slip .slip-subgroup-row { break-after: avoid; page-break-after: avoid; }
  .printing-slip .slip-net-row { background: #f0f0f0 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  /* Keep the tint on paper — without this the emphasis survives only on screen. */
  .printing-slip .slip-postponed,
  .printing-slip .slip-debt { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  .printing-slip .slip-signatures,
  .printing-slip .slip-print-bottom,
  .printing-slip .slip-signoff-block,
  .printing-slip .slip-net-row,
  .printing-slip .slip-carryover-row,
  .printing-slip .slip-carryover-hint,
  .printing-slip .slip-carryover-moved,
  .printing-slip .slip-debt,
  .printing-slip .slip-postponed,
  .printing-slip .slip-print-notes {
    break-inside: avoid;
    page-break-inside: avoid;
  }
  .printing-slip .slip-breakdown-table tr { break-inside: avoid; page-break-inside: avoid; }
  .printing-slip .slip-detail-compact { font-size: 9.25pt; line-height: 1.38; }
  .printing-slip .slip-detail-compact .slip-print-header { padding-bottom: 0.6rem; margin-bottom: 0.75rem; }
  .printing-slip .slip-detail-compact .slip-brand-logo { width: 60px !important; height: 60px !important; }
  .printing-slip .slip-detail-compact .slip-title-main { font-size: 15pt; }
  .printing-slip .slip-detail-compact .slip-brand-name { font-size: 13.5pt; }
  .printing-slip .slip-detail-compact .slip-print-employee { padding: 0.55rem 0.85rem; margin-bottom: 0.7rem; }
  .printing-slip .slip-detail-compact .slip-emp-name { font-size: 12.5pt; }
  .printing-slip .slip-detail-compact .slip-section-title { font-size: 9.25pt; margin: 0.6rem 0 0.3rem !important; padding: 0.25rem 0.55rem; }
  .printing-slip .slip-detail-compact .slip-breakdown-table { font-size: 8.75pt; }
  .printing-slip .slip-detail-compact .slip-breakdown-table td { padding: 3px 7px; }
  .printing-slip .slip-detail-compact .slip-breakdown-table .slip-subgroup-row td {
    padding: 4px 7px 0;
    font-size: 7pt;
  }
  .printing-slip .slip-detail-compact .slip-breakdown-table .slip-subgroup-row:first-child td { padding-top: 0; }
  .printing-slip .slip-detail-compact .slip-total-row td { padding-top: 4px; padding-bottom: 4px; }
  .printing-slip .slip-detail-compact .badge { padding: 0.07rem 0.35rem; font-size: 0.66rem; }
  .printing-slip .slip-detail-compact .slip-net-row { margin-top: 0.75rem; padding: 0.7rem 0.95rem; font-size: 11pt; }
  .printing-slip .slip-detail-compact .slip-net-value { font-size: 13.5pt; }
  .printing-slip .slip-detail-compact .slip-carryover-row { margin-top: 0.55rem !important; padding: 0.5rem 0.75rem !important; }
  .printing-slip .slip-detail-compact .slip-debt { margin-top: 0.55rem !important; }
  .printing-slip .slip-detail-compact .slip-debt-head { padding: 0.5rem 0.8rem !important; }
  .printing-slip .slip-detail-compact .slip-debt-total { font-size: 12pt !important; }
  .printing-slip .slip-detail-compact .slip-postponed { margin-top: 0.55rem !important; }
  .printing-slip .slip-detail-compact .slip-postponed-head { padding: 0.5rem 0.8rem !important; }
  .printing-slip .slip-detail-compact .slip-postponed-note { padding: 0 0.8rem 0.45rem !important; font-size: 8pt !important; }
  .printing-slip .slip-detail-compact .slip-carryover-hint,
  .printing-slip .slip-detail-compact .slip-carryover-moved { margin-top: 0.2rem !important; font-size: 8pt !important; }
  .printing-slip .slip-detail-compact .slip-signatures { margin-top: 1rem; }
  .printing-slip .slip-detail-compact .slip-sig-space { height: 1.5rem; }
  .printing-slip .slip-detail-compact .slip-sig-label { font-size: 8.5pt; }
  .printing-slip .slip-detail-compact .slip-print-bottom { margin-top: 0.85rem; padding-top: 0.35rem; }
  .printing-slip .slip-signoff-block { display: block !important; break-inside: avoid; page-break-inside: avoid; }
  .printing-slip .slip-signatures { display: flex !important; flex-direction: row !important; justify-content: space-between; margin-top: 18px; padding: 0 1rem; }
  .printing-slip .slip-sig-block { display: flex; flex-direction: column; align-items: center; flex: 0 0 40%; }
  .printing-slip .slip-sig-space { height: 1.8rem; }
  .printing-slip .slip-sig-line { width: 100%; border-bottom: 1.5px solid #333; }
  .printing-slip .slip-sig-label { margin-top: 0.4rem; font-size: 9pt; font-weight: 600; color: #444; letter-spacing: 0.02em; }
  .printing-slip .slip-print-bottom { display: flex !important; justify-content: center; margin-top: 1rem; border-top: 1px solid #e5e7eb; padding-top: 0.35rem; }
  .printing-slip .print-only { display: block !important; }
  .printing-slip .slip-signatures.print-only { display: flex !important; }
  .printing-slip textarea { display: none !important; }
  .printing-slip .badge { border: 1px solid #999; }
  .printing-slip #slip-print-logo {
    position: static !important;
    opacity: 1 !important;
    width: 64px !important;
    height: 64px !important;
    overflow: visible !important;
    object-fit: contain;
    flex-shrink: 0;
    order: 3;
  }
  .printing-slip #slip-print-company-name {
    position: static !important;
    opacity: 1 !important;
    width: auto !important;
    height: auto !important;
    overflow: visible !important;
    display: block !important;
    flex: 1;
    text-align: center;
    font-size: 13pt;
    font-weight: 700;
    order: 2;
  }
  .printing-slip .modal-header > div:first-child {
    justify-content: flex-start !important;
    gap: 1rem;
  }
  .printing-slip .modal-header > div:first-child h3 { order: 1; }

  @page { size: A4 portrait; margin: 15mm; }
}

/* ─── RTL Support ─── */
/* No per-direction font-family override: one family covers both scripts (D1),
   which is what removes the EN/AR metric mismatch this rule used to create. */

[dir="rtl"] .sidebar {
  left: auto;
  right: 0;
}

/* The four `.sidebar nav a` border overrides that used to live here are gone:
   the base rules now use `border-inline-start`, which mirrors itself. */

[dir="rtl"] .sidebar-footer {
  flex-direction: row-reverse;
}

[dir="rtl"] .sidebar.collapsed .sidebar-footer {
  flex-direction: column;
}

[dir="rtl"] .sidebar-collapse-btn svg {
  transform: rotate(180deg);
}

[dir="rtl"] .sidebar.collapsed .sidebar-collapse-btn svg {
  transform: rotate(0deg);
}

[dir="rtl"] .main-content {
  margin-left: 0;
  margin-right: 222px;
}

[dir="rtl"] .app.sidebar-collapsed .main-content {
  margin-left: 0;
  margin-right: 72px;
}

[dir="rtl"] table th,
[dir="rtl"] table td {
  text-align: right;
}

[dir="rtl"] .filter-bar {
  direction: rtl;
}

[dir="rtl"] .form-row {
  direction: rtl;
}

[dir="rtl"] .payroll-generate-actions {
  direction: rtl;
}

[dir="rtl"] .btn-group {
  direction: rtl;
}

[dir="rtl"] .modal-actions {
  direction: rtl;
}

[dir="rtl"] .page-header {
  text-align: right;
}

[dir="rtl"] .stat-card {
  text-align: right;
}

[dir="rtl"] .section-toolbar {
  direction: rtl;
}

[dir="rtl"] .toast {
  direction: rtl;
}

@media (min-width: 641px) {
  [dir="rtl"] .toast-container {
    right: auto;
    left: 1.5rem;
  }
}

[dir="rtl"] .toast {
  animation: toastInRtl 0.3s ease;
}

[dir="rtl"] .toast.hiding {
  animation: toastOutRtl 0.3s ease forwards;
}

[dir="rtl"] .toast.toast-success,
[dir="rtl"] .toast.toast-error,
[dir="rtl"] .toast.toast-warning,
[dir="rtl"] .toast.toast-info {
  border-left: none;
  border-right-width: 4px;
  border-right-style: solid;
}
[dir="rtl"] .toast.toast-success { border-right-color: var(--success); }
[dir="rtl"] .toast.toast-error   { border-right-color: var(--danger); }
[dir="rtl"] .toast.toast-warning { border-right-color: var(--warning); }
[dir="rtl"] .toast.toast-info    { border-right-color: var(--primary); }

@keyframes toastInRtl {
  from { opacity: 0; transform: translateX(-30px); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes toastOutRtl {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(-30px); }
}

@media (max-width: 768px) {
  [dir="rtl"] .main-content,
  [dir="rtl"] .app.sidebar-collapsed .main-content {
    margin-right: 0;
  }
  [dir="rtl"] .sidebar {
    left: auto;
    right: 0;
    transform: translateX(100%);
  }
  [dir="rtl"] .sidebar.mobile-open {
    transform: translateX(0);
  }
}

/* ── Searchable Select ────────────────────────────────── */
.ss-container {
  position: relative;
  width: 100%;
}

.ss-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 0.55rem 0.85rem;
  border: 1px solid var(--gray-300);
  border-radius: 8px;
  font-size: 0.875rem;
  font-family: inherit;
  color: var(--gray-900);
  background: var(--surface);
  cursor: pointer;
  transition: all var(--transition);
  min-height: 2.25rem;
  box-sizing: border-box;
  gap: 0.5rem;
}

.ss-trigger:hover {
  border-color: var(--gray-400);
}

.ss-trigger:focus,
.ss-container.ss-open .ss-trigger {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-50);
}

.ss-display {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ss-display.ss-placeholder {
  color: var(--gray-500);
}

.ss-arrow {
  flex-shrink: 0;
  color: var(--gray-400);
  display: flex;
  align-items: center;
  transition: transform 0.2s ease;
}

.ss-container.ss-open .ss-arrow {
  transform: rotate(180deg);
}

.ss-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  min-width: 220px;
  width: max-content;
  max-width: 320px;
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0,0,0,0.13), 0 2px 8px rgba(0,0,0,0.07);
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  /* Delay the visibility flip on close so the fade-out stays visible; the
     open state flips it instantly so the search input is focusable the same
     tick the dropdown opens. */
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0s linear 0.15s;
}

.ss-container.ss-open .ss-dropdown {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: opacity 0.15s ease, transform 0.15s ease;
}

.ss-container.ss-above .ss-dropdown {
  top: auto;
  bottom: calc(100% + 6px);
  transform: translateY(6px);
}

.ss-container.ss-above.ss-open .ss-dropdown {
  transform: translateY(0);
}

.ss-search-wrap {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.85rem;
  border-bottom: 1px solid var(--gray-100);
  background: var(--gray-50);
  border-radius: 12px 12px 0 0;
}

.ss-search-icon {
  color: var(--gray-400);
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

.ss-input {
  width: 100%;
  border: none;
  outline: none;
  font-size: 0.85rem;
  font-family: inherit;
  color: var(--gray-900);
  background: transparent;
}

/* The generic `.form-group input` rule applies `transition: all`, which also
   transitions this input's inherited visibility when the dropdown opens -
   leaving it unfocusable in the same tick, so the open-time autofocus
   silently fails. The input is borderless/transparent, so no transition. */
.ss-search-wrap input.ss-input {
  transition: none;
}

.ss-input::placeholder {
  color: var(--gray-400);
}

.ss-list {
  list-style: none;
  margin: 0;
  padding: 0.35rem 0;
  max-height: 220px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.ss-option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.55rem 1rem;
  font-size: 0.875rem;
  cursor: pointer;
  color: var(--gray-700);
  transition: background 0.1s, color 0.1s;
  gap: 0.5rem;
}

.ss-option:hover,
.ss-option.ss-active {
  background: var(--primary-50, #eef2ff);
  color: var(--primary);
}

.ss-option.ss-selected {
  font-weight: 600;
  color: var(--primary);
}

.ss-option.ss-selected::after {
  content: '';
  display: inline-block;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  background-color: var(--primary);
  mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
  mask-repeat: no-repeat;
  mask-size: contain;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-size: contain;
}

.ss-no-results {
  padding: 0.85rem 1rem;
  font-size: 0.83rem;
  color: var(--text-muted);
  text-align: center;
}

/* RTL support – flex-direction:row already flows R→L under dir="rtl" */

/* Scrollbar styling for the dropdown list */
.ss-list::-webkit-scrollbar {
  width: 6px;
}
.ss-list::-webkit-scrollbar-track {
  background: transparent;
}
.ss-list::-webkit-scrollbar-thumb {
  background: var(--gray-300);
  border-radius: 3px;
}
.ss-list::-webkit-scrollbar-thumb:hover {
  background: var(--gray-400);
}

/* ── Multi-Select Dropdown ───────────────────────────── */
.ms-container {
  position: relative;
  width: 100%;
}

.ms-trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 0.55rem 0.85rem;
  border: 1px solid var(--gray-300);
  border-radius: 8px;
  font-size: 0.875rem;
  font-family: inherit;
  color: var(--gray-900);
  background: var(--surface);
  cursor: pointer;
  transition: all var(--transition);
  box-sizing: border-box;
  height: 2.5rem;
  gap: 0.5rem;
}

.ms-trigger:hover {
  border-color: var(--gray-400);
}

.ms-trigger:focus,
.ms-container.ms-open .ms-trigger {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-50);
}

.ms-display {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ms-container.ms-open .ss-arrow {
  transform: rotate(180deg);
}

.ms-dropdown {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  /* The panel is pinned to both edges of its TRIGGER, so it inherited whatever
     width the trigger was squeezed to. In the dashboard's review card three
     filters share one row, which took the employee trigger down to 166px, and
     ordinary names then wrapped onto two and three lines - the list read as a
     column of fragments rather than a list of people.
     A floor fixes it without repositioning anything: with `left`/`right` both
     set, an over-constrained absolute box drops the inline-END offset, so the
     panel grows away from its inline-start edge - leftward in RTL, rightward in
     LTR - and stays anchored under the trigger in both directions.
     The viewport cap below does NOT make that growth safe, and an earlier
     version of this comment claimed it did: `max-width` caps the panel's WIDTH,
     never its POSITION, so a trigger already close to the page's inline-end
     edge still overhangs the PAGE - and it does so while the panel is CLOSED,
     because `visibility: hidden` keeps a box in layout. That is what made the
     phone dashboard scroll sideways (33px at 360px wide). The only fix is to
     anchor the panel to something wider than the squeezed trigger; see the
     `#warnings-card .dash-review-filters` override in the max-width:768px
     block, and copy that shape for any new row that squeezes several triggers
     together. */
  min-width: 260px;
  max-width: calc(100vw - 1.5rem);
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.12), 0 2px 6px rgba(0,0,0,0.06);
  z-index: 1000;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-4px);
  /* Delay the visibility flip on close so the fade-out stays visible; the
     open state flips it instantly so the search input is focusable the same
     tick the dropdown opens. */
  transition: opacity 0.15s ease, transform 0.15s ease, visibility 0s linear 0.15s;
}

.ms-container.ms-open .ms-dropdown {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
  transition: opacity 0.15s ease, transform 0.15s ease;
}

.ms-list {
  list-style: none;
  margin: 0;
  padding: 0.25rem 0;
  max-height: 220px;
  overflow-y: auto;
}

/* Done button footer. .ms-list above owns the only scroll box in the panel
   (max-height + overflow-y), so this sibling is pinned at the panel bottom by
   construction - no position: sticky, which this codebase has been burned by
   in table headers. justify-content: flex-end resolves against the flex
   container's INLINE axis, so it lands on the right in LTR and the left in
   RTL with no [dir="rtl"] override; the logical border and corner radii
   follow the same rule. */
.ms-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  padding: 0.4rem 0.6rem;
  border-block-start: 1px solid var(--gray-200);
  background: var(--surface);
  border-end-start-radius: 10px;
  border-end-end-radius: 10px;
}

/* The panel's close X is borderless: .btn-icon's boxed edge sat directly under
 * the .ms-actions divider and read as a second frame inside the panel. Only the
 * chrome is dropped - .btn-icon still supplies the 32x32 box, so the button
 * stays an identical size in every language.
 *
 * Hover uses --gray-200 rather than .btn-icon's --gray-50: under
 * [data-theme="dark"] both --gray-50 and --gray-100 resolve to #1e293b, the
 * same value as --surface, so either would be invisible exactly where the
 * border is no longer there to carry the affordance.
 *
 * No `outline: none` anywhere here, deliberately. .btn-icon sets no
 * :focus-visible rule, so the UA outline IS this button's keyboard focus
 * indicator; with the border gone it is the only one left. (.btn-ghost and
 * .modal-close-btn both suppress it - do not copy that here.)
 */
.ms-done {
  border-color: transparent;
  background: transparent;
  box-shadow: none;
}
.ms-done:hover {
  border-color: transparent;
  background: var(--gray-200);
  color: var(--gray-900);
  transform: none;
  box-shadow: none;
}

.ms-option {
  padding: 0;
}

.ms-option label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.45rem 0.85rem;
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--gray-700);
  transition: background 0.1s;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;   /* E1: chrome, not data — no long-press callout */
}

.ms-option label:hover {
  background: var(--primary-50);
}

.ms-option input[type="checkbox"] {
  width: 15px;
  height: 15px;
  accent-color: var(--primary);
  cursor: pointer;
  flex-shrink: 0;
}

[dir="rtl"] .ms-option label {
  text-align: right;
}

/* ─── Login Page ─── */
.login-page {
  background: linear-gradient(135deg, var(--sidebar-bg) 0%, #1e293b 50%, var(--primary-dark) 100%);
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

.login-container {
  width: 100%;
  max-width: 420px;
}

.login-card {
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg), 0 0 40px rgba(79, 70, 229, 0.1);
  padding: 2.5rem 2rem 2rem;
}

.login-brand {
  text-align: center;
  margin-bottom: 2rem;
}

.brand-icon-lg {
  width: 56px;
  height: 56px;
  background: var(--primary);
  border-radius: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 0.75rem;
  color: #fff;
}

.login-brand h1 {
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--gray-900);
  margin-bottom: 0.15rem;
}

.login-subtitle {
  font-size: 0.85rem;
  color: var(--gray-500);
}

.login-card .form-group {
  margin-bottom: 1.25rem;
}

.login-card .form-group label {
  display: block;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--gray-700);
  margin-bottom: 0.35rem;
}

.login-card .form-group input {
  width: 100%;
  padding: 0.65rem 0.85rem;
  border: 1.5px solid var(--gray-200);
  border-radius: var(--radius);
  font-size: 0.95rem;
  color: var(--gray-900);
  background: var(--gray-50);
  transition: border-color var(--transition), box-shadow var(--transition);
}

.login-card .form-group input:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-50);
  background: var(--surface);
}

.password-input-wrapper {
  position: relative;
  display: flex;
  align-items: center;
}

.password-input-wrapper input {
  width: 100%;
  padding-right: 2.75rem;
}

[dir="rtl"] .password-input-wrapper input {
  padding-right: 0.85rem;
  padding-left: 2.75rem;
}

.password-toggle-btn {
  position: absolute;
  right: 0.5rem;
  background: none;
  border: none;
  cursor: pointer;
  padding: 0.5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--gray-500);
  transition: color var(--transition);
  border-radius: var(--radius);
  font-size: 0;
}

[dir="rtl"] .password-toggle-btn {
  right: auto;
  left: 0.5rem;
}

.password-toggle-btn:hover {
  color: var(--gray-700);
  background: var(--gray-100);
}

.password-toggle-btn:focus {
  outline: none;
  box-shadow: 0 0 0 2px var(--primary-50);
}

.password-toggle-btn svg {
  width: 20px;
  height: 20px;
}

.password-toggle-btn .eye-icon,
.password-toggle-btn .eye-off-icon {
  transition: opacity var(--transition);
}

.password-toggle-btn .hidden {
  display: none;
}

.btn-full {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.7rem;
  font-size: 0.95rem;
  font-weight: 600;
  margin-top: 0.5rem;
}

.spinner {
  width: 16px;
  height: 16px;
  border: 2px solid rgba(255,255,255,0.3);
  border-top-color: #fff;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* ─── Form inline error / warning banners (modals, settings tab forms) ─── */
.form-msg {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.65rem 0.9rem;
  border-radius: var(--radius);
  font-size: 0.85rem;
  line-height: 1.45;
  margin-bottom: 0.75rem;
}
/* `display: flex` above beats the UA stylesheet's `[hidden] { display: none }`,
   so a .form-msg carrying the hidden ATTRIBUTE renders as an empty red box.
   Every older caller sidesteps it with a `.hidden` class (which is !important);
   this makes the attribute work too, because the attribute is what a new caller
   reaches for and the failure is silent - the banner is empty, so there is
   nothing to read that says it should not be there. Found by rendering the
   connector wizard's confirm screen, not by reading the CSS. */
.form-msg[hidden] { display: none; }
.form-msg::before {
  content: '';
  display: block;
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  margin-top: 0.05rem;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}
.form-msg-error {
  background: var(--danger-light);
  color: var(--danger);
  border: 1px solid rgba(220, 38, 38, 0.2);
}
.form-msg-error::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23dc2626' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cline x1='12' y1='8' x2='12' y2='12'/%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'/%3E%3C/svg%3E");
}
.form-msg-warning {
  background: var(--warning-light);
  color: var(--warning);
  border: 1px solid rgba(217, 119, 6, 0.2);
}
.form-msg-warning::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23d97706' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z'/%3E%3Cline x1='12' y1='9' x2='12' y2='13'/%3E%3Cline x1='12' y1='17' x2='12.01' y2='17'/%3E%3C/svg%3E");
}

.login-error {
  margin-top: 1rem;
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  background: var(--danger-light);
  color: var(--danger);
  font-size: 0.85rem;
  text-align: center;
  border: 1px solid rgba(220, 38, 38, 0.2);
}

.login-warning {
  margin-top: 1rem;
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  background: var(--warning-light);
  color: var(--warning);
  font-size: 0.85rem;
  text-align: center;
  border: 1px solid rgba(217, 119, 6, 0.2);
}

.login-success {
  margin-top: 1rem;
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  background: rgba(16, 185, 129, 0.1);
  color: #059669;
  font-size: 0.9rem;
  text-align: center;
  border: 1px solid rgba(16, 185, 129, 0.25);
}

.login-secondary-action {
  margin-top: 0.85rem;
  text-align: center;
  font-size: 0.85rem;
}
.login-secondary-action a {
  color: var(--primary, #2563eb);
  text-decoration: none;
  cursor: pointer;
}
.login-secondary-action a:hover {
  text-decoration: underline;
}

/* Self-service password-change rule indicators (login page). */
.pwd-rules {
  list-style: none;
  padding: 0;
  margin: 0.4rem 0 0;
  font-size: 0.78rem;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.pwd-rules li {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--gray-500, #6b7280);
  transition: color 0.15s ease;
}
.pwd-rules li.met { color: #059669; }
.pwd-rules li .rule-icon {
  display: inline-flex;
  width: 1rem;
  justify-content: center;
  font-weight: 700;
}

.pwd-match-feedback {
  margin-top: 0.35rem;
  font-size: 0.78rem;
}
.pwd-match-feedback.match { color: #059669; }
.pwd-match-feedback.mismatch { color: var(--danger, #dc2626); }

.login-footer {
  margin-top: 1.5rem;
  text-align: center;
}

.lang-toggle-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.85rem;
  background: var(--gray-50);
  border: 1px solid var(--gray-200);
  border-radius: 20px;
  color: var(--gray-600);
  font-size: 0.8rem;
  cursor: pointer;
  transition: all var(--transition);
}

.lang-toggle-btn:hover {
  background: var(--gray-100);
  border-color: var(--gray-300);
}

/* ─── Subscription Banner ─── */
.subscription-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  padding: 0.6rem 1rem;
  font-size: 0.85rem;
  font-weight: 500;
  text-align: center;
  flex-wrap: wrap;
  z-index: 100;
}

.subscription-banner-danger {
  background: var(--danger-light);
  color: var(--danger);
  border-bottom: 1px solid rgba(220, 38, 38, 0.2);
}

.subscription-banner-warning {
  background: var(--warning-light);
  color: var(--warning);
  border-bottom: 1px solid rgba(217, 119, 6, 0.2);
}

.banner-contact {
  font-weight: 400;
  opacity: 0.85;
}

/* ─── Subscription expiry bar (expiring_soon — gentle amber) ─── */
.subscription-bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  /* The bar lives in <body> above .app; the sidebar is position:fixed on the
     inline-start side (left in LTR, right in RTL). Clear it so the bar fills the
     content area instead of sliding under the sidebar. Auto width fills the rest. */
  margin: 0;
  margin-inline-start: 222px;
  padding: 0.7rem 1.25rem;
  background: var(--warning-light);
  border-inline-start: 3px solid var(--warning);
  font-size: 0.875rem;
  color: var(--gray-800);
  z-index: 100;
}

/* Collapsed sidebar (desktop) — narrower offset. */
body:has(.app.sidebar-collapsed) .subscription-bar {
  margin-inline-start: 72px;
}

.subscription-bar__icon {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--warning-bg);
  color: var(--warning);
}

.subscription-bar__icon svg {
  width: 18px;
  height: 18px;
}

.subscription-bar__msg {
  flex: 1;
  min-width: 0;
  line-height: 1.4;
}

.subscription-bar__count {
  font-weight: 500;
}

.subscription-bar__actions {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
}

.subscription-bar__details {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.35rem 0.7rem;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--surface) 55%, transparent);
  color: var(--primary);
  font-size: 0.83rem;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
}

.subscription-bar__details:hover {
  background: color-mix(in srgb, var(--surface) 80%, transparent);
}

.subscription-bar__details svg {
  width: 15px;
  height: 15px;
}

[dir="rtl"] .subscription-bar__details svg {
  transform: scaleX(-1);
}

.subscription-bar__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--gray-500);
  border-radius: var(--radius);
  cursor: pointer;
}

.subscription-bar__close:hover {
  background: var(--gray-100);
  color: var(--gray-700);
}

.subscription-bar__close svg {
  width: 16px;
  height: 16px;
}

/* Below 768px the sidebar is an off-canvas drawer (L1) — the bar spans full
   width with no rail inset, whatever the desktop collapsed state. */
@media (max-width: 768px) {
  .subscription-bar,
  body:has(.app.sidebar-collapsed) .subscription-bar { margin-inline-start: 0; }
}

@media (max-width: 640px) {
  .subscription-bar { padding-inline: 0.85rem; }
  .subscription-bar__details span { display: none; }
}

/* ─── Subscription: summary card ─── */
/* Leading accent strip signals subscription health: primary when healthy,
   amber under .sub-warn (expiring soon / grace period). */
#sub-summary-card {
  position: relative;
  overflow: hidden;
}
#sub-summary-card::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  inset-inline-start: 0;
  width: 4px;
  background: var(--primary);
}
#sub-summary-card.sub-warn::before {
  background: var(--warning);
}

.sub-summary-header {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}
.sub-summary-identity {
  min-width: 0;
}
.sub-summary-header h3 {
  margin: 0;
  min-width: 0;
  font-size: 1.3rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}
#sub-customer-since {
  font-size: 0.8rem;
  margin-top: 0.2rem;
}
.sub-summary-badges {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-inline-start: auto;
}

.sub-stats-grid {
  grid-template-columns: repeat(3, 1fr);
}

/* Calmer, flatter stat tiles for the summary card — no hover-lift, no top accent
   bar (the shared dashboard .stat-card keeps those elsewhere). */
#sub-summary-card .sub-stats-grid .stat-card {
  background: var(--surface-alt);
  border: 1px solid var(--border-light);
  box-shadow: none;
  padding: 0.9rem 1.1rem;
  transition: none;
}
#sub-summary-card .sub-stats-grid .stat-card::before { content: none; }
#sub-summary-card .sub-stats-grid .stat-card:hover {
  transform: none;
  box-shadow: none;
  border-color: var(--border-light);
}
#sub-summary-card .sub-stats-grid .stat-card .label {
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: var(--fs-2xs);
  color: var(--text-muted);
}
#sub-summary-card .stat-card .value {
  font-size: 1.35rem;
  font-variant-numeric: tabular-nums;
}

/* Thicker, fully-rounded, gradient time-remaining bar. */
#sub-summary-card .stat-progress {
  height: 9px;
  border-radius: 999px;
}
#sub-summary-card .stat-progress-fill {
  border-radius: 999px;
  background: linear-gradient(to right, var(--primary), var(--primary-light));
}
#sub-summary-card.sub-warn .stat-progress-fill {
  background: linear-gradient(to right, var(--warning), color-mix(in srgb, var(--warning) 65%, white));
}

@media (max-width: 640px) {
  /* Company name on its own line, badges in a row underneath. */
  .sub-summary-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
  }
  .sub-summary-badges { margin-inline-start: 0; }

  /* Stats become compact label/value rows instead of three squeezed columns. */
  .sub-stats-grid {
    grid-template-columns: 1fr;
    gap: 0.5rem;
    margin-bottom: 0;
  }
  /* #sub-summary-card prefix keeps these at/above the desktop overrides' specificity. */
  #sub-summary-card .sub-stats-grid .stat-card {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.7rem 0.95rem;
  }
  #sub-summary-card .sub-stats-grid .stat-card .label { margin-bottom: 0; }
  #sub-summary-card .stat-card .value {
    font-size: 1.05rem;
    text-align: end;
    font-variant-numeric: tabular-nums;
  }

  /* CTA contact buttons go full-width for easy tapping. */
  #sub-cta .btn { flex: 1 1 100%; }
}

/* ─── Subscription: enabled-features chip grid ─── */
.feature-chips {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(205px, 1fr));
  gap: 0.5rem;
  margin: 0;
  padding: 0;
}

@media (max-width: 560px) {
  .feature-chips { grid-template-columns: 1fr; }
}

.feature-chip {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.55rem 0.7rem;
  border: 0.5px solid var(--border-color);
  border-radius: var(--radius);
  background: var(--surface);
  color: inherit;
  text-decoration: none;
}

.feature-chip-icon {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius);
}

.feature-chip-icon svg { width: 16px; height: 16px; }

.feature-chip-label {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.86rem;
  font-weight: 500;
  overflow-wrap: anywhere;
}

.feature-chip-state {
  flex-shrink: 0;
  display: flex;
  align-items: center;
}

.feature-chip-state svg { width: 16px; height: 16px; }

/* Enabled state */
.feature-chip-on .feature-chip-icon {
  background: var(--primary-50);
  color: var(--primary);
}

.feature-chip-on .feature-chip-label { color: var(--text-primary); }
.feature-chip-on .feature-chip-state { color: var(--success); }

/* Disabled (upsell) state */
.feature-chip-off {
  background: var(--surface-alt);
}

a.feature-chip-off { cursor: pointer; }
a.feature-chip-off:hover { border-color: var(--gray-300); }

.feature-chip-off .feature-chip-icon {
  background: var(--gray-100);
  color: var(--text-muted);
}

.feature-chip-off .feature-chip-label { color: var(--text-muted); }

.feature-chip-pill {
  white-space: nowrap;
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--border-color);
  border-radius: 999px;
  font-size: var(--fs-2xs);
  font-weight: 500;
  color: var(--text-secondary);
}

/* Counter pill in the section header */
.feature-count-pill {
  display: inline-block;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  background: var(--primary-50);
  color: var(--primary);
  font-size: var(--fs-2xs);
  font-weight: 600;
}

.feature-count-pill:empty { display: none; }

/* ─── Sidebar Logout Button ─── */
/* Quiet at rest. Logging out is reversible and costs nothing, so it has no
   consequence to signal — red at rest made it the loudest element in the whole
   sidebar and devalued the red that marks genuinely destructive actions. The
   danger tint appears on hover/focus instead, at the moment of intent.
   Rest chrome deliberately mirrors `.sidebar-theme-btn`/`.sidebar-collapse-btn`
   (same fill, border and `--gray-400`) since the three share the footer row; the
   LABEL is what keeps this one findable on a shared workstation, not the colour,
   which is why it is not flattened all the way down to an icon. */
.sidebar-logout-btn {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  height: 36px;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 6px;
  color: var(--sidebar-text);
  cursor: pointer;
  font-size: 0.8125rem;
  white-space: nowrap;
  transition: all var(--transition);
  flex: 1;
}

.sidebar-logout-btn:hover,
.sidebar-logout-btn:focus-visible {
  background: var(--sidebar-danger-bg);
  border-color: var(--sidebar-danger-border);
  color: var(--sidebar-danger);
}

/* The button had no focus style at all — it inherited the UA outline, which is
   invisible against a near-black sidebar. */
.sidebar-logout-btn:focus-visible {
  outline: 2px solid var(--sidebar-danger);
  outline-offset: 1px;
}

.sidebar-logout-btn span {
  overflow: hidden;
}

.sidebar.collapsed .sidebar-logout-btn span {
  display: none;
}

.sidebar.collapsed .sidebar-logout-btn {
  width: 36px;
  height: 36px;
  flex: 0 0 auto;
  justify-content: center;
  padding: 0;
}

/* ─── Warning table clickable rows ─── */
tr.warning-row {
  cursor: pointer;
}
tr.warning-row td {
  transition: background 0.15s;
}
.warning-fix-cell {
  width: 2rem;
  text-align: center;
}
.warning-fix-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  border-radius: var(--radius);
  color: var(--gray-400);
  transition: color 0.15s, background 0.15s;
}
tr.warning-row:hover .warning-fix-link {
  color: var(--primary);
  background: color-mix(in srgb, var(--primary) 10%, transparent);
}
[dir="rtl"] .warning-fix-link svg {
  transform: scaleX(-1);
}

/* ─── Warnings period picker overrides ─── */
/* Match height of the adjacent employee multi-select trigger (2.5rem) */
#warnings-period-picker {
  height: 2.5rem;
  padding-block: 0;
}
/* Smaller font size scoped to this picker only */
#warnings-period-picker .sync-interval-trigger {
  font-size: 0.78rem;
}
#warnings-period-picker .sync-interval-menu {
  min-width: 140px;
}
#warnings-period-picker .sync-interval-option {
  font-size: 0.78rem;
  padding: 0.4rem 0.65rem;
}
/* In RTL the picker sits on the left side of the header — open menu rightward */
[dir="rtl"] #warnings-period-picker .sync-interval-menu {
  right: auto;
  left: 0;
}

/* ─── Warnings filters row ─── */
.warnings-filters {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* Compact pickers inside the warnings card header */
.warnings-filters .sync-interval-picker {
  padding-inline-start: 0.5rem;
  padding-inline-end: 0.55rem;
  padding-block: 0.28rem;
  height: 30px;
}
.warnings-filters .sync-interval-trigger {
  font-size: 0.8rem;
  padding: 0;
}
.warnings-filters .sync-chevron {
  width: 12px;
  height: 12px;
}

/* ─── Raw-warnings card header: wrap-safe on narrow cards ───
   The card can be as narrow as ~240 px when it is the side panel, so the
   title + badge row and the filter-picker row are allowed to wrap onto
   separate lines.  The filters always occupy their own full-width row below
   the title (flex-basis:100%) and the controls stretch to share that width
   evenly, so the row reads as one balanced toolbar rather than a cluster
   pinned to the inline-end. */
#raw-warnings-header {
  flex-wrap: wrap;
  row-gap: 0.35rem;
}
#raw-warnings-header .warnings-filters {
  flex: 1 1 100%;
  width: 100%;
}
/* Stretch the pickers + employee multiselect to fill the row. min-width:0 lets
   them shrink so long employee names don't blow out the row. (The hide button is
   not in this row - it sits on the title line, in the header's inline-end corner.) */
/* The name dropdown and time-range picker stay sized to their own content so
   their labels are always fully readable (name capped so a long name can't run
   away). The warning-type (issue) picker is the flexible one: it grows to fill
   the row and truncates its long selected label via the trigger ellipsis.
   It keeps a readable min-width so on a narrow side-panel card it wraps onto
   its own line (the filter row is flex-wrap) instead of collapsing to a bare
   chevron with no visible label. */
#raw-warnings-header .warnings-filters > #raw-warnings-issue-picker {
  flex: 1 1 7rem;
  min-width: 7rem;
}
#raw-warnings-header .warnings-filters > #raw-warnings-period-picker {
  flex: 0 0 auto;
}
#raw-warnings-header .warnings-filters > .warnings-emp-ms {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 175px;
}
#raw-warnings-header .warnings-filters > .warnings-dept-ms {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 175px;
}
#raw-warnings-header .warnings-filters > .sync-interval-picker .sync-interval-trigger {
  flex: 1;
  justify-content: space-between;
  min-width: 0;
}
#raw-warnings-header .warnings-filters > .sync-interval-picker .sync-interval-trigger span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* On very small screens make pickers a touch more compact */
@media (max-width: 480px) {
  #raw-warnings-header .warnings-filters .sync-interval-trigger {
    font-size: var(--fs-2xs);
  }
  #raw-warnings-header .warnings-filters .sync-interval-picker {
    padding-inline-start: 0.4rem;
    padding-inline-end: 0.45rem;
    height: 27px;
  }
}
/* On very small screens make pickers a touch more compact */
@media (max-width: 480px) {
  #raw-warnings-header .warnings-filters .sync-interval-trigger {
    font-size: var(--fs-2xs);
  }
  #raw-warnings-header .warnings-filters .sync-interval-picker {
    padding-inline-start: 0.4rem;
    padding-inline-end: 0.45rem;
    height: 27px;
  }
}

/* ─── Warnings/alerts card: right column of the attendance page two-column layout ───
   A triage panel (~30% width), align-self:flex-start pins it to the top. It is
   hidden by default and revealed by the toolbar toggle (#btn-toggle-warnings);
   there is no collapsed state any more - the card is either on screen or gone,
   and applyRawWarningsVisibility() in attendance.js owns that decision. */

#raw-warnings-card {
  flex: 0 0 28%;
  min-width: 240px;
  align-self: flex-start;
  overflow: visible;
  transition:
    background-color 0.25s ease,
    border-color 0.25s ease;
}

#raw-warnings-content {
  padding: 0.9rem 1.2rem;
}

/* All-clear (nothing in the widest period, on fresh data): a quiet green frame so
   the opened card reads as "checked and clean" rather than "empty, maybe broken". */
#raw-warnings-card.warnings-all-clear {
  border-color: color-mix(in srgb, var(--success) 40%, transparent);
  background: color-mix(in srgb, var(--success) 5%, var(--surface));
}
[data-theme="dark"] #raw-warnings-card.warnings-all-clear {
  background: color-mix(in srgb, var(--success) 10%, var(--surface));
}

/* ─── Toolbar warnings toggle (#btn-toggle-warnings) ───
   Ghost button carrying the unfiltered warning count. The badge disappears at
   zero (:empty) so a clean tenant sees a plain, quiet button. */
.warnings-toggle-count {
  margin-inline-start: 0.1rem;
  font-size: var(--fs-2xs);
  padding: 0.08rem 0.42rem;
  line-height: 1.4;
}
.warnings-toggle-count:empty {
  display: none;
}
#btn-toggle-warnings.is-active {
  background: var(--warning-bg);
  color: var(--warning-text);
  border-color: var(--warning-light);
}
#btn-toggle-warnings.is-active svg {
  color: var(--warning, #f59e0b);
}

/* ─── Sync-freshness caution above the raw filter bar ───
   Shown only when the last successful sync is stale/unknown/missing. */
.raw-sync-freshness-note {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
  padding: 0.45rem 0.8rem;
  background: var(--warning-bg);
  color: var(--warning-text);
  border: 1px solid var(--warning-light);
  border-radius: var(--radius-lg);
  font-size: 0.82rem;
  font-weight: 500;
}
.raw-sync-freshness-note svg {
  color: var(--warning, #f59e0b);
}
#raw-sync-freshness-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ─── Header hide button (in-card X) ─── */
.raw-warnings-collapse-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 1px solid var(--gray-200, #e5e7eb);
  background: var(--surface, #fff);
  color: var(--gray-500, #6b7280);
  border-radius: 6px;
  cursor: pointer;
  margin-inline-start: 0.25rem;
  transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.raw-warnings-collapse-btn:hover {
  background: var(--gray-50, #f9fafb);
  color: var(--gray-700, #374151);
  border-color: var(--gray-300, #d1d5db);
}
.raw-warnings-collapse-btn:focus-visible {
  outline: 2px solid var(--primary, #4f46e5);
  outline-offset: 1px;
}
[data-theme="dark"] .raw-warnings-collapse-btn {
  background: color-mix(in srgb, var(--surface) 80%, #000);
  border-color: var(--gray-700, #374151);
  color: var(--gray-400, #9ca3af);
}
[data-theme="dark"] .raw-warnings-collapse-btn:hover {
  background: color-mix(in srgb, var(--surface) 65%, #000);
  color: var(--gray-200, #e5e7eb);
}

/* ─── Attendance page: two-column layout ───
   #raw-warnings-card is the right column (first in DOM → right in RTL flex).
   #attendance-left-col is the left column (filter bar + punches table + daily view). */
#attendance-content-layout {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  align-items: flex-start;
}

#attendance-left-col {
  flex: 1 1 0%;
  min-width: 0;
}

/* Strict column widths: overflow clips with ellipsis on fixed-layout table */
.raw-punches-col td {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 0; /* required for text-overflow to work in table cells */
}

/* Employee punch detail modal: keep the compact read-only table inside the dialog. */
#emp-day-detail-modal .emp-day-detail-table-wrap {
  overflow-x: hidden;
  min-height: 0;
}

#emp-day-detail-modal .emp-day-detail-table {
  width: 100%;
  min-width: 0;
  table-layout: fixed;
}

#emp-day-detail-modal .emp-day-detail-table th,
#emp-day-detail-modal .emp-day-detail-table td {
  padding: 0.5rem 0.45rem;
  font-size: 0.8125rem;
  line-height: 1.25;
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}

#emp-day-detail-modal .emp-day-detail-table th {
  letter-spacing: 0;
}

#emp-day-detail-modal .emp-day-detail-table td {
  vertical-align: middle;
}

#emp-day-detail-modal .flag-pill {
  max-width: 100%;
  padding: 0.12rem 0.38rem;
  font-size: var(--fs-2xs);
  line-height: 1.2;
  white-space: normal;
}

/* Tighter spacing around warnings card in attendance page */
#toolbar-raw ~ .filter-bar,
#toolbar-raw ~ #attendance-content-layout .filter-bar {
  margin-bottom: 0.75rem;
}

/* ─── Inline warnings card: scoped visual overrides ─── */

/* Push the scrollable list out to the card's inline-end edge so the
   scrollbar sits flush against the card border instead of floating in the
   middle of the panel. The negative inline-end margin cancels the parent
   #raw-warnings-content's inline padding (1.2rem); inline-end padding on
   the list keeps items visually clear of the scrollbar. The header above
   keeps its normal padding so it remains inset from the edge.
   In RTL the list is forced to ltr so the browser places the scrollbar on
   the physical right; each item is restored to rtl for content rendering. */
#raw-warnings-card .dash-review-list {
  direction: ltr;
  margin-inline-end: -1.2rem;
  padding-inline-end: 0.4rem;
}
#raw-warnings-card .dash-review-list > li {
  direction: rtl;
}

/* Reduce horizontal padding so the item content has more room in the narrow card */
#raw-warnings-card .dash-review-item {
  padding: 0.45rem 0.55rem;
  gap: 0.5rem;
}

/* Issue title — slightly larger to lead the eye clearly */
#raw-warnings-card .dash-review-row1 {
  font-size: 0.85rem;
}

/* Meta row — smaller + more gap so date and time breathe; no wrapping in narrow card */
#raw-warnings-card .dash-review-row2 {
  font-size: var(--fs-2xs);
  gap: 0.25rem;
  flex-wrap: nowrap;
  overflow: hidden;
}
#raw-warnings-card .dash-review-emp {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
#raw-warnings-card .dash-review-date,
#raw-warnings-card .dash-review-last {
  white-space: nowrap;
  flex-shrink: 0;
}

/* CTA button — simple ghost; no primary fill, no bold colour */
#raw-warnings-card .dash-review-cta {
  background: transparent;
  border-color: var(--gray-200, #e5e7eb);
  color: var(--gray-500, #6b7280);
  font-weight: 500;
  padding: 0.22rem 0.5rem;
  font-size: 0.75rem;
}
#raw-warnings-card .dash-review-item:hover .dash-review-cta {
  background: var(--gray-50, #f9fafb);
  border-color: var(--gray-300, #d1d5db);
  color: var(--gray-700, #374151);
}
[data-theme="dark"] #raw-warnings-card .dash-review-cta {
  border-color: var(--gray-700, #374151);
  color: var(--gray-400, #9ca3af);
}
[data-theme="dark"] #raw-warnings-card .dash-review-item:hover .dash-review-cta {
  background: var(--gray-800, #1f2937);
  border-color: var(--gray-600, #4b5563);
  color: var(--gray-300, #d1d5db);
}

/* ─── Attendance page: responsive two-column layout ─── */
/* Stack vertically on screens narrower than 900px */
@media (max-width: 900px) {
  #attendance-content-layout {
    flex-direction: column;
    /* In column mode the cross-axis is horizontal; the desktop
       `align-items: flex-start` would shrink each column to its content
       width, collapsing the filter card. Stretch so both columns fill. */
    align-items: stretch;
  }
  #attendance-left-col {
    width: 100%;
  }
  #raw-warnings-card {
    flex: none !important;
    width: 100% !important;
    max-width: 100% !important;
  }
}

#warnings-employee-ms .ms-trigger,
#warnings-department-ms .ms-trigger {
  font-size: 0.78rem;
  padding: 0.3rem 0.6rem;
  min-height: auto;
}

/* Warning-toolbar labels should have one visual weight whether they currently
   show an "All …" placeholder or a selected employee/department value. */
#warnings-card .warnings-filters .ms-display,
#raw-warnings-card .warnings-filters .ms-display {
  font-weight: 400;
}

#warnings-department-ms .ms-dropdown {
  min-width: 210px;
}

/* ─── Compact employee multiselect inside the raw-warnings card header ───
   Sized to sit alongside the issue/period pickers (30 px tall, ~0.8rem).
   The trigger is kept narrow so the filter row fits the ~240 px side panel;
   the dropdown is widened independently so employee names stay readable. */
#raw-warnings-employee-ms.warnings-emp-ms {
  width: auto;
  min-width: 0;
  max-width: none;
}
#raw-warnings-department-ms.warnings-dept-ms {
  width: auto;
  min-width: 0;
  max-width: none;
}
#raw-warnings-employee-ms.warnings-emp-ms .ms-trigger,
#raw-warnings-department-ms.warnings-dept-ms .ms-trigger {
  height: 30px;
  padding: 0.28rem 0.55rem;
  border-radius: 8px;
  font-size: 0.8rem;
  gap: 0.4rem;
}
#raw-warnings-employee-ms.warnings-emp-ms .ms-dropdown,
#raw-warnings-department-ms.warnings-dept-ms .ms-dropdown {
  min-width: 200px;
}
@media (max-width: 480px) {
  #raw-warnings-employee-ms.warnings-emp-ms .ms-trigger,
  #raw-warnings-department-ms.warnings-dept-ms .ms-trigger {
    font-size: var(--fs-2xs);
    height: 27px;
  }
}
@media (max-width: 600px) {
  #warnings-card > h3 { flex-wrap: wrap; gap: 0.5rem; }
  .warnings-filters { width: 100%; }
  #warnings-employee-ms { min-width: 0 !important; max-width: none !important; flex: 1; }
}

/* ─── Reports accuracy note banner ─── */
.report-accuracy-note {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.7rem 1rem;
  border-radius: var(--radius, 0.75rem);
  background: color-mix(in srgb, var(--primary) 7%, transparent);
  border: 1px solid color-mix(in srgb, var(--primary) 22%, transparent);
  border-inline-start: 3px solid var(--primary);
  margin-bottom: 0.85rem;
  font-size: 0.815rem;
  line-height: 1.5;
  color: var(--text-secondary, #4b5563);
}
.report-accuracy-note__icon {
  color: var(--primary);
  flex-shrink: 0;
  opacity: 0.85;
}
.report-accuracy-note__text {
  flex: 1;
  margin: 0;
}
.report-accuracy-note__link {
  flex-shrink: 0;
  align-self: center;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--primary);
  text-decoration: none;
  white-space: nowrap;
  padding: 0.25rem 0.65rem;
  border-radius: 6px;
  border: 1px solid color-mix(in srgb, var(--primary) 35%, transparent);
  background: color-mix(in srgb, var(--primary) 8%, transparent);
  transition: background 0.15s, border-color 0.15s;
}
.report-accuracy-note__link:hover {
  background: color-mix(in srgb, var(--primary) 16%, transparent);
  border-color: var(--primary);
}

/* ─── Generated-report employee preview selector ─── */
.report-preview-select-wrap {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

/* Override menu alignment so it drops left-aligned under the picker */
#report-preview-picker .sync-interval-menu {
  left: 0;
  right: auto;
  min-width: 200px;
}

[dir="rtl"] #report-preview-picker .sync-interval-menu {
  left: auto;
  right: 0;
}

/* Search box inside the employee preview dropdown */
.report-preview-search-wrap {
  padding: 0.3rem 0.35rem 0.25rem;
}
.report-preview-search {
  width: 100%;
  box-sizing: border-box;
  padding: 0.3rem 0.6rem;
  font-size: 0.8rem;
  font-family: inherit;
  border: 1.5px solid var(--gray-200);
  border-radius: 7px;
  background: var(--surface-alt, var(--gray-50));
  color: var(--text-primary);
  outline: none;
  transition: border-color 0.15s;
}
.report-preview-search:focus {
  border-color: var(--primary);
  background: var(--surface);
}
[data-theme="dark"] .report-preview-search {
  background: color-mix(in srgb, var(--surface) 80%, transparent);
  border-color: var(--gray-600, #4b5563);
  color: var(--text-primary);
}
[data-theme="dark"] .report-preview-search:focus {
  border-color: var(--primary);
}

/* Employee count badge inside action buttons */
.report-action-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.82rem;
  font-weight: 700;
  line-height: 1;
  background: none;
  padding: 0;
  margin: 0 1px 0 2px;
  vertical-align: middle;
  letter-spacing: 0;
}
.report-action-count:empty {
  display: none;
}

/* ─── Editable balance cells ─── */
.balance-cell {
  cursor: pointer;
  position: relative;
  transition: background 0.15s;
}
.balance-cell:hover {
  background: color-mix(in srgb, var(--primary) 10%, transparent);
}
.balance-cell::after {
  content: '✎';
  position: absolute;
  top: 2px;
  right: 4px;
  font-size: var(--fs-2xs);
  opacity: 0;
  transition: opacity 0.15s;
  color: var(--primary);
}
.balance-cell:hover::after {
  opacity: 0.7;
}

/* Leave-balance matrix cell for a type the employee is NOT eligible for:
   just a muted dash (with tooltip), non-interactive. No .balance-cell class, so
   it has no hover/edit affordance and the click handler skips it. */
.balance-na {
  color: var(--text-muted);
  cursor: default;
}

.balance-breakdown {
  border: 1px solid var(--gray-200);
  border-radius: var(--radius-md);
  background: var(--gray-50);
  padding: 0.75rem;
  margin-bottom: 1rem;
}

.balance-breakdown h4 {
  margin: 0 0 0.5rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--gray-700);
}

.balance-breakdown-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.25rem 0;
  font-size: 0.82rem;
  color: var(--gray-600);
}

.balance-breakdown-row[hidden] {
  display: none;
}

.balance-breakdown-row strong {
  color: var(--gray-800);
  font-weight: 600;
}

.balance-breakdown-row--offset {
  color: var(--warning);
}

.balance-breakdown-row--offset strong {
  color: var(--warning);
}

.balance-breakdown-row--remaining {
  border-top: 1px solid var(--gray-200);
  margin-top: 0.35rem;
  padding-top: 0.5rem;
  font-weight: 600;
  color: var(--gray-800);
}

/* ─────────────────────────────────────────────────────────────────
   Dashboard redesign — hero, KPI grid, pulse, review list, etc.
   ───────────────────────────────────────────────────────────────── */

.visually-hidden,
.sr-only {
  position: absolute !important;
  width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* The dashboard opens with a small status chip rather than a page title, so the
   global 2rem top padding read as a gap above it. Scoped to this page. Kept
   non-zero so the chip's border does not sit flush against the viewport edge. */
.main-content[data-page="dashboard"] { padding-top: 0.5rem; }

/* ── Sync status row (above the hero card) ─────────────────────── */
.dash-statusbar {
  display: flex;
  justify-content: flex-end;   /* inline end: right in LTR, left in RTL */
  /* The Connector chip (M3 Task 6) shares this row with the sync chip; without
     a gap the two pills touch. `gap` is direction-agnostic, so the row needs no
     RTL variant. */
  gap: 0.4rem;
  flex-wrap: wrap;
  margin-bottom: 0.4rem;
  min-width: 0;
}

/* ── Hero header ──────────────────────────────────────────────── */
.dash-hero {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.55rem 1rem;
  padding: 0.7rem 1.1rem;
  /* Plain card surface, same tokens as every other dashboard card: the tinted
     gradient + primary glow + primary edge stripe made the page's first card
     read as a different component from the ones under it. */
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  margin-bottom: 0.6rem;
  position: relative;
  overflow: hidden;
}
.dash-lower {
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
}
.dash-lower > .card { margin-bottom: 0 !important; }

/* Content-sized, so the quick-action chips and the status chip in
   `.dash-hero-side` get the rest of the row instead of being pushed off it. */
.dash-hero-main {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-width: 0;
  flex: 0 1 auto;
}
.dash-hero-avatar {
  width: 40px;
  height: 40px;
  border-radius: 11px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg,
    color-mix(in srgb, var(--primary) 18%, var(--surface)) 0%,
    color-mix(in srgb, var(--primary) 8%, var(--surface)) 100%);
  color: var(--primary);
  border: 1px solid color-mix(in srgb, var(--primary) 22%, transparent);
  flex-shrink: 0;
  box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 60%, transparent);
}
.dash-hero-avatar-logo {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* With a tenant logo the slot is the logo itself — the tile chrome (gradient,
   border, inset highlight) would frame it like a second card, so it is dropped
   and the slot grows to give the artwork room. */
.dash-hero-avatar.has-logo {
  width: 48px;
  height: 48px;
  background: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
}
/* Meta line (date · employee count) sits UNDER the greeting. */
.dash-hero-greet {
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.2rem;
}
/* The heading carries the SALUTATION, which is boilerplate - so it sits a step
   back (600 / --gray-700) and `.dash-hero-greet-name` below takes the emphasis.
   Reversing that put the same weight on "Good morning" as on who you are. */
.dash-hero-greet h2 {
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: -0.015em;
  margin: 0;
  color: var(--gray-700);
  /* NOT 1.25, and this is an RTL constraint rather than taste: Arabic faces
     have a content box around 1.36em (ascent + descent), so a 1.25 line box is
     shorter than the glyphs and their tails (ص / ح / خ) cross it. Latin never
     showed it - ~1.17em fits inside 1.25. Same metric difference CLAUDE.md
     records for .ms-done. Re-check any change with an Arabic font installed. */
  line-height: 1.45;
  /* `overflow: hidden` + `text-overflow: ellipsis` USED to sit here and were
     inert: ellipsis needs `white-space: nowrap`, which this rule never set, so
     a long name wrapped rather than truncating. Measured with a 60-char name at
     260px, hidden and visible were byte-identical (3 lines, 73px, no
     horizontal overflow). The one thing `hidden` did do was clip the descender
     overflow above into a hard cut. Do not restore the pair without `nowrap` -
     and if you ever want real truncation, add all three together and give the
     box padding-block-end, since the clip edge is the PADDING box. */
}
/* Wrapped by _writeGreeting() in dashboard.js around the name only. */
.dash-hero-greet-name {
  font-weight: 700;
  color: var(--gray-900);
}
.dash-hero-meta {
  margin: 0;
  font-size: 0.8rem;
  color: var(--gray-500);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  line-height: 1.4;
}
.dash-hero-meta-extra {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
.dash-hero-meta-extra[hidden] { display: none; }
/* --gray-400, not --gray-300: at this line's 0.8rem a #d1d5db dot on white sat
   below the visible threshold, so the date and the count read as one run. */
.dash-hero-sep {
  color: var(--gray-400);
  font-weight: 600;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;   /* E1: chrome, not data — no long-press callout */
}
.dash-hero-attendance {
  font-weight: 500;
  color: var(--gray-700);
}

.dash-hero-side {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  justify-content: flex-end;
  /* Content-sized so the hero's own `flex-wrap` drops this group onto a second
     line when the greeting and the chips stop fitting. A `0` basis (needed back
     when the long sync sentence lived here) made it always "fit", and since the
     chips cannot shrink they then overflowed sideways OVER the greeting. */
  flex: 0 1 auto;
  min-width: 0;
}

/* ── Quick-action chips (hero) ─────────────────────────────────────
   These were a `.dash-qa-panel` aside inside the KPI band. Three stacked
   buttons made it the tallest column of that grid (276px), which stretched
   the KPI cards to match and left dead space inside them. As chips they fit
   in the hero's spare width and cost the band nothing.
   `margin-inline-end: auto` keeps them next to the greeting and pushes the
   sync-status chip to the far edge.                                    */
.dash-hero-qa {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  /* Pushed to the hero's far inline end — the opposite side from the greeting
     (far left in RTL, far right in LTR). */
  margin-inline-start: auto;
  /* Don't shrink: each chip is `nowrap`, so the group answers a squeeze by
     wrapping a chip to a second line, growing the hero instead of narrowing. */
  flex: 0 0 auto;
}
.dash-hero-qa-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.4rem 0.75rem;
  background: var(--surface);
  border: 1px solid var(--gray-200);
  /* Squared off, matching the KPI cards' 8px rather than the pill shape. */
  border-radius: 9px;
  cursor: pointer;
  font-family: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--gray-800);
  white-space: nowrap;
  transition: background 140ms, border-color 140ms, box-shadow 140ms, transform 140ms;
}
/* Feature-gated: dashboard.js sets `hidden` on the leave/salary chips when the
   tenant flag is off, and `display:inline-flex` above would otherwise win. */
.dash-hero-qa-btn[hidden] { display: none !important; }
/* The one accent in this group, and it is the brand's — it used to be a second
   green, competing with the attendance ring for the same meaning. */
.dash-hero-qa-btn-primary {
  background:
    linear-gradient(135deg, color-mix(in srgb, var(--primary) 9%, transparent), transparent 70%),
    var(--surface);
  border-color: color-mix(in srgb, var(--primary) 24%, var(--gray-200));
}
.dash-hero-qa-btn:hover {
  background: var(--gray-50);
  border-color: color-mix(in srgb, var(--primary) 30%, var(--gray-200));
  box-shadow: 0 4px 14px -10px color-mix(in srgb, var(--gray-900) 32%, transparent);
  transform: translateY(-1px);
}
.dash-hero-qa-btn:active { transform: translateY(0); }
.dash-hero-qa-btn:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
.dash-hero-qa-icon {
  width: 24px; height: 24px;
  border-radius: 7px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
/* One neutral CHIP treatment for all three - button surface, border and label
   stay identical, so the hero still spends no colour on chrome. Only the glyph
   carries a semantic hue, at the muted badge-TEXT tone (not the saturated icon
   fill), which is why the neutral --gray-100 tile is kept underneath. Each
   token has a dark-theme value, so the tints stay legible on #1e293b with no
   [data-theme] override here. Hover still carries the brand (rule below wins
   on source order at equal specificity - keep these three ABOVE it). */
.dash-hero-qa-btn .dash-hero-qa-icon { background: var(--gray-100); color: var(--gray-500); }
.dash-hero-qa-btn[data-tone="punch"]  .dash-hero-qa-icon { color: var(--success-text); }
.dash-hero-qa-btn[data-tone="leave"]  .dash-hero-qa-icon { color: var(--purple-text); }
.dash-hero-qa-btn[data-tone="salary"] .dash-hero-qa-icon { color: var(--warning-text); }
.dash-hero-qa-btn:hover .dash-hero-qa-icon { background: var(--primary-50); color: var(--primary); }

/* Lives in `.dash-statusbar` above the hero. Shrinkable so a long status
   sentence ellipsizes instead of widening its row; renderHeroStatus() mirrors
   the text into `title` (i18n.js turns that into the custom tooltip), so
   nothing is lost. */
.dash-hero-status {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  flex: 0 1 auto;
  min-width: 0;
  padding: 0.28rem 0.7rem;
  background: color-mix(in srgb, var(--surface) 85%, transparent);
  backdrop-filter: blur(4px);
  border: 1px solid var(--gray-200);
  border-radius: 999px;
  font-size: 0.78rem;
  color: var(--gray-600);
  font-weight: 500;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dash-hero-status-dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--gray-400);
  flex-shrink: 0;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gray-400) 22%, transparent);
}
.dash-hero-status[data-tone="success"] {
  border-color: color-mix(in srgb, var(--success) 28%, transparent);
  color: color-mix(in srgb, var(--success) 80%, var(--gray-700));
}
.dash-hero-status[data-tone="success"] .dash-hero-status-dot {
  background: var(--success);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 22%, transparent);
  animation: dash-pulse-dot 2.4s ease-in-out infinite;
}
.dash-hero-status[data-tone="danger"] {
  border-color: color-mix(in srgb, var(--danger) 28%, transparent);
  color: color-mix(in srgb, var(--danger) 80%, var(--gray-700));
}
.dash-hero-status[data-tone="danger"] .dash-hero-status-dot {
  background: var(--danger);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 22%, transparent);
}
.dash-hero-status[data-tone="info"] {
  border-color: color-mix(in srgb, var(--primary) 28%, transparent);
  color: color-mix(in srgb, var(--primary) 80%, var(--gray-700));
}
.dash-hero-status[data-tone="info"] .dash-hero-status-dot {
  background: var(--primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 22%, transparent);
  animation: dash-pulse-dot 1.4s ease-in-out infinite;
}
/* min-width:0 is what lets a flex item shrink below its content width — without
   it the ellipsis never engages and the chip forces the hero to wrap. */
.dash-hero-status-text { overflow: hidden; text-overflow: ellipsis; min-width: 0; }

/* ── Kronex Connector status chip (M3 Task 6) ────────────────────────────────
   Reuses `.dash-hero-status` for its shape so the two pills in `.dash-statusbar`
   are visibly one component, and adds only what differs: it is a LINK, and it
   is toned by SEVERITY + a separate ALERT boolean rather than by the sync chip's
   own `data-tone` vocabulary.

   `data-alert` is in the selector on purpose. "`away` never alerts" is a product
   rule (§4: never say "offline" when the truth is "off for the night"), and
   putting the boolean in the CSS means the quiet states cannot pick up an alarm
   colour by acquiring a severity later — an `info` or `ok` verdict has no loud
   rule to match at all.

   No hardcoded left/right anywhere here: shape comes from flex + gap, and the
   only spacing is `margin-inline` on the separator. */
.dash-connector-chip {
  text-decoration: none;
  cursor: pointer;
}
/* `.dash-hero-status`'s `display: inline-flex` is an AUTHOR rule, so it beats
   the UA stylesheet's `[hidden] { display: none }` outright - author styles win
   over UA styles regardless of specificity. Without this the chip ships
   `hidden` in the markup and renders ANYWAY: an empty grey "Connector ·" pill
   on the dashboard of every tenant whose `feature_connector_enabled` is off,
   which today is every tenant but one, plus a flash of it on every dashboard
   load before the first `GET /status` lands.

   Found by Task 8's E2E guard, and only because that guard asserts VISIBILITY.
   Task 6 proved the same rule by reading `chip.hidden === true` - the property
   was correct the whole time and the page was wrong, which is precisely the
   class of defect a property read cannot see.

   Fourth instance of this exact trap in this file (`.btn[hidden]`,
   `.form-msg[hidden]`, `.dash-hero-qa-btn[hidden]`): every element whose
   author CSS sets `display` owes its own `[hidden]` rule. Scoped to the shared
   `.dash-hero-status` rather than the chip so the sync pill beside it inherits
   the correction if it is ever hidden too. */
.dash-hero-status[hidden] { display: none; }
.dash-connector-chip:hover {
  border-color: var(--gray-300);
  background: var(--surface);
}
.dash-connector-chip:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
.dash-connector-chip-sep {
  margin-inline: 0.3rem;
  opacity: 0.55;
}
/* Healthy. Quiet enough that a green pill is not the loudest thing on the page,
   and deliberately not animated — the sync chip's pulsing dot means "running
   right now", which is a different claim. */
.dash-connector-chip[data-severity="ok"] {
  border-color: color-mix(in srgb, var(--success) 28%, transparent);
  color: color-mix(in srgb, var(--success) 80%, var(--gray-700));
}
.dash-connector-chip[data-severity="ok"] .dash-hero-status-dot {
  background: var(--success);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 22%, transparent);
}
/* The two loud states, and they are reachable ONLY through data-alert="true".
   `away` and the never-paired case are `info`/`info` and match neither. */
.dash-connector-chip[data-alert="true"][data-severity="warn"] {
  border-color: color-mix(in srgb, var(--warning) 34%, transparent);
  color: var(--warning-text);
}
.dash-connector-chip[data-alert="true"][data-severity="warn"] .dash-hero-status-dot {
  background: var(--warning);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--warning) 24%, transparent);
}
.dash-connector-chip[data-alert="true"][data-severity="error"] {
  border-color: color-mix(in srgb, var(--danger) 34%, transparent);
  color: color-mix(in srgb, var(--danger) 85%, var(--gray-700));
}
.dash-connector-chip[data-alert="true"][data-severity="error"] .dash-hero-status-dot {
  background: var(--danger);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--danger) 24%, transparent);
  animation: dash-pulse-dot 2.4s ease-in-out infinite;
}
/* Never paired: an invitation, not a fault. A dashed edge reads as "something to
   do" without borrowing a status colour that would say something is wrong. */
.dash-connector-chip[data-state="none"] {
  border-style: dashed;
  color: var(--gray-600);
}
@keyframes dash-pulse-dot {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.6; transform: scale(0.85); }
}

.dash-hero-controls {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  padding: 0.25rem;
  background: color-mix(in srgb, var(--surface) 80%, transparent);
  border: 1px solid var(--gray-200);
  border-radius: 11px;
}
.dash-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  width: 34px; height: 34px;
  padding: 0;
  background: transparent;
  color: var(--gray-600);
  border: 1px solid transparent;
  border-radius: 8px;
  cursor: pointer;
  transition: border-color .15s, color .15s, background .15s, transform .1s;
  position: relative;
}
.dash-icon-btn:hover {
  background: var(--surface);
  border-color: var(--gray-200);
  color: var(--primary);
}
.dash-icon-btn:active { transform: scale(0.95); }
.dash-icon-btn:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
.dash-icon-btn:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}
#sync-btn[data-spinning="true"] svg { animation: dash-spin 0.9s linear infinite; }
@keyframes dash-spin {
  to { transform: rotate(360deg); }
}
[dir="rtl"] #sync-btn[data-spinning="true"] svg { animation-direction: reverse; }

.dash-theme-btn { cursor: pointer; }
.dash-theme-btn .icon-moon { display: none; }
[data-theme="dark"] .dash-theme-btn .icon-sun { display: none; }
[data-theme="dark"] .dash-theme-btn .icon-moon { display: inline-block; }

.dash-hero-sync {
  margin: 0 !important;
  background: transparent;
  border: none;
}
.dash-hero-sync .sync-interval-trigger {
  font-size: 0.78rem;
  padding: 0 0.6rem;
  height: 34px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 8px;
}
.dash-hero-sync .sync-interval-trigger:hover {
  background: var(--surface);
  border-color: var(--gray-200);
}
[dir="rtl"] .dash-hero .sync-interval-menu {
  right: auto; left: 0;
}

.dash-hero-sync-progress {
  flex-basis: 100%;
  margin: 0;
}
.dash-hero-sync-result {
  flex-basis: 100%;
  font-size: 0.8rem;
  color: var(--gray-600);
}
.dash-hero-sync-result:empty { display: none; }

/* ── Today at a Glance section ──────────────────────────────────
   No heading row: it cost 33px to label four cards that label themselves, and
   its employee count moved to the hero meta line. The section keeps its
   accessible name via `data-i18n-aria-label` on the element itself.        */
.dash-overview {
  /* One number, shared by the KPI band's height floor, the overtime card and the
     review card's cap below, so the columns cannot fight over the row height:
     without the cap a long review list stretched the whole band (and every KPI
     card in it). This is the STACKED baseline — where the three share a row the
     media query below raises it, and only the KPI block reads it here. */
  --dash-band-h: 186px;
  /* The overtime card's width as the band's last column. Sized to the primary
     KPI card's cap (300px) so the two read as peers rather than as a card next
     to a sliver — see the card's own block below for the height budget this
     width buys. Raising it raises `--dash-band-3col` by the same amount. */
  --dash-ot-w: 300px;
  /* The review card's measured floor, and the number that decides when the band
     can be three columns at all. MEASURED, not guessed: forcing the card to a
     fixed width and reading `.dash-review-head`, the title and the filters stop
     sharing one line below 420px in English and below 400px in Arabic (the
     Arabic title is the shorter of the two). 440px keeps a 20px cushion over the
     English figure. A tenant that also shows the DEPARTMENT picker needs 560px
     for one line — that third control cannot be budgeted for here (it would push
     the three-column band past 1760px and cost every tenant the layout for the
     benefit of some), so those tenants get the same two-line header they get
     today; the card's height is capped either way, so the list absorbs it. */
  --dash-review-min: 440px;
  margin-bottom: 0.6rem;
}

/* Stacked (below the side-by-side breakpoint) the card sits directly under the
   band in normal flow, so it needs its own gap; the flex layout below supplies
   that gap itself and resets this. */
.dash-overview > .dash-review-card { margin-top: 0.6rem; }

/* ── The hero band, side by side ────────────────────────────────
   Reading order is KPIs | review | overtime, which is also the DOM order, so
   nothing here reorders anything at the widths where all three fit.

   The band has two side-by-side tiers, because the arithmetic allows only two:
     >= 1460px  KPIs + review share the row, overtime takes a full-width strip
                UNDER them (`order: 3`, which the review card's explicit
                `order: 2` makes necessary even though the DOM already puts
                the strip last)
     >= 1640px  all three are columns; the overtime card takes the last one

   Where 1640 comes from: the band's inline width is the viewport minus 302px
   (sidebar + page padding, constant at every width in this range). Three
   columns need 568 (capped KPIs) + 8 + 300 (overtime) + 8 + 440 (the review
   card's measured floor) = 1324px, i.e. a 1626px viewport; 1640 rounds that up
   and leaves the review card 454px. Narrowing the KPI tracks from 658 to 568 is
   what pulled this down from the 1780px it used to be — a 1512px laptop still
   gets the two-tier layout, but the review card there grows from 544px to
   634px, which is the first width at which a tenant WITH a department picker
   gets a one-line header.                                                    */
@media (min-width: 1460px) {
  .dash-overview {
    display: flex;
    /* Wrap is what drops the overtime strip onto its own line in the first
       tier. The KPI band and the review card never wrap against each other
       (568px fixed + a `min-width: 0` card), so it changes nothing for them. */
    flex-wrap: wrap;
    align-items: stretch;
    gap: 0.5rem;
    /* The band is TALLER than the stacked baseline, and the raise is scoped to
       the widths where the three actually share a row — below this breakpoint
       `--dash-band-h` is only the KPI block's own floor, and stretching that in
       isolation buys nothing.

       The number is derived from the review card, which is what the height is
       for: 56px of chrome (padding + border, plus a 30px filter row and its
       0.55rem margin) and then WHOLE 38px rows — a part-row reads as a glitch,
       the same reason `.dash-review-list`'s own cap is a whole multiple.
       56 + 5x38 = 246, so this shows five rows where 186px showed three; 248
       was the first try and it left a 2px sliver of a sixth. MEASURED in the
       browser, not derived — re-measure rather than re-deriving if the header
       or the row height moves, and re-check all three consumers this feeds:
       the KPI block's floor, the review card's cap, and the overtime card's
       cap in the 1640 tier. */
    --dash-band-h: 246px;
  }
  /* Content-sized: its own tracks decide its width, the card takes the rest. */
  .dash-overview > .kpi-command { flex: 0 0 auto; }
  .dash-overview > .dash-review-card {
    /* Absorbs all surplus width, as before — but the basis is now the measured
       floor rather than `auto`, so in the three-column tier the card can never
       be squeezed under it: if the surplus is not there, the OVERTIME strip is
       what wraps (`order: 3` below), not this card's header onto two lines. */
    flex: 1 1 var(--dash-review-min);
    order: 2;
    min-width: 0;
    margin-top: 0;
    /* Capped, not content-sized: the band is the row's height authority and the
       list scrolls inside whatever is left. */
    max-height: var(--dash-band-h);
    display: flex;
    flex-direction: column;
    padding: 0.55rem 0.7rem;
  }
  /* The card is stretched to the band's height, so the list takes whatever the
     header leaves rather than its own fixed max-height. */
  .dash-overview > .dash-review-card .dash-review-list {
    flex: 1 1 auto;
    min-height: 0;
    max-height: none;
  }
  /* Compact header: title and filters share one line where they fit, and the
     controls lose the full-size form padding. */
  .dash-overview > .dash-review-card .dash-review-head {
    margin-bottom: 0.4rem;
    gap: 0.3rem 0.5rem;
  }
  /* No `font-size` here any more: this used to shrink the title to 0.88rem for
     the narrow side-by-side column, and the base size is now below that — the
     override would have made the title BIGGER in exactly the tight layout it
     was written to relieve. */
  .dash-overview > .dash-review-card .dash-review-title-wrap { flex: 0 0 auto; }
  .dash-overview > .dash-review-card .dash-review-filters {
    flex: 1 1 auto;
    width: auto;
    gap: 0.3rem;
    /* nowrap so the pickers SHRINK when the row is tight: wrapping is decided by
       hypothetical size, so with `wrap` a 190px picker jumped to a second line
       (doubling the header) instead of narrowing to its 105px floor. */
    flex-wrap: nowrap;
  }
  /* 30px controls, matching the compact set the attendance side panel already
     uses. The id-scoped rules above (`#warnings-employee-ms .ms-trigger`) beat a
     plain class selector, so these have to be id-scoped too. */
  .dash-overview #warnings-employee-ms .ms-trigger,
  .dash-overview #warnings-department-ms .ms-trigger {
    height: 30px;
    padding: 0.25rem 0.5rem;
    border-radius: 8px;
  }
  .dash-overview #warnings-period-picker { height: 30px; }
  /* The controls carry inline min/max widths in the HTML, so width has to shout
     to be heard. */
  /* Sized to their labels instead of growing: at 1920 the employee picker was
     stretching to 555px for the word "All Employees". */
  .dash-overview #warnings-employee-ms,
  .dash-overview #warnings-department-ms {
    min-width: 105px !important;
    max-width: 190px !important;
    flex: 0 1 190px !important;
  }
  .dash-overview #warnings-period-picker {
    flex: 0 1 120px !important;
    max-width: 130px !important;
  }
}

/* ── Overtime trend card ─────────────────────────────────────────
   Present at EVERY width, in one of two layouts:
     >= 1640px  the hero band's LAST column, a 300px card laid out vertically
     narrower   its own full-width strip under the band, laid out horizontally

   The horizontal strip is the DEFAULT and the band card is the special case,
   because the strip is the layout that works at any width — so the base rules
   need no breakpoint and the 1640px query only has to flip what differs.

   THREE ZONES, divided by hairlines: [title + window] | [headline + subtitle] |
   [contributors]. Without them the strip read as one run of unaligned
   fragments with no boundary between the summary and the breakdown. The rule
   is per-zone rather than a single `> * + *` so the card layout can turn the
   side hairline into a top border for the one zone that still needs it.

   `:not([hidden])` throughout is what still lets loadOvertimeCard() hide the
   card outright on a 403 (an employee with no direct reports).            */
.dash-ot-card:not([hidden]) {
  display: flex;
  flex-direction: row;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.35rem 0;
  padding: 0.5rem 0.75rem;
  margin-bottom: 0 !important;
  min-width: 0;
}
.dash-overview > .dash-ot-card { margin-top: 0.5rem; }

/* First side-by-side tier: not enough width for three columns, so the strip
   keeps its full-width line underneath both of the cards that share the row.
   The card is last in the DOM, so the explicit `order` only restates that —
   it is kept because the review card carries an explicit `order: 2` and a
   strip wedged between the KPI band and the review card would split the row
   the two of them share. */
@media (min-width: 1460px) {
  .dash-overview > .dash-ot-card:not([hidden]) {
    flex: 1 0 100%;
    order: 3;
  }
}

/* Zones own their own inline padding so the hairline sits in the gutter rather
   than hard against the text. */
.dash-ot-zone {
  display: flex;
  align-items: center;
  min-width: 0;
  padding-inline: 0.85rem;
}
.dash-ot-zone:first-child { padding-inline-start: 0; }
.dash-ot-zone-figure,
.dash-ot-top { border-inline-start: 1px solid var(--gray-200); }

.dash-ot-zone-head {
  gap: 0.6rem;
  flex: 0 0 auto;
}
.dash-ot-zone-head h3 {
  margin: 0;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--gray-800);
  white-space: nowrap;
}
/* Denser than the settings-page segmented control: three windows have to fit
   beside a title in the strip AND inside a 268px column in the band. */
/* 30px to match the review card's filter controls (.ms-trigger and the days
   picker) across the band. Pinned as a height rather than tuned via padding so
   it holds in both languages: the buttons stretch to the track and .seg-btn
   centres its own text, so the Arabic line box cannot push it taller. */
.dash-ot-seg { padding: 2px; min-height: 30px; }
.dash-ot-seg .seg-btn {
  padding: 0.1rem 0.38rem;
  font-size: var(--fs-2xs);
  /* Pinned rather than inherited: the Arabic line box is ~8px taller than the
     Latin one at the same size, so an inherited line-height made this control
     37.9px in AR against 26.9px in EN. */
  line-height: 1.25;
}
.dash-ot-seg::before { top: 2px; bottom: 2px; left: 2px; width: calc((100% - 4px) / var(--seg-count, 3)); }
[dir="rtl"] .dash-ot-seg::before { left: auto; right: 2px; }

.dash-ot-zone-figure {
  gap: 0.6rem;
  flex: 0 0 auto;
}
/* Baseline-aligned so the unit sits on the number's baseline, not its box. */
.dash-ot-figure {
  display: flex;
  align-items: baseline;
  gap: 0.25rem;
  flex: 0 0 auto;
}
.dash-ot-value {
  font-size: 1.5rem;
  font-weight: 700;
  line-height: 1.2;
  color: var(--gray-900);
  font-variant-numeric: tabular-nums;
}
.dash-ot-unit { font-size: 0.78rem; color: var(--gray-500); }
/* Neutral until the renderer sets `data-dir`: an unknown trend must not borrow
   the language of a good or bad one. Same colour carries the "flat" state. */
.dash-ot-delta {
  font-size: var(--fs-2xs);
  font-weight: 600;
  color: var(--gray-500);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  /* The leading "+"/"-" is bidi-NEUTRAL, so inside the Arabic card it reordered
     to the far end and "-5.8%" displayed as "5.8%-", which reads as a different
     number. Isolating at a forced LTR direction pins the sign to its digits;
     only the internal order is fixed, never the chip's placement. */
  direction: ltr;
  unicode-bidi: isolate;
}
/* Amber for a rise, green for a fall - the COST framing, restored by product
   2026-08-07 over the plain up-is-good stock-ticker reading that briefly
   replaced it. Overtime is money and burnout, so a spike is a caution the owner
   should feel and a drop is the good news; green on a rise congratulated the
   tenant for a 40% overtime jump. Do not "correct" this back to green-up.
   All three directions must move together with the `data-dir` assignment in
   dashboard.js. "flat" is explicit rather than folded into "up": under the old
   green-up scheme an unchanged "+0%" borrowed the good colour harmlessly, but
   amber would have warned the owner about a period that did not move. */
.dash-ot-delta[data-dir="up"]   { color: var(--warning-text); }
.dash-ot-delta[data-dir="down"] { color: var(--success-text); }
.dash-ot-delta[data-dir="flat"] { color: var(--gray-500); }
.dash-ot-delta[hidden] { display: none; }
.dash-ot-sub {
  font-size: var(--fs-2xs);
  color: var(--gray-500);
  margin: 0;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Contributors are CHIPS, not free text. In the horizontal strip a bare
   "Name 2.7 Name 2.3" leaves each number floating between two names with
   nothing to say which it belongs to; the chip's own background is what binds
   the pair, so the gap INSIDE a chip is deliberately much smaller than the gap
   between chips. That is also why no per-chip unit is needed. */
.dash-ot-top {
  list-style: none;
  margin: 0;
  gap: 0.4rem;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  padding-block: 0;
}
.dash-ot-top li {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
  font-size: 0.75rem;
  min-width: 0;
  padding: 0.15rem 0.45rem;
  border-radius: 6px;
  background: var(--gray-50);
  border: 1px solid var(--gray-100);
}
.dash-ot-name {
  color: var(--gray-700);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dash-ot-hrs {
  color: var(--gray-900);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
}
.dash-ot-empty {
  font-size: 0.75rem;
  color: var(--gray-500);
  background: none;
  border: none;
  padding: 0;
}
/* Disclosure for period-mode employees excluded from the figure. */
.dash-ot-note {
  margin: 0;
  font-size: var(--fs-2xs);
  color: var(--gray-500);
  line-height: 1.35;
  flex: 0 0 auto;
  padding-inline: 0.85rem;
}
.dash-ot-note[hidden] { display: none; }

/* ── Band card layout (the special case) ─────────────────────────
   Only here does the card become the band's last column: `--dash-ot-w` wide
   so the review card keeps absorbing surplus width, and stretched to the band
   height so it sits on the same two edges as the KPI cards beside it and the
   review card after it. Every zone loses its side hairline (they are stacked,
   not side by side) and the contributor list takes a top border instead.

   The width is what makes this layout work, and it is not arbitrary. MEASURED
   intrinsic head widths, forced onto one row: English title 61px + control
   146px, Arabic title 101px + control 120px — 226px in the worse language, so a
   248px card already fits the title and the window control on ONE line. (The
   old 268px column stacked them on the strength of a ~330px estimate that was
   never measured; the Arabic segment labels are the SHORTER pair, not the
   longer.) Un-stacking the head is worth ~24px of the hard 186px height, which
   is exactly one contributor chip — so this card shows all three, and the
   `nth-child(n+3) { display: none }` rule that used to hide the third one is
   gone rather than re-tuned.

   Height budget against 186px, worst language: 17.6 padding + 2 border leaves
   166px for ~30 head + 5 head gap + 42 figure + 3 figure gap + 6 list border and
   padding + 80 for three 24px chips = ~166px. The subtitle stays hidden here
   (see below); restoring it costs ~16px and puts the third chip back at risk,
   so re-measure `lastChipOverflowPx` in BOTH languages if you change anything
   in this block.                                                            */
@media (min-width: 1640px) {
  .dash-overview > .dash-ot-card:not([hidden]) {
    /* KPIs are `order: 0`, the review card `order: 2` — so this lands after
       both of them without depending on DOM order to break a tie. */
    flex: 0 0 var(--dash-ot-w);
    order: 3;
    flex-direction: column;
    align-items: stretch;
    flex-wrap: nowrap;
    gap: 0;
    max-height: var(--dash-band-h);
    margin-top: 0;
    padding: 0.55rem 0.7rem;
  }
  .dash-overview > .dash-ot-card .dash-ot-zone {
    padding-inline: 0;
    border-inline-start: none;
  }
  /* Title and window on one line, pushed to opposite edges — the card is wide
     enough for both in either language (see the measurements above), so the
     head reads as a header rather than as the first two rows of a stack. */
  .dash-overview > .dash-ot-card .dash-ot-zone-head {
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.3rem;
  }
  .dash-overview > .dash-ot-card .dash-ot-seg { flex: 0 0 auto; }
  .dash-overview > .dash-ot-card .dash-ot-value { font-size: 1.35rem; }
  /* A COLUMN here, unlike the strip: the zone holds the figure AND the subtitle,
     the figure is `width: 100%`, and the zone does not wrap — so as a row it
     squeezed the restored subtitle to zero width, and the line rendered its
     full 18px height off the edge of a zone that stayed 26px tall. */
  .dash-overview > .dash-ot-card .dash-ot-zone-figure {
    flex-direction: column;
    align-items: stretch;
    justify-content: flex-start;
    gap: 0;
    /* Separates the headline from the contributor rows. */
    margin-bottom: 0.45rem;
  }
  /* Restored at 246px. It used to be dropped here because its ~16px was what
     the third contributor chip stood on; the taller band pays for both, and the
     line ("N employees with overtime") is what tells the reader whether 13.7hrs
     is one person or the whole company. */
  .dash-overview > .dash-ot-card .dash-ot-sub { display: block; }
  .dash-overview > .dash-ot-card .dash-ot-figure {
    flex-wrap: wrap;
    width: 100%;
  }
  /* PHYSICAL margins, and one of the few places that is correct rather than a
     slip. `.dash-ot-delta` declares `direction: ltr` to keep the leading "-"
     with its digits, and a logical property resolves against the ELEMENT's own
     direction — so `margin-inline-start` was always `margin-left` here and in
     Arabic it pushed the chip toward the line's start (the right) instead of
     its end, parking "-16.5%" in the middle of the card at x=214 in a
     52..328 figure. The `[dir="rtl"]` pair reads the PAGE direction, which is
     the one that decides which edge the chip belongs on. */
  .dash-overview > .dash-ot-card .dash-ot-delta { margin-left: auto; }
  [dir="rtl"] .dash-overview > .dash-ot-card .dash-ot-delta {
    margin-left: 0;
    margin-right: auto;
  }
  /* Takes the band's slack rather than leaving it under the last chip: the
     server caps `top` at 3 (`LIMIT 3` in the overtime-summary query), so the
     list cannot grow into extra height and a top-packed card would just show a
     hole where the fourth chip isn't. CENTRED, not `space-between`: spreading
     three chips over the leftover 140px parked them ~30px apart, which read as
     three unrelated lines rather than a ranked top-three. */
  .dash-overview > .dash-ot-card .dash-ot-top {
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    flex: 1 1 auto;
    gap: 0.35rem;
    overflow: hidden;
    min-height: 0;
    border-top: 1px solid var(--gray-100);
    padding-block-start: 0.45rem;
  }
  /* The chips absorb some of the same slack, so the three of them read as rows
     with weight rather than as a tight cluster floating in a tall card. */
  .dash-overview > .dash-ot-card .dash-ot-top li {
    justify-content: space-between;
    padding: 0.3rem 0.45rem;
  }
  .dash-overview > .dash-ot-card .dash-ot-empty { padding: 0.5rem 0; text-align: center; }
  .dash-overview > .dash-ot-card .dash-ot-note { margin-top: 0.3rem; padding-inline: 0; }
}

/* ── KPI command panel ──────────────────────────────────────────
   The emphasised "present" card, with the secondary cards STACKED in a narrow
   column beside it. Both tracks are capped rather than fractional and the grid
   packs to the inline start; on a wide screen the review card fills the spare
   width beside it (see above).
   Height is set by the primary card's `min-height`; the rail's rows are `1fr`
   with a 0 minimum, so it contributes nothing to the row height and instead
   divides the primary's height between its cards.                          */
/* 568px at the cap (300 + 8 + 260), down from 658 (330 + 8 + 320). The 90px goes
   to the band: it is what buys the overtime card a place BETWEEN these cards and
   the review card from 1640px instead of 1780px, and the review card is still
   58px wider than before at every three-column width (it gives up 90 to the KPIs
   and 32 to the overtime card's extra width, and takes 90 back).
   Both caps are floors of what the cards need, measured rather than trimmed by
   eye: the primary card's row is a 118px ring + a 12px gap + the "27 / 31"
   value, and the rail cards' longest label is "Absences today" / "الغيابات
   اليوم" beside a 24px icon and a two-digit value. The mins came down in the
   same proportion so the two tracks still shrink together below the cap. */
.kpi-command {
  display: grid;
  grid-template-columns: minmax(236px, 300px) minmax(206px, 260px);
  justify-content: start;
  gap: 0.5rem;
  min-width: 0;
}
/* Auto-flow rather than a fixed 3-track template: the "On leave" card only
   exists when the leaves feature is on, and a fixed `repeat(3, …)` would leave
   an empty row on tenants that have it off. */
.kpi-rail {
  display: grid;
  grid-auto-flow: row;
  grid-auto-rows: minmax(0, 1fr);
  gap: 0.5rem;
  min-width: 0;
}
.kpi-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "head value"
    "sub value";
  align-items: center;
  gap: 0.2rem 0.85rem;
  padding: 0.65rem 0.85rem;
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: 8px;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: border-color .18s, box-shadow .18s, transform .18s, background .18s;
  /* No min-height: these stretch to the primary card's height inside
     `.kpi-command`, and a floor here only adds dead space on narrow layouts. */
  min-height: 0;
}
/* No tone stripe. The 3px `::before` bar used to run down the card's inline
   START edge — which mirrors, so in Arabic it sat on the RIGHT. The three
   compact cards had already opted out of it (the coloured icon is their whole
   colour budget), leaving the Present Today card as the only one wearing it,
   where a green rule against a green icon, a green ring and a green caption was
   the fourth green on one surface and read as an alert stripe rather than a
   tone. Removed outright rather than hidden per-card: with `.kpi-card-primary`
   and `.kpi-card-compact` the only two variants in the markup, an override on
   both leaves a base rule nothing can ever reach. */
.kpi-card.kpi-card-static { cursor: default; }
.kpi-card:hover {
  border-color: color-mix(in srgb, var(--primary) 28%, var(--gray-200));
  box-shadow: 0 12px 28px -24px color-mix(in srgb, var(--gray-900) 38%, transparent);
  transform: translateY(-1px);
}
.kpi-card.kpi-card-static:hover {
  transform: none;
  box-shadow: none;
  border-color: var(--gray-200);
}
.kpi-card:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
.kpi-head {
  grid-area: head;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-width: 0;
}
.kpi-icon {
  width: 28px; height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background: var(--gray-100);
  color: var(--gray-500);
  flex-shrink: 0;
}
.kpi-icon-success { background: var(--success-light); color: var(--success); }
.kpi-icon-warning { background: var(--warning-light); color: var(--warning); }
.kpi-icon-danger  { background: var(--danger-light);  color: var(--danger); }
.kpi-icon-info    { background: var(--primary-50);    color: var(--primary); }

.kpi-label {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--gray-600);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.kpi-value {
  grid-area: value;
  font-size: 1.65rem;
  font-weight: 700;
  color: var(--gray-900);
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  display: inline-flex;
  align-items: baseline;
  gap: 0.05rem;
  justify-self: end;
}
.kpi-value-xl {
  font-size: clamp(1.9rem, 2.4vw, 2.5rem);
  justify-self: start;
}
.kpi-divider {
  font-size: 1rem;
  /* gray-400 was 2.54:1 — this slash separates two real numbers. */
  color: var(--gray-500);
  font-weight: 500;
  margin-inline-start: 0.15rem;
}
.kpi-divider bdi { color: var(--gray-500); }
.kpi-sub {
  grid-area: sub;
  font-size: 0.78rem;
  color: var(--gray-500);
  line-height: 1.35;
  min-height: 1.1em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The `-text` ramp step, not the mid-ramp hue: --success is 3.77:1 and --warning
   3.07:1 on the card, both under AA for a 12px caption (--danger squeaked by at
   4.83:1, moved too so the three stay one family). The `-text` tokens are the
   theme-aware "readable on surface" step — light in the dark theme, dark in the
   light one — so this fixes contrast without inverting per theme by hand. */
.kpi-card[data-tone="warning"] .kpi-sub { color: var(--warning-text); }
.kpi-card[data-tone="danger"]  .kpi-sub { color: var(--danger-text); }
.kpi-card[data-tone="success"] .kpi-sub { color: var(--success-text); }

/* This card sets the whole band's height: the rail's rows are `minmax(0, 1fr)`,
   so they contribute nothing and split whatever height this floor asks for.
   180px is the smallest value that leaves the three stacked rail cards a
   comfortable row each (~54px) — lower it and their labels start clipping. */
.kpi-card-primary {
  grid-template-columns: minmax(0, 1fr);
  /* No "meter" row: the horizontal bar under the ring encoded the same 27/31 the
     ring already shows. With two rows left, the second one TAKES the slack (its
     own flex centres the number and ring in it) — `space-between` would instead
     pin them to the card's bottom edge, where the bar used to be. */
  grid-template-areas:
    "top"
    "main";
  grid-template-rows: auto 1fr;
  gap: 0.35rem;
  min-height: var(--dash-band-h, 180px);
  padding: 0.7rem 0.9rem;
  /* Plain surface. The ring, the tone stripe and the status pill already carry
     `--success` here; a fourth green — a wash behind the ring — made the card's
     most saturated element sit on a tinted field instead of a neutral one, which
     reads as leftover gradient rather than intent. It was also a PHYSICAL 135deg
     angle in a layout that mirrors, so in Arabic the tint pooled on the ring's
     own side. Dropping it fixes both; `--surface` is theme-aware, so the dark
     override this used to need is gone too. */
  background: var(--surface);
}
.kpi-primary-top {
  grid-area: top;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  min-width: 0;
}
.kpi-status-pill {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 0 0.6rem;
  border: 1px solid var(--gray-200);
  border-radius: 999px;
  background: color-mix(in srgb, var(--gray-200) 40%, transparent);
  color: var(--gray-600);
  font-size: var(--fs-2xs);
  font-weight: 700;
  white-space: nowrap;
}
.kpi-primary-main {
  grid-area: main;
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}
.kpi-primary-numbers {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}
/* The one `.kpi-sub` in the band with vertical room to spare, so the only one
   that may wrap: this column is ~62px tall inside a 130px row. `nowrap` was
   cutting the English sentence — "27 of 31 employees present today" needs 202px
   and the column offers 169px (199px even before the ring grew, so this was
   already truncating) — for no gain, since nothing sits below it. The compact
   rail cards keep `nowrap`: their rows are 57px and `overflow: hidden` on the
   card would clip a second line rather than make room for it. Clamped at two so
   an unexpectedly long string still cannot grow the card. */
.kpi-card-primary .kpi-sub {
  white-space: normal;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}
/* Sized to FILL the card, because the card's height is not its own to choose:
   the rail's three stat cards need 185px, so the band is 186px and this card is
   stretched to match it. The card's fixed overhead is 56px (padding + the top
   row + the row gap), so a ring smaller than ~120px leaves the difference as
   dead air in the card's lower half — at 88px that was 42px of it, and a card
   whose content stops well short of its own bottom edge reads as a failed
   alignment rather than a deliberate one even though the two columns' bottoms
   match to the sub-pixel. 118px closes the gap to ~12px, i.e. ordinary padding.
   The element carries BOTH `.dash-pulse-ring-wrap` and `.kpi-ring-embed`, and
   the former's 118px default is declared later in this file — so a lone
   `.kpi-ring-embed` rule loses the tie and does nothing. Match on both classes
   so the embedded size actually wins. */
.dash-pulse-ring-wrap.kpi-ring-embed {
  width: 118px;
  height: 118px;
  flex-shrink: 0;
}
.dash-pulse-ring-wrap.kpi-ring-embed .dash-pulse-ring-label { font-size: 1.35rem; }
/* The ring is drawn in a 36-unit viewBox, so `stroke-width` scales with the
   wrapper: at 118px the base 2.8/3.6 render 34% thicker in real pixels than the
   88px version they were tuned on, which turns a fine dial into a fat one.
   Scale them back by 88/118 to hold the drawn thickness where it was — the size
   change is meant to fill the card, not to restyle the ring. */
.kpi-ring-embed .dash-pulse-ring-track { stroke-width: 2.1; }
.kpi-ring-embed .dash-pulse-ring-progress { stroke-width: 2.7; }
/* Below this the primary card drops off its 330px cap toward its 260px floor,
   and the ring stops being free: every pixel it takes comes out of the
   "27 of 31 present today" caption beside it, which is `nowrap` + ellipsis and
   so truncates silently. The band is the same height here, so the dead air
   comes back — that is the honest trade at a width where the card cannot carry
   both. Measured: at 1000px the caption still fits at 118px; at 850px it does
   not (it needs 140px and gets 130px). */
@media (max-width: 1000px) {
  .dash-pulse-ring-wrap.kpi-ring-embed {
    width: 88px;
    height: 88px;
  }
  .dash-pulse-ring-wrap.kpi-ring-embed .dash-pulse-ring-label { font-size: 1.15rem; }
  /* Back to the base widths with the base diameter — the scale-down above only
     exists to cancel the 118px enlargement. (The 560px tier drops to 80px and
     is declared later in this file, so it inherits these too.) */
  .kpi-ring-embed .dash-pulse-ring-track { stroke-width: 2.8; }
  .kpi-ring-embed .dash-pulse-ring-progress { stroke-width: 3.6; }
}
/* Secondary cards: the small coloured icon is the whole colour budget. The
   tinted subtitle made three cards read as three alerts sitting next to a
   fourth. (Their `::before` opt-out is gone with the stripe itself, above.) */
.kpi-card-compact[data-tone="warning"] .kpi-sub,
.kpi-card-compact[data-tone="danger"] .kpi-sub,
.kpi-card-compact[data-tone="success"] .kpi-sub { color: var(--gray-500); }
/* Zero is an empty state — the quietest thing on the row, not a styled metric.
   `.kpi-card-quiet` is set by dashboard.js when the count is 0. */
/* `--gray-500`, not `--gray-400`: gray-400 is 2.54:1 on the card surface, so the
   fade was rendering a real number and its caption below the AA floor. gray-500
   is 4.77:1 and still reads as "faded" next to the normal --gray-800 value. */
.kpi-card-compact.kpi-card-quiet .kpi-icon { background: var(--gray-100); color: var(--gray-500); }
.kpi-card-compact.kpi-card-quiet .kpi-label { color: var(--gray-500); }
.kpi-card-compact.kpi-card-quiet .kpi-value { color: var(--gray-500); font-weight: 600; }
.kpi-card-compact.kpi-card-quiet .kpi-sub { color: var(--gray-500); }
.kpi-card-compact .kpi-value { min-width: 2.2ch; }
/* Stacked in their own column these get the full ~300px for one label, and their
   row height is fixed by the primary card — so a wrapped second line would be
   clipped by the card's `overflow: hidden` rather than given room. Truncate.
   (They used to share a row four-across at ~200px, where wrapping was the only
   way to show "Today's absences" in full.) */
.kpi-card-compact .kpi-label { white-space: nowrap; }
/* Tighter than the four-across card: three of these have to fit inside the
   primary card's height, so the icon and the vertical padding come down. */
.kpi-card-compact {
  padding: 0.4rem 0.8rem;
  gap: 0.1rem 0.75rem;
}
.kpi-card-compact .kpi-icon { width: 24px; height: 24px; }
.kpi-card-compact .kpi-value { font-size: 1.45rem; }
.kpi-card-compact .kpi-sub { font-size: var(--fs-2xs); min-height: 0; }

/* ── Today's pulse ring ───────────────────────────────────────────
   Only the RING survives of the old "Today's pulse" panel: it is embedded in
   the Present Today KPI card via `.kpi-ring-embed`. The panel wrapper and its
   head/title/body/list/dot/summary rules were deleted — nothing carried those
   classes any more. `@keyframes dash-pulse-dot` is NOT part of that set: it
   animates `.dash-hero-status-dot` on the sync chip and is still live. */
.dash-pulse-ring-wrap {
  position: relative;
  width: 100px; height: 100px;
}
.dash-pulse-ring {
  width: 100%; height: 100%;
  transform: rotate(-90deg);
}
.dash-pulse-ring-track {
  stroke: var(--gray-200);
  stroke-width: 2.8;
}
.dash-pulse-ring-progress {
  stroke: var(--primary);
  stroke-width: 3.6;
  transition: stroke-dasharray .8s cubic-bezier(.4,0,.2,1);
}
/* State-dependent ring colour. `data-tone` is set by dashboard.js renderPulse()
   from the attendance percentage; the bands and the reason for each live there.
   `brand` is the 60-79 middle band — deliberately the brand colour and not
   `--gray-300`, which is the no-data tone two lines down and would read as an
   unloaded ring rather than a low-but-normal day. */
.dash-pulse-ring-progress[data-tone="success"] { stroke: var(--success); }
.dash-pulse-ring-progress[data-tone="brand"]   { stroke: var(--primary); }
.dash-pulse-ring-progress[data-tone="warning"] { stroke: var(--warning); }
.dash-pulse-ring-progress[data-tone="danger"]  { stroke: var(--danger); }
.dash-pulse-ring-progress[data-tone="neutral"] { stroke: var(--gray-300); }
.dash-pulse-ring-label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* LTR, in both languages. This is a flex container holding two items — the
     number and the "%" sign — and flex ordering follows `direction`, so under
     `dir="rtl"` the sign was laid out FIRST and the label rendered "%84".
     (A <bdi> around the number cannot fix that: bidi reordering is not what
     places flex items, and isolating the digits actually prevents the "%" from
     joining their run.) The label is a language-neutral numeric readout, so
     pinning it to LTR is the whole fix; `margin-inline-start` on the sign then
     resolves against LTR too and keeps the gap on the correct side. */
  direction: ltr;
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--gray-900);
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.dash-pulse-pct-sign {
  font-size: 0.75rem;
  margin-inline-start: 1px;
  color: var(--gray-500);
  font-weight: 600;
}
/* ── Needs your review ─────────────────────────────────────────── */
.dash-review-card { padding: 0.85rem 1.2rem; margin-bottom: 0 !important; }
/* Emphasis that is CONDITIONAL on there being something to act on.
   `has-warnings` is set by renderWarnings() only when it draws rows, and
   cleared on both empty states (genuine all-clear AND filtered-empty) and while
   the deferred fetch's skeleton is up — so the card is framed in caution colour
   for having work in it, never for being clean. An unconditional tint would put
   the loudest border in the band around "All clear ✓".
   Border only, no wash: this card sits between two others in the band and a
   tinted field behind a list of amber row stripes was the one combination that
   read as an error state rather than a queue. */
.dash-review-card.has-warnings {
  border-color: color-mix(in srgb, var(--warning) 42%, var(--gray-200));
}
.dash-review-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem 1rem;
  margin-bottom: 0.85rem;
}
/* The help sentence moved into the title icon's tooltip, so this row is a single
   line of title + filters. The h3's 16px bottom margin from the generic
   card-heading rule was the gap above that sentence — with the sentence now
   `sr-only` it is dead space, killed by the `margin-bottom: 0` below.
   The row stays top-aligned (the base `flex-start`): the filters are ~34px and
   the title ~19px, so centring parked the title a third of the way down the card
   and re-opened the gap the tightened padding had just closed. */
.dash-review-card .dash-review-head { margin-bottom: 0.55rem; }
/* The size lives here, not on the bare `.dash-review-title` below: this <h3> is
   inside a `.card`, and `.card h3 { font-size: 1rem }` (0,1,1) outranks a lone
   class — the base declaration never applied, which is why this title used to
   render a notch larger than its `.dash-card-head h3` siblings. */
/* 0.95rem against the 0.85rem the overtime card's <h3> and the lower cards'
   heads carry. The band deliberately does NOT read as three equal peers: this
   is the one card that asks the user to DO something, so it gets the band's
   only headline a step up. */
.dash-review-card .dash-review-title { margin-bottom: 0; font-size: 0.95rem; }
.dash-review-title-icon { display: inline-flex; flex-shrink: 0; color: var(--warning); }
/* The header ICON states which of the two things the card is: a queue of work, or a
   clean bill of health. Both icons live in the DOM (see the comment in
   dashboard.html) and this pair of rules picks one, so the swap costs no JS and
   survives a language toggle. The TITLE TEXT is constant in both states — it names
   the card, it does not report on it — so there is no text pair to swap here.
   `is-all-clear` is set by setReviewHeaderState() ONLY on the genuine all-clear —
   never on a filtered-empty list, a failed fetch, or the loading skeleton. Those
   look identical here (an empty list) but are NOT clean bills of health, which is
   the same distinction `.dash-review-empty.is-neutral` below exists to make. */
.dash-review-title-icon.is-clear { display: none; }
.dash-review-card.is-all-clear .dash-review-title-icon.is-warn { display: none; }
.dash-review-card.is-all-clear .dash-review-title-icon.is-clear { display: inline-flex; color: var(--success); }
/* The colour has to be re-stated ON THE SVG, not just on the span above it.
   `.dash-review-title svg` (line ~7860) sets `color: var(--warning)` directly on
   the svg, and the svg's stroke is the `currentColor` presentation attribute — the
   weakest thing in the cascade — so it resolves against the svg's OWN colour. A
   direct declaration beats inherit at any specificity, so setting green on the
   parent span alone painted a green-labelled header with an AMBER check. Caught by
   rendering it; the span's computed colour reported green the whole time. */
.dash-review-card.is-all-clear .dash-review-title-icon.is-clear svg { color: var(--success); }
.dash-review-title-wrap { min-width: 0; flex: 1 1 320px; }
.dash-review-title {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--gray-800);
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0 0 0.2rem;
}
.dash-review-title svg { color: var(--warning); flex-shrink: 0; }
/* Sized off the enlarged title beside it — at 0.7rem it read as a footnote on
   the count that is the card's whole point. */
.dash-review-count { font-size: 0.78rem; font-weight: 700; padding: 0.1rem 0.5rem; }
.dash-review-help {
  margin: 0;
  font-size: 0.78rem;
  color: var(--gray-500);
  line-height: 1.4;
}
.dash-review-filters {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex: 1 1 560px;
  width: min(100%, 680px);
  min-width: 0;
  flex-wrap: nowrap;
  justify-content: flex-end;
  max-width: 680px;
}
.dash-review-filters > * {
  min-width: 0 !important;
}
#warnings-employee-ms {
  flex: 2 1 180px !important;
  max-width: 260px !important;
}
#warnings-department-ms {
  flex: 2 1 160px !important;
  max-width: 230px !important;
}
#warnings-period-picker {
  flex: 1 1 120px !important;
  min-width: 115px !important;
  max-width: 150px !important;
}
.dash-review-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  max-height: 420px;
  overflow-y: auto;
}
.dash-review-item {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.6rem 0.75rem;
  background: var(--surface);
  border: 1px solid var(--gray-200);
  border-radius: 10px;
  cursor: pointer;
  transition: border-color .15s, background .15s;
}
.dash-review-item:hover {
  border-color: var(--gray-300);
  background: color-mix(in srgb, var(--primary) 4%, var(--surface));
}
.dash-review-item:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}
.dash-review-sev {
  width: 4px;
  align-self: stretch;
  border-radius: 4px;
  background: var(--warning);
  flex-shrink: 0;
}
.dash-review-item[data-severity="danger"] .dash-review-sev { background: var(--danger); }
.dash-review-item[data-severity="warning"] .dash-review-sev { background: var(--warning); }
.dash-review-body { flex: 1 1 auto; min-width: 0; }
.dash-review-row1 {
  font-size: 0.88rem;
  font-weight: 600;
  color: var(--gray-800);
  margin-bottom: 0.1rem;
}
.dash-review-issue {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dash-review-row2 {
  font-size: 0.76rem;
  color: var(--gray-500);
  display: flex;
  align-items: center;
  gap: 0.2rem;
  flex-wrap: wrap;
}
.dash-review-meta { color: var(--gray-300); }
.dash-review-emp { font-weight: 500; color: var(--gray-600); }
.dash-review-cta {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.3rem 0.65rem;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--primary);
  background: color-mix(in srgb, var(--primary) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--primary) 22%, transparent);
  border-radius: 7px;
  flex-shrink: 0;
  white-space: nowrap;
  transition: background .15s, border-color .15s;
}
.dash-review-item:hover .dash-review-cta {
  background: color-mix(in srgb, var(--primary) 14%, transparent);
  border-color: var(--primary);
}
[dir="rtl"] .dash-review-cta svg { transform: scaleX(-1); }

/* ── Dashboard's dense review list ────────────────────────────────
   Scoped to `.dash-review-card` on purpose: the attendance page reuses the same
   row classes in a narrow sidebar card (`#raw-warnings-card`), where one line
   would be mostly ellipsis — it keeps the two-line row.
   The old row was 72px tall to show ~157px of text in a 1127px-wide body: two
   lines, a 10px-radius mini-card each, and a labelled button that set the
   height. One line + a flat divider list + an icon-only affordance is ~38px. */
.dash-review-card .dash-review-list {
  gap: 0;
  /* Whole multiple of the row height — a half-visible row reads as a glitch. */
  max-height: 380px;
  /* Scrollbar side, pinned to the PHYSICAL right in both languages so this card
     matches the dashboard's other scrolling cards. A scroll container puts its
     scrollbar on its own inline end, and this list is the only one on the page
     that inherited the page direction — so in Arabic its bar sat on the left
     while Today's Punches and Away Today (`.dash-recent-table-wrap`, forced
     `ltr` for exactly this reason) kept theirs on the right, and the band read
     as two cards scrolling one way and one the other.
     Forcing `ltr` here would reverse the ROWS too, so each item is handed the
     page direction back below. Same shape as `.dash-recent-table-wrap` and its
     `[dir="rtl"] … .dash-recent-table` restore; the attendance page's
     `#raw-warnings-card` reuses these row classes but not this one, and keeps
     its own (id-specific, higher-specificity) copy of the trick. */
  direction: ltr;
}
/* Every child, not just `.dash-review-item`: the all-clear/filtered-empty box
   and the loading skeleton are `<li>`s of the same list, and an Arabic
   "All clear" sentence left in an `ltr` box renders with its punctuation on
   the wrong end. */
[dir="rtl"] .dash-review-card .dash-review-list > li { direction: rtl; }
.dash-review-card .dash-review-item {
  min-height: 38px;
  /* The list is a fixed-height flex column, so its children are shrinkable by
     default — 35 rows in 380px squashed each one to 16px until this floor and
     `flex-shrink: 0` stopped it. They scroll; they never shrink. */
  flex-shrink: 0;
  padding: 0.25rem 0.5rem;
  gap: 0.55rem;
  border-radius: 0;
  /* Divider list, not 35 stacked cards. The side borders stay (transparent) so
     the `warning-card--active` highlight's `border-color` still shows. */
  border-color: transparent;
  border-bottom-color: var(--gray-200);
}
.dash-review-card .dash-review-item:last-child { border-bottom-color: transparent; }
/* FILLS the list rather than floating in the middle of it. The list is already a
   flex column stretched to whatever the header leaves (see the band tier above),
   so one `flex-grow` on the box absorbs that surplus — no `:has()` and no state
   class, because with rows present the box is not in the DOM and with rows absent
   it is the list's only child. It used to take `margin-block: auto` instead, which
   centred it and left the card reading as a mostly-empty panel with a green strip
   through the middle. Where the list is content-sized (the narrow stacked tier)
   there is no surplus to absorb and the box keeps its natural height, so this is
   only ever "fill what is there". */
.dash-review-card .dash-review-empty {
  flex: 1 1 auto;
  min-height: 0;
  margin-block: 0;
}
.dash-review-card .dash-review-sev { width: 3px; }
/* One severity colour for the whole card. The header already reads as amber (the
   warning triangle + the `badge-warning` count), so a red row stripe next to them
   claimed a second severity level the card never acts on — every row opens the
   same review flow. Scoped to `.dash-review-card`, so the attendance page's
   `#raw-warnings-card` keeps its own red/amber split. */
.dash-review-card .dash-review-item[data-severity="danger"] .dash-review-sev {
  background: var(--warning);
}
/* One line: employee · issue · date · weekday.
   The EMPLOYEE leads, unlike the attendance page's card. This list is a triage
   entry point - you are deciding who needs attention and then clicking through
   - and a non-technical HR manager reads a name with no domain knowledge while
   "Consecutive Check-ins" needs some. On the attendance page the name is
   context the user already filtered by, so the issue stays the headline there.
   Nothing is lost by demoting it: this card deliberately shows ONE severity
   colour (see the rule above), so the issue text was not carrying urgency
   either, only identity - and the grouped feed now emits one row per physical
   mistake rather than one per detection, so there is less for it to
   disambiguate. */
.dash-review-card .dash-review-body {
  display: flex;
  align-items: baseline;
  gap: 0.3rem;
  white-space: nowrap;
  overflow: hidden;
}
.dash-review-card .dash-review-emp {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--gray-800);
  /* Shrinks LAST: the name is the row's headline, and a clipped name is the one
     thing on this row the reader cannot reconstruct. Names vary far more in
     rendered width than the issue labels do, and an Arabic name runs wider than
     a Latin one at the same size. */
  flex: 0 1 auto;
  flex-shrink: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dash-review-card .dash-review-issue {
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--gray-600);
  /* Shrinks FIRST: the longest and most compressible span in the row, and the
     one whose truncation still leaves a usable row. */
  flex: 0 1 auto;
  flex-shrink: 4;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dash-review-card .dash-review-date,
.dash-review-card .dash-review-last {
  font-size: 0.78rem;
  color: var(--gray-500);
  flex: 0 0 auto;
}
.dash-review-card .dash-review-meta { flex: 0 0 auto; }
/* Icon only. The whole row is a `role="link"` with its own tooltip, so the
   label was pure repetition — and it was what set the row height. */
.dash-review-card .dash-review-cta {
  width: 26px;
  height: 26px;
  padding: 0;
  justify-content: center;
  /* Neutral ghost, matching #raw-warnings-card's CTA on the attendance page:
     the row itself is the link, so the arrow is an affordance rather than the
     page's one primary action, and --primary here competed with the KPI band.
     Deliberately NO [data-theme="dark"] block - the --gray-* scale inverts
     between themes, so these four tokens already resolve to a mid-grey glyph on
     a subtle chip in both. (The attendance card DOES carry a dark override, but
     its values pin light-theme hex fallbacks against the inverted scale and
     land on a near-white hover chip; not something to copy across.) */
  color: var(--gray-500);
  background: transparent;
  border-color: var(--gray-200);
}
.dash-review-card .dash-review-item:hover .dash-review-cta {
  background: var(--gray-50);
  border-color: var(--gray-300);
  color: var(--gray-700);
}
/* Solid, not dashed. A dashed edge reads as a PLACEHOLDER — an empty slot waiting
   to be filled, or a drop target — which is the opposite of what this panel says:
   the check ran and found nothing. A soft filled panel with a quiet solid edge
   reads as a settled result. Both tints are `color-mix` over `--success`, so the
   panel follows the token in either theme instead of pinning a literal green. */
.dash-review-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 1.5rem 0.75rem;
  background: color-mix(in srgb, var(--success) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--success) 22%, transparent);
  border-radius: 10px;
  color: var(--success);
  font-size: 0.85rem;
  font-weight: 500;
  text-align: center;
  line-height: 1.45;
}
/* An empty list is only GOOD NEWS when it is empty because nothing is wrong. The
   success green above says "all clear ✓" on its own, louder than the sentence
   inside it, so the two states that are NOT all-clear - a filter hiding every
   warning, and a failed fetch - must drop it or the copy fixing the claim is
   overruled by the colour making it. Neutral grey, not danger red: neither state
   is itself a problem to act on, it just is not a clean bill of health.
   Only the three colours are restated - the solid edge above is shared, so this
   variant is the same panel in grey rather than a differently-shaped one. */
.dash-review-empty.is-neutral {
  background: var(--gray-50);
  border-color: var(--gray-300);
  color: var(--gray-600);
}

/* "Nothing lit up" note on the ACTIVE warning card (attendance page only).
   Appended by updateWarningCardHighlightCount when a click highlighted no row,
   so the user can tell a working feature from a broken one. Deliberately quiet -
   it reports a state, it is not a problem to act on - and it wraps, because the
   sentence is longer than the meta row and must not force the card to scroll. */
.dash-review-nohighlight {
  margin-block-start: 0.2rem;
  font-size: var(--fs-2xs);
  line-height: 1.35;
  color: var(--gray-500);
  white-space: normal;
}

/* ── Filter-transparency bar (warnings cards, both pages) ──────────
   Sits between the card header and the list, stating how many rows the active
   filters are withholding and carrying the one-click way out. Shown by
   renderFilterNote() in api.js only while something is actually hidden. */
.warnings-filter-note {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.35rem 0.6rem;
  margin-bottom: 0.5rem;
  padding: 0.4rem 0.6rem;
  border: 1px solid color-mix(in srgb, var(--primary) 25%, transparent);
  border-radius: 8px;
  background: color-mix(in srgb, var(--primary) 7%, transparent);
  font-size: 0.78rem;
  line-height: 1.35;
}
/* .warnings-filter-note sets display:flex, which outranks the UA [hidden] rule.
   Without this the bar can never be hidden. */
.warnings-filter-note[hidden] { display: none; }
.warnings-filter-note-text {
  color: var(--gray-700);
  font-weight: 500;
  min-width: 0;
}
/* A link-style button, not a .btn: it is a secondary escape hatch inside an
   advisory strip, and a filled button here would compete with the "Fix" CTA on
   every row below it. */
.warnings-filter-note-clear {
  flex-shrink: 0;
  padding: 0.1rem 0.2rem;
  border: none;
  border-radius: 5px;
  background: transparent;
  color: var(--primary);
  font-family: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.warnings-filter-note-clear:hover {
  background: color-mix(in srgb, var(--primary) 12%, transparent);
}
.warnings-filter-note-clear:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}

/* ── Secondary grid: recent + absent ──────────────────────────── */
.dash-secondary-grid {
  display: grid;
  /* Not 2:1 — the recent-punches table had slack at every width while Away Today
     was forced into a horizontal scrollbar.
     The first track carries a REAL minimum (31rem = the recent table's 405px of
     intrinsic width — it dropped from 547px when the provenance pills became
     glyphs — plus the card's 80px of padding, with a little slack) instead of a bare ratio, so the
     ratio can favour Away Today without ever squeezing recent into a scrollbar:
     whichever is larger of the two wins per width. */
  grid-template-columns: minmax(31rem, 1.35fr) minmax(0, 1fr);
  gap: 0.65rem;
  margin-bottom: 0;
  align-items: stretch;
}
.dash-secondary-grid > .card {
  margin-bottom: 0 !important;
  min-height: 0;
  /* Tighter than the generic `.card` 1.5rem: at 24px these two were the only
     cards on the page whose title floated well clear of the top edge, against
     ~14px on the review card beside them. 1.2rem inline matches the review card
     and the sync log, so the whole lower stack shares one padding rhythm. */
  padding: 0.85rem 1.2rem;
}
.dash-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.85rem;
}
.dash-card-head h3 {
  margin: 0;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--gray-800);
}
.dash-card-sub { font-size: 0.78rem; color: var(--gray-500); }

.dash-recent-card,
.dash-absent-card {
  display: flex;
  flex-direction: column;
}
/* Away Today card is a scroll target for the Absences KPI — it takes focus
   programmatically, so suppress the default outline and use the flash instead. */
.dash-absent-card:focus { outline: none; }
.dash-absent-card.dash-card--flash {
  animation: dash-card-flash 900ms ease-out;
}
@keyframes dash-card-flash {
  0%   { box-shadow: 0 0 0 3px var(--primary); }
  100% { box-shadow: 0 0 0 3px transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .dash-absent-card.dash-card--flash { animation: none; }
}
.dash-recent-table-wrap {
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  flex: 1;
  /* The only height knob for both lower cards (Recent Punches, Away Today) -
     there is no responsive override, so this one value sets the band at every
     width. Each card is content-sized until it hits this cap, so in practice
     this trims whichever list is long enough to scroll. 17rem still shows 8
     rows under the sticky header, so the list still reads as a list. */
  max-height: 17rem;
  scrollbar-gutter: stable both-edges;
  direction: ltr;
}
.dash-recent-scroll .dash-recent-table {
  min-width: 34rem;
}
/* Away Today: three columns that have to fit a ~1/3-width card — Name, the
   reason badge, and "Last present". (The Shift column was dropped: it repeated
   the same value down the whole list for the single-shift tenants that are the
   common case, and it was the column the other three were squeezing.)
   `table-layout: fixed` is what lets the name cell ellipsize — in auto layout a
   cell's max-width is ignored, so the row just grew and the wrap scrolled. The
   renderer mirrors the name into `title`, so the full value is one hover away. */
.dash-away-scroll .dash-recent-table {
  /* The floor has to hold all three at a legible width; below it the wrap
     scrolls (which `resetRecentTableScroll` already handles in both directions)
     rather than crushing a column to nothing. */
  min-width: 18rem;
  table-layout: fixed;
}
/* Name takes the remainder (no width of its own) — it is the only column here
   whose content has no bound, so it is the one that should absorb slack and, at
   the narrowest widths, truncate. */
/* Fixed, not a share of the width: this badge carries the REASON someone is
   away, and a truncated pill is the worst thing to read here. Every ordinary
   label (Absent, Annual leave, a holiday name) needs ~78px against the 97.6px
   this leaves inside the cell padding. */
.dash-away-scroll .dash-recent-table th:nth-child(2),
.dash-away-scroll .dash-recent-table td:nth-child(2) { width: 7rem; }
/* Also fixed: the phrase is short and bounded ("Yesterday" / "12 days ago" /
   "منذ 12 أيام"), so it costs a known amount and giving it a share of the width
   would only make it wider than it needs to be. Sized to the widest thing in the
   column, which is the English HEADER (~98px) rather than any value. */
.dash-away-scroll .dash-recent-table th:nth-child(3),
.dash-away-scroll .dash-recent-table td:nth-child(3) { width: 7.5rem; }
.dash-away-scroll .dash-recent-table th,
.dash-away-scroll .dash-recent-table td {
  /* Inline only — the block padding is the shared row rhythm and belongs to the
     `.dash-recent-table` rule alone, or these two tables drift apart again. */
  padding-inline: 0.45rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Keep the pill's shape and let its label truncate instead: a clipped pill
   reads as a rendering fault, a clipped word does not. */
.dash-away-scroll .dash-recent-table .badge {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: middle;
}
.dash-recent-table {
  width: 100%;
  border: none;
  border-radius: 0;
  box-shadow: none;
  background: transparent;
  min-width: 0;
}
/* ONE row-height rhythm for both lower cards. Recent Punches and Away Today sit
   side by side, so their rows have to step at the same pitch — they used to run
   36px and 34px because each table set its own block padding. The denser value
   wins (these are at-a-glance lists, not data grids). Only the INLINE padding is
   still per-table: Away Today is a third of the width with three fixed columns.
   Keep the block padding here and nowhere else. */
.dash-recent-table th,
.dash-recent-table td {
  padding: 0.34rem 0.6rem;
  border-bottom: 1px solid var(--gray-100);
  white-space: nowrap;
}
/* Equal padding is not yet equal rows: a baseline-aligned inline-block sits its
   own descent BELOW the row's baseline, so the punch-type/flag pills made the
   Recent Punches line box ~0.3px taller than the Away Today badges, which are
   already `vertical-align: middle`. Centre the pills on the same axis so the two
   tables land on the same row height rather than merely the same padding. */
.dash-recent-table .flag-pill { vertical-align: middle; }
.dash-recent-table thead th {
  background: var(--surface);
  font-size: var(--fs-2xs);
  color: var(--gray-500);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-bottom: 1px solid var(--gray-200);
  position: sticky;
  top: 0;
  z-index: 1;
}
/* Same reasoning as `[dir="rtl"] th`, repeated because this rule (0,2,1)
   outranks it (0,1,1) and would otherwise keep the Arabic header at 600. */
[dir="rtl"] .dash-recent-table thead th {
  font-weight: 700;
  color: var(--gray-600);
}
.dash-recent-table tbody tr:nth-child(even) td { background: transparent; }
.dash-recent-table tbody tr:hover td {
  background: color-mix(in srgb, var(--primary) 4%, transparent);
}
.dash-recent-table tbody tr:last-child td { border-bottom: none; }
.dash-recent-table .dash-cell-name { font-weight: 500; color: var(--gray-800); }
/* Provenance marks (added manually / auto-filled / edited): metadata, so a muted
   inline glyph with a tooltip. The pills in this table are reserved for punch
   type and flags — three filled pills per row read as three alerts. */
.dash-recent-table .dash-cell-mark {
  display: inline-flex;
  align-items: center;
  margin-inline-start: 0.3rem;
  color: var(--gray-400);
  vertical-align: middle;
  flex-shrink: 0;
}
.dash-recent-table .dash-cell-mark svg { display: block; }
.dash-recent-table tbody tr:hover .dash-cell-mark { color: var(--gray-600); }
.dash-recent-table .dash-cell-time { color: var(--gray-600); font-variant-numeric: tabular-nums; }
/* Tabular figures for the same reason as the time column: this is a numeric
   readout stacked down a column, and proportional digits make the numbers in
   "9 days ago" / "12 days ago" step out of line row to row. */
.dash-recent-table .dash-cell-lastseen { color: var(--gray-600); font-variant-numeric: tabular-nums; }
/* ONE alignment for every column of both cards, header and cell together, so a
   value always sits under its own label. `start`, not `center`: these columns
   are much wider than their content (the 100%-width table hands its surplus to
   whichever column is widest), so centring left a hole between the content and
   the column edge - measured at 95px on the Arabic name column, a 58px name
   adrift in a 248px column, which read as dead space at the card's right edge.
   Start-alignment pins every column to the reading edge and the surplus
   collects between the columns, where it is invisible. `start` and not a
   direction pair: it resolves to the left in English and the right in Arabic
   with no RTL mirror to keep in sync.

   The `.dash-recent-table-wrap` prefix is REQUIRED, not decoration: the global
   `[dir="rtl"] table th, [dir="rtl"] table td { text-align: right }` near the top
   of this file is specificity 0,1,2 and out-ranks a bare `.dash-recent-table td`
   (0,1,1), so without the second class this rule applies in English and is
   silently dropped in Arabic - which is exactly how the old
   `.dash-recent-flag { text-align: end }` (0,2,0) got away with it. Do not
   simplify the selector back. */
.dash-recent-table-wrap .dash-recent-table th,
.dash-recent-table-wrap .dash-recent-table td { text-align: start; }
[dir="rtl"] .dash-recent-table-wrap .dash-recent-table {
  direction: rtl;
}
/* No RTL first-column inset. The extra `padding-inline-start: 1.35rem` that used
   to sit here kept the START-ALIGNED name text off the card edge; now that every
   column is centred, an inset on one side only is pure skew - it pulled the name
   6px off its column axis while its three neighbours sat dead centre. The wrap's
   `scrollbar-gutter: stable both-edges` already reserves the edge space. */

.dash-absent-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  max-height: 300px;
  overflow-y: auto;
}
.dash-absent-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.34rem 0.25rem;
  border-bottom: 1px solid var(--gray-100);
  font-size: 0.85rem;
}
.dash-absent-item:last-child { border-bottom: none; }
.dash-absent-name { font-weight: 500; color: var(--gray-800); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-absent-shift { font-size: 0.78rem; color: var(--gray-500); flex-shrink: 0; }
.dash-absent-meta {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
}
.dash-absent-badge { margin-inline-end: 0; }
.dash-absent-more { padding: 0.5rem 0.25rem; font-size: 0.78rem; color: var(--gray-500); }
.dash-card-head-right {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
}
/* The cell keeps its default `table-cell` display ON PURPOSE. `display: flex`
   here takes the <td> out of the table formatting context, which silently
   voids its `colspan` - the cell collapsed to the FIRST column's width (65px
   of a 409px table) and `.dash-recent-table-wrap`'s overflow clipped the
   message to "ne is at wo" in every language. The flex box that centres the
   icon beside the text belongs on the inner span, not on the cell. */
.dash-empty {
  padding: 1rem 0.75rem;
  font-size: 0.85rem;
  color: var(--gray-500);
  text-align: center;
}
.dash-empty-inner {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
}
.dash-empty-success { color: var(--success); }

/* ── Sync log (collapsed) ─────────────────────────────────────── */
.dash-sync-log { padding: 0.5rem 1.2rem; margin-bottom: 0 !important; }
.dash-sync-log > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  /* Top-aligned, not centred: the Sync Now / auto-sync buttons set this row's
     height, and centring the much shorter title against them left it sitting
     well clear of the card's top edge. */
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.2rem 0;
}
.dash-sync-log > summary::-webkit-details-marker { display: none; }
.dash-sync-log > summary::after { display: none; }
.dash-sync-log-title-group {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-weight: 600;
  font-size: 0.85rem;
  color: var(--gray-700);
  min-width: 0;
}
.dash-sync-log-chevron {
  color: var(--gray-400);
  flex-shrink: 0;
  transition: transform .2s;
}
.dash-sync-log[open] .dash-sync-log-chevron { transform: rotate(180deg); }
.dash-sync-log-actions {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-shrink: 0;
}
/* Size the auto-sync picker to match the manual sync button */
#dash-sync-picker {
  height: 34px;
  padding: 0 0.6rem 0 0.6rem;
  border-radius: 8px;
  font-size: 0.82rem;
}
#dash-sync-picker .sync-interval-trigger {
  font-size: 0.82rem;
  padding: 0;
  height: auto;
}
.dash-sync-now-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  height: 34px;
  padding: 0 0.75rem;
  background: transparent;
  color: var(--gray-600);
  border: 1px solid var(--gray-200);
  border-radius: 8px;
  font-size: 0.82rem;
  font-weight: 500;
  cursor: pointer;
  transition: border-color .15s, color .15s, background .15s, transform .1s;
  white-space: nowrap;
}
.dash-sync-now-btn:hover {
  background: var(--surface);
  border-color: var(--primary);
  color: var(--primary);
}
.dash-sync-now-btn:active { transform: scale(0.95); }
.dash-sync-now-btn:disabled { opacity: 0.55; cursor: not-allowed; }
/* No colour per state. Toggling auto-sync is reversible and harmless in both
   directions, so neither half earns `--danger` or `--success` — the LABEL
   already names the action ("Turn on" / "Stop"), and `aria-pressed` carries the
   state for assistive tech. Left as-is, a red outline on "Stop" made a scheduler
   toggle look like the loudest control in the footer, and it was the last red on
   the dashboard that was not an actual absence.
   `data-on` itself STAYS — dashboard.js reads it as the toggle's state. */
.dash-sync-log .sync-status-bar { font-weight: 400; font-size: 0.78rem; }
/* Every column of the sync-history table is a count, a duration or an age, so
   the whole body takes tabular figures rather than a class per cell (the rows
   are built in dashboard.js loadSyncHistory() and carry none).
   `font-variant-numeric` only affects digits, so the one word-valued column —
   the status badge — is untouched. */
.dash-sync-log tbody td { font-variant-numeric: tabular-nums; }
.dash-sync-result { font-size: 0.8rem; color: var(--gray-600); margin-top: 0.4rem; }
.dash-sync-result:empty { display: none; }
/* The failure line carries the device's own error text ("Connect failed:
   Connection to 192.168.1.201:4370 timed out"), not a one-word status, so the
   badge default `white-space: nowrap` made it wider than a phone viewport and
   gave the whole dashboard a horizontal scroll. Let it wrap, and break inside
   the unspaced host:port token so a narrow screen has somewhere to break. */
.dash-sync-result .badge {
  display: inline-block;
  max-width: 100%;
  white-space: normal;
  overflow-wrap: anywhere;
  text-align: start;
  padding: 0.3rem 0.65rem;
  border-radius: var(--radius);
  line-height: 1.45;
}
/* The device's own diagnostic, kept for whoever debugs the device remotely.
   `dir="ltr"` on the <code> keeps the IP:port readable inside an RTL page; the
   summary itself stays in the page direction. */
.dash-sync-details { margin-top: 0.3rem; font-size: var(--fs-2xs); }
.dash-sync-details > summary {
  cursor: pointer;
  color: var(--gray-500);
  user-select: none;
}
.dash-sync-details > code {
  display: block;
  margin-top: 0.25rem;
  padding: 0.35rem 0.5rem;
  border-radius: var(--radius);
  background: var(--gray-100);
  color: var(--gray-600);
  font-size: var(--fs-2xs);
  overflow-wrap: anywhere;
  text-align: start;
}

/* ── Responsive ───────────────────────────────────────────────── */
/* Below 1400px Away Today is down to ~330px, where
   even the type pill truncates. Stack them full-width instead: no scrollbar and
   no truncation, at the cost of one card's height. */
@media (max-width: 1399px) {
  .dash-secondary-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}
@media (max-width: 768px) {
  /* TWO cards on a phone, not one. The greeting and the quick actions already
     stack here, and sharing a surface made the buttons read as part of the
     header rather than as their own group. So the hero itself drops its card
     chrome and becomes a bare wrapping-row container, and each half takes the
     chrome instead — same tokens, so dark theme needs no override (see the
     `.dash-hero` note at the bottom of this file).
     The chrome goes on `.dash-hero-qa`, NOT on its `.dash-hero-side` wrapper:
     dashboard.js hides `#hero-quick-actions` outright for team-lead ESS
     accounts, and the wrapper survives that `display:none` — it would render
     as an empty card. Keep it a wrapping ROW, never `flex-direction: column`:
     `.dash-hero-main`'s `flex: 1 1 100%` basis would then apply to its HEIGHT. */
  .dash-hero {
    padding: 0;
    gap: 0.6rem;
    background: none;
    border: none;
    border-radius: 0;
    box-shadow: none;
    /* The desktop `overflow: hidden` would clip each inner card's shadow. */
    overflow: visible;
  }
  .dash-hero-main {
    flex: 1 1 100%;
    padding: 0.8rem 0.95rem;
    background: var(--surface);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
  }
  .dash-hero-side {
    width: 100%;
    justify-content: flex-start;
    /* The desktop `min-width: min-content` keeps the chips on the greeting's
       line; here it is measured against the WIDEST chip (and, once this becomes
       a column at 560, against the status chip's whole sentence), which forced
       the row ~45px past the hero's own width. */
    min-width: 0;
  }
  /* Full-width card of its own: the chips and the status chip no longer compete
     for one line, so neither is squeezed to an ellipsis. */
  .dash-hero-qa {
    margin-inline-end: 0;
    width: 100%;
    padding: 0.7rem 0.8rem;
    background: var(--surface);
    border: 1px solid var(--gray-200);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
  }
  .dash-hero-qa-btn { flex: 1 1 auto; justify-content: center; }
  .dash-hero-status {
    flex: 1 1 auto;
    justify-content: center;
  }
  .dash-hero-greet h2 { font-size: 1rem; }
  .dash-review-head {
    flex-direction: column;
    align-items: stretch;
  }
  /* In the column layout, flex-basis applies to the main axis (height), so the
     desktop `flex: 1 1 320px/560px` bases would force the title-wrap and filters
     to huge heights. Reset them to content-sized. */
  .dash-review-title-wrap {
    flex: 0 0 auto;
  }
  .dash-review-filters {
    flex: 0 0 auto;
    width: 100%;
    justify-content: flex-start;
    flex-wrap: wrap;
    max-width: none;
  }
  .dash-review-filters > * {
    min-width: 0;
  }
  /* Two capped tracks no longer fit side by side: the rail drops under the
     primary card, which then sizes to its own content. */
  .kpi-command { grid-template-columns: minmax(0, 1fr); }
  .kpi-card-primary { min-height: 0; }
  .kpi-card-compact { padding: 0.55rem 0.8rem; }
  #warnings-employee-ms { min-width: 0 !important; max-width: none !important; flex: 2 1 140px !important; }
  #warnings-department-ms { min-width: 0 !important; max-width: none !important; flex: 2 1 140px !important; }
  #warnings-period-picker { flex: 1 1 100px !important; max-width: 130px !important; }
  /* Anchor the two filter panels to the filter ROW, not to their own trigger.
     `.ms-dropdown` is `left:0;right:0` plus a min-width floor, which is
     over-constrained, so the browser drops the inline-END offset and the panel
     grows away from its inline-start edge (leftward in RTL, rightward in LTR).
     On a phone the two triggers are ~145px inside a ~297px row, so the
     department panel's 210px floor starts too close to the page's inline-end
     edge and overhangs the PAGE by up to 33px at 360px — which makes the whole
     document horizontally scrollable. The base rule's `max-width: 100vw - …`
     caps the panel's WIDTH, never its position, so it cannot prevent that, and
     the overhang is what let the Android shell's overview zoom-out expose the
     closed off-canvas drawer parked just outside the layout viewport.
     Dropping the floor and pinning both inline edges to the row keeps the panel
     inside the card in BOTH directions, whatever the control count or order —
     and at ~297px it is wider than the 210/260px floors it replaces. */
  #warnings-card .dash-review-filters { position: relative; }
  #warnings-card .dash-review-filters > .ms-container { position: static; }
  #warnings-card .dash-review-filters .ms-dropdown {
    inset-inline: 0;
    min-width: 0;
  }
}
@media (max-width: 560px) {
  /* Phone reading order: Today's Punches → Away Today → Punch Alerts.
     `#warnings-card` is a child of `.dash-overview` while the two secondary
     cards are inside `.dash-lower`, and `order` only sorts SIBLINGS — so both
     wrappers drop to `display: contents` here, which promotes their children to
     direct flex items of the page column and puts all five in one sort space.
     Selectors still match the DOM through a `contents` box, which is why the
     margin reset below has to name each wrapper's children explicitly rather
     than relying on `.main-content > *`. Desktop is untouched: above 560px the
     wrappers keep their boxes and their own DOM order. */
  .main-content[data-page="dashboard"] {
    display: flex;
    flex-direction: column;
    gap: 0.65rem;
  }
  .dash-overview,
  .dash-lower { display: contents; }
  /* One rhythm, from the column gap. The wrappers' own bottom margins went with
     their boxes, and the per-card ones that survive would now stack on top of
     the gap instead of being it. */
  .main-content[data-page="dashboard"] > *,
  .dash-overview > *,
  .dash-lower > * { margin-top: 0; margin-bottom: 0; }
  /* Both carry a stacked-layout `margin-top` at a higher specificity than the
     reset above, and both are inside the reordered column now. */
  .dash-overview > .dash-review-card,
  .dash-overview > .dash-ot-card { margin-top: 0; }
  .dash-secondary-grid { order: 1; }
  #warnings-card { order: 2; }
  #sync-log-card { order: 3; }

  .kpi-rail { gap: 0.45rem; }
  .kpi-card {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "head value"
      "sub value";
    padding: 0.55rem 0.75rem;
    gap: 0.1rem 0.75rem;
    align-items: center;
  }
  .kpi-card .kpi-icon { width: 24px; height: 24px; }
  .kpi-card .kpi-label { font-size: 0.78rem; }
  .kpi-card .kpi-sub { font-size: var(--fs-2xs); min-height: 0; }
  /* The `.kpi-card` rule above re-declares grid areas for the compact cards, and
     the primary card is also a `.kpi-card` — so it has to restate its own areas
     here or its children reference names that no longer exist, get auto-placed
     into the same cells, and render on top of each other. */
  .kpi-card-primary {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "top"
      "main";
    /* Tighter block padding and row gap than the rail cards get: this card is
       the tallest thing on the phone dashboard and the ring already guarantees
       it clears a comfortable minimum, so the vertical padding was pure slack.
       Inline padding is unchanged — it aligns with the rail cards below. */
    padding: 0.6rem 0.9rem;
    gap: 0.3rem;
  }
  /* The ring sets the primary card's row height, so shrinking it is what
     actually compacts the card. */
  .kpi-card-primary .kpi-primary-main { gap: 0.5rem; }
  .kpi-value { justify-self: end; font-size: 1.55rem; }
  /* Roomier than the desktop clamp: on one card per row the number is the only
     thing competing for the width. */
  .kpi-value-xl { justify-self: start; font-size: 2.1rem; }
  .kpi-status-pill { display: none; }
  .dash-hero-avatar { width: 38px; height: 38px; }
  .dash-hero-avatar.has-logo { width: 44px; height: 44px; }
  .dash-hero-greet h2 { font-size: 0.95rem; }
  .dash-hero-meta { font-size: 0.78rem; gap: 0.3rem; }
  .dash-hero-side { flex-direction: column; align-items: stretch; }
  /* Column layout: the chips get their own stacked row, full width. */
  .dash-hero-qa { width: 100%; }
  .dash-hero-status, .dash-hero-controls { width: 100%; justify-content: center; }
  .dash-review-cta {
    padding: 0.25rem 0.5rem;
    font-size: var(--fs-2xs);
  }
  /* Label-only chip on the attendance card; the dashboard's is icon-only, so
     hiding its svg would leave an empty box. */
  .dash-review-cta svg { display: none; }
  .dash-review-card .dash-review-cta { padding: 0; }
  .dash-review-card .dash-review-cta svg { display: inline-block; }
  /* "Needs your review" card: tighten outer padding, header spacing and the
     per-warning rows so the list reads denser on phones. */
  .dash-review-card { padding: 0.8rem 0.9rem; }
  .dash-review-head { margin-bottom: 0.6rem; gap: 0.5rem; }
  .dash-review-item { padding: 0.5rem 0.6rem; gap: 0.55rem; }
  .dash-review-row1 { font-size: 0.84rem; }
  .dash-review-row2 { font-size: var(--fs-2xs); }
  /* Still one line here: wrapping would leave a dangling "·" separator, and the
     employee name ellipsizing keeps the date and weekday visible. */
  .dash-review-card .dash-review-item { min-height: 34px; padding: 0.35rem 0.5rem; }
  .dash-review-card .dash-review-issue { font-size: 0.84rem; }
  .dash-review-card .dash-review-emp,
  .dash-review-card .dash-review-date,
  .dash-review-card .dash-review-last { font-size: var(--fs-2xs); }
  .dash-pulse-ring-wrap { width: 80px; height: 80px; }
  .dash-pulse-ring-label { font-size: 1.05rem; }
  /* One card per row here, so the ring can be bigger than the desktop 62px.
     84px, not 80: the ring is the only element in `.kpi-primary-main` with a
     height of its own, so it IS the row height — and at 80px it left the
     numbers column (55px) floating in a band it under-filled, with the card's
     lower half reading as slack. 84px is the largest step that still clears the
     narrowest real case: at a 320px viewport with three-digit counts
     ("138 / 142" + a two-line caption) the row measures 76 + 141 + 16px of gap
     against 233px of inner width, so nothing shrinks, wraps or overflows.
     76 rather than 84: the ring is the only element in `.kpi-primary-main` with
     a height of its own, so it IS the row height and therefore the card's — the
     numbers column beside it is 55px, so every pixel of ring above that is
     height the card carries for one element. 76 is the floor that still leaves
     the widest readout ("100%") room inside the disc, below. */
  .dash-pulse-ring-wrap.kpi-ring-embed { width: 76px; height: 76px; }
  /* 1rem: "100%" is the widest readout and the ring's inner disc is ~65px
     across at this diameter and stroke, so the label has ~8px of air at the
     worst case. */
  .dash-pulse-ring-wrap.kpi-ring-embed .dash-pulse-ring-label { font-size: 1rem; }
  /* The stroke is drawn in a 36-unit viewBox, so `stroke-width` is a FRACTION OF
     THE DIAMETER, not a pixel value — the inherited 2.8/3.6 paint 7.8%/10.0% of
     the ring's width whatever its size. That was tuned on the 88px/118px desktop
     ring; carried down to a phone-sized ring the same proportion reads as a fat
     wedge rather than a dial, which is the "circle looks too heavy for its box"
     complaint. Re-scale so the drawn thickness holds the DESKTOP proportion
     (2.1/2.7 of 118px = 5.8%/7.5%) at this diameter: 2.05/2.65 of 84px lands at
     4.8px/6.2px, i.e. the same visual weight relative to the arc it draws. */
  .kpi-ring-embed .dash-pulse-ring-track { stroke-width: 2.05; }
  .kpi-ring-embed .dash-pulse-ring-progress { stroke-width: 2.65; }

  /* Comfortable tap targets for the quick-action chips. */
  .dash-hero-qa-btn { padding: 0.5rem 0.8rem; justify-content: center; }
  .dash-hero-qa-icon { width: 26px; height: 26px; }

  /* Compact recent-punches / away-today tables: fit the viewport instead of
     forcing a horizontal scroll, shrink padding/fonts, and let the name cell
     (with its provenance badge) wrap rather than push the row wide. */
  .dash-recent-scroll .dash-recent-table,
  .dash-away-scroll .dash-recent-table { min-width: 0; }
  .dash-recent-table th,
  .dash-recent-table td {
    padding: 0.3rem 0.4rem;
    font-size: 0.8rem;
    white-space: normal;
  }
  .dash-recent-table thead th { font-size: var(--fs-2xs); letter-spacing: 0.02em; }
  .dash-recent-table .dash-cell-name { word-break: break-word; }
  .dash-recent-table .dash-cell-time { white-space: nowrap; }
  .dash-recent-table .flag-pill { font-size: var(--fs-2xs); padding: 0.1rem 0.35rem; }
  .dash-recent-table .dash-cell-mark { margin-inline-start: 0.25rem; }
  /* The RTL first-column inset is gone at every width - see the note beside the
     base rule. This narrow-width variant only re-stated it at 0.4rem. */
}

/* ── Dark mode adjustments ────────────────────────────────────── */
[data-theme="dark"] .kpi-card {
  background: var(--surface);
  border-color: var(--gray-200);
}
/* No dark-theme `.dash-hero` background override: the base rule is `var(--surface)`
   + `var(--gray-200)`, both already theme-aware, so the flattened hero picks up
   the dark surface/border the same way the cards below it do. */
[data-theme="dark"] .dash-hero-status { background: color-mix(in srgb, var(--surface) 70%, transparent); }
[data-theme="dark"] .dash-hero-controls { background: color-mix(in srgb, var(--surface) 70%, transparent); }
[data-theme="dark"] .dash-review-item { background: var(--surface); }
[data-theme="dark"] .dash-review-empty {
  background: color-mix(in srgb, var(--success) 12%, transparent);
}
/* Re-stated after the dark .dash-review-empty rule above, which would otherwise
   paint the success wash back over the neutral variant: `[data-theme] .a` and
   `.a.b` are both (0,2,0), so the later rule wins on order. Only the background
   needs restating - the grey scale itself inverts with the theme, so the border
   and text tokens are already correct. */
[data-theme="dark"] .dash-review-empty.is-neutral {
  background: var(--gray-50);
}
[data-theme="dark"] .warnings-filter-note {
  background: color-mix(in srgb, var(--primary) 14%, transparent);
}

/* ─────────────────────────────────────────────────────────
   Reports header actions — refined export & print buttons
   (scoped via .report-actions-bar so other split-dropdowns
   used in payroll, etc. remain untouched)
   ───────────────────────────────────────────────────────── */
.report-actions-bar {
  gap: 0.5rem;
  align-items: stretch;
}
.report-actions-bar .btn {
  min-height: 1.65rem;
  padding: 0.22rem 0.55rem;
  border-radius: 8px;
  font-weight: 600;
  font-size: var(--fs-2xs);
  letter-spacing: 0.01em;
  position: relative;
}

/* Export split-button — matches print btn & payroll export (outlined secondary) */
.report-actions-bar .report-export-dropdown .btn-export-main {
  gap: 0.45rem;
  padding-inline-end: 0.7rem;
  background: var(--surface);
  color: var(--gray-700);
  border: 1px solid var(--gray-200);
  box-shadow: 0 1px 2px rgba(15,23,42,0.04);
}
.report-actions-bar .report-export-dropdown .btn-export-main:hover {
  background: var(--surface-alt);
  color: var(--primary);
  border-color: color-mix(in srgb, var(--primary) 35%, var(--gray-200));
  box-shadow:
    0 2px 4px rgba(15,23,42,0.06),
    0 6px 16px color-mix(in srgb, var(--primary) 12%, transparent);
  transform: translateY(-1px);
}
.report-actions-bar .report-export-dropdown .btn-export-main:active {
  transform: translateY(0);
  box-shadow: 0 1px 2px rgba(15,23,42,0.05);
}
.report-actions-bar .report-export-dropdown .btn-export-main:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 3px color-mix(in srgb, var(--primary) 28%, transparent),
    0 1px 2px rgba(15,23,42,0.05);
}
.report-actions-bar .split-dropdown-chevron {
  opacity: 0.85;
  margin-inline-start: 0.15rem;
  transition: transform 0.2s ease;
}
.report-actions-bar .split-dropdown.open .split-dropdown-chevron {
  transform: rotate(180deg);
  opacity: 1;
}

/* Dropdown menu — refined floating panel */
.report-actions-bar .split-dropdown-menu {
  min-width: 130px;
  margin-top: 6px;
  padding: 0.35rem;
  border-radius: 12px;
  background: var(--surface);
  border: 1px solid color-mix(in srgb, var(--gray-200) 80%, transparent);
  box-shadow:
    0 1px 2px rgba(15,23,42,0.06),
    0 12px 28px rgba(15,23,42,0.12);
  transform-origin: top center;
  animation: reportExportMenuIn 0.14s ease-out;
}
[dir="rtl"] .report-actions-bar .split-dropdown-menu {
  transform-origin: top center;
}
@keyframes reportExportMenuIn {
  from { opacity: 0; transform: translateY(-4px) scale(0.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

.report-actions-bar .split-dropdown-menu button {
  gap: 0.65rem;
  padding: 0.55rem 0.7rem;
  border-radius: 8px;
  font-size: 0.88rem;
  font-weight: 500;
  color: var(--gray-700);
  justify-content: space-between;
  transition: background 0.15s ease, color 0.15s ease, transform 0.05s ease;
}
.report-actions-bar .split-dropdown-menu button > span:not(.export-type-icon) {
  flex: 1 1 auto;
  text-align: start;
}
.report-actions-bar .split-dropdown-menu button:hover {
  background: var(--surface-alt);
  color: var(--gray-900, var(--gray-700));
}
.report-actions-bar .split-dropdown-menu button:active {
  transform: scale(0.985);
}
.report-actions-bar .split-dropdown-menu button:focus-visible {
  outline: none;
  background: var(--surface-alt);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 35%, transparent);
}

/* Visual separator between Excel and PDF rows */
.report-actions-bar .split-dropdown-menu button + button {
  position: relative;
  margin-top: 2px;
}
.report-actions-bar .split-dropdown-menu button + button::before {
  content: "";
  position: absolute;
  inset-inline: 0.4rem;
  top: -1px;
  height: 1px;
  background: color-mix(in srgb, var(--gray-200) 70%, transparent);
}

/* Color-coded file-type icons */
.report-actions-bar .split-dropdown-menu .export-type-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 7px;
  background: var(--surface-alt);
  color: var(--gray-500, var(--gray-700));
  flex-shrink: 0;
  transition: background 0.15s ease, color 0.15s ease;
}
.report-actions-bar .split-dropdown-menu button[data-export-type="excel"] .export-type-icon {
  background: color-mix(in srgb, #16a34a 14%, transparent);
  color: #15803d;
}
.report-actions-bar .split-dropdown-menu button[data-export-type="pdf"] .export-type-icon {
  background: color-mix(in srgb, #dc2626 14%, transparent);
  color: #b91c1c;
}
[data-theme="dark"] .report-actions-bar .split-dropdown-menu button[data-export-type="excel"] .export-type-icon {
  background: color-mix(in srgb, #22c55e 22%, transparent);
  color: #4ade80;
}
[data-theme="dark"] .report-actions-bar .split-dropdown-menu button[data-export-type="pdf"] .export-type-icon {
  background: color-mix(in srgb, #ef4444 22%, transparent);
  color: #fca5a5;
}

/* Print button — outlined / ghost with subtle lift */
.report-actions-bar .report-print-btn {
  background: var(--surface);
  color: var(--gray-700);
  border: 1px solid var(--gray-200);
  box-shadow: 0 1px 2px rgba(15,23,42,0.04);
}
.report-actions-bar .report-print-btn:hover {
  background: var(--surface-alt);
  color: var(--primary);
  border-color: color-mix(in srgb, var(--primary) 35%, var(--gray-200));
  box-shadow:
    0 2px 4px rgba(15,23,42,0.06),
    0 6px 16px color-mix(in srgb, var(--primary) 12%, transparent);
  transform: translateY(-1px);
}
.report-actions-bar .report-print-btn:active {
  transform: translateY(0);
  box-shadow: 0 1px 2px rgba(15,23,42,0.05);
}
.report-actions-bar .report-print-btn:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 3px color-mix(in srgb, var(--primary) 28%, transparent),
    0 1px 2px rgba(15,23,42,0.05);
}

/* Refined count badge inside the action buttons */
.report-actions-bar .report-action-count {
  display: inline;
  background: none;
  padding: 0;
  margin-inline-start: 0.08rem;
  color: inherit;
  font-size: 0.82rem;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0;
}
.report-actions-bar .report-action-count:empty {
  display: none;
}
.report-actions-bar .report-print-btn .report-action-count {
  background: none;
  color: inherit;
}

/* Reports output header row — title + employee preview select on one side,
   export/print actions on the other. Inline styles were lifted to these
   classes so the mobile layout below can fully override them. */
.report-header-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.report-header-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  min-width: 0;
}

/* Tighter layout on narrow screens */
@media (max-width: 640px) {
  /* Stack the header into clear bands: title, preview select, then actions —
     separated by a hairline so the post-generation toolbar reads as one block. */
  .report-header-row {
    flex-direction: column;
    align-items: stretch;
    gap: 0.65rem;
  }
  .report-header-title {
    flex-direction: column;
    align-items: stretch;
    gap: 0.55rem;
    width: 100%;
  }
  .report-header-title #report-title {
    font-size: 1.05rem;
  }
  /* Let the employee preview picker fill the width and become a tap target */
  .report-header-title .report-preview-select-wrap {
    width: 100%;
  }
  .report-header-title .sync-interval-picker,
  .report-header-title .sync-interval-trigger {
    width: 100%;
  }
  .report-header-title .sync-interval-trigger {
    justify-content: space-between;
    min-height: 2.5rem;
  }

  .report-actions-bar {
    width: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0.5rem;
    padding-top: 0.65rem;
    border-top: 1px solid var(--gray-100);
  }
  .report-actions-bar .report-export-dropdown,
  .report-actions-bar .report-print-btn {
    flex: none;
    width: 100%;
  }
  /* Full-width, comfortably tall touch targets stacked vertically */
  .report-actions-bar .report-export-dropdown .btn-export-main,
  .report-actions-bar .report-print-btn {
    width: 100%;
    justify-content: center;
    min-height: 2.6rem;
    font-size: 0.9rem;
    border-radius: 10px;
  }
  /* Drop the picker menu to full width so options aren't cramped */
  .report-actions-bar .split-dropdown-menu {
    width: 100%;
    min-width: 0;
  }

  /* ── Reports tables: bounded inner scroll + sticky header / first column ──
     The wrapper becomes the vertical scroll container so sticky works on both
     axes; without a max-height the page scrolls instead and sticky is inert. */
  [data-page="reports"] .table-responsive {
    max-height: calc(100vh - 10rem);
    max-height: calc(100dvh - 10rem); /* dvh after vh: graceful fallback */
    /* The local/scroll gradient scroll-hint bg forces main-thread scrolling
       and is hidden behind the opaque table anyway — swap for plain surface. */
    background: var(--surface);
  }
  /* The global table{overflow:hidden} (border-radius clipping) makes the table
     itself the sticky containing clip — header/first-column sticky silently
     die inside it. Inside the wrapper the radius is already 0, so the clip
     serves no purpose; visible restores sticky against the wrapper.
     table-layout auto (!important beats DataTable's inline 'fixed', which only
     exists for desktop mouse column-resizing): under auto a column can never
     be narrower than its longest header/cell word, so single words ("الموظف")
     neither split across lines nor overlap neighbors — in any language. */
  [data-page="reports"] .table-responsive table {
    overflow: visible;
    table-layout: auto !important;
  }
  /* Data cells wrap at spaces, never inside a word ("الخميس" must not split
     as "الخمي"/"س"). The global mobile td rule uses word-break:break-word,
     a legacy alias of overflow-wrap:anywhere whose soft breaks SHRINK the
     column's min-content; overflow-wrap:break-word keeps whole words in the
     sizing math, so auto layout always fits them — the emergency mid-word
     break can then only fire for a word wider than the viewport. */
  [data-page="reports"] .table-responsive td {
    word-break: normal;
    overflow-wrap: break-word;
  }

  /* Sticky header. !important beats DataTable's inline
     th.style.position='relative' (api.js — anchors the col-resize handle;
     sticky is also a positioned state so the handle keeps anchoring).
     Collapsed borders don't travel with sticky cells → inset box-shadow
     hairline, same technique as .dash-recent-table / .slip-att-table.
     Headers wrap at spaces ONLY — never inside a word (same rule as the print
     stylesheet: "الرسمية" must stay intact, not "الرس"/"مية"); table-layout
     auto above guarantees the column always fits the longest single word. */
  [data-page="reports"] .table-responsive thead th {
    position: sticky !important;
    top: 0;
    z-index: 2;
    box-shadow: inset 0 -1px 0 var(--gray-200);
    white-space: normal;
    word-break: normal;
    overflow-wrap: normal;
    hyphens: none;
  }

  /* Sticky first column — thead th + tbody td only (the detail tfoot totals
     label is colspan=5 and must stay excluded). The leave table never
     h-scrolls (min-width 0 below), so this is a no-op there. */
  [data-page="reports"] .table-responsive tbody td:first-child,
  [data-page="reports"] .table-responsive thead th:first-child {
    position: sticky !important;
    inset-inline-start: 0;
  }
  [data-page="reports"] .table-responsive tbody td:first-child {
    z-index: 1;
    background: var(--surface);                 /* odd rows: td is transparent */
    box-shadow: inset -1px 0 0 var(--gray-200); /* separation edge; RTL flip below */
  }
  /* Re-state zebra + hover: the pinned-cell background above outranks the
     global rules. Zebra before hover — equal specificity, source order wins. */
  [data-page="reports"] .table-responsive tbody tr:nth-child(even) td:first-child {
    background: var(--gray-50);
  }
  [data-page="reports"] .table-responsive tbody tr:hover td:first-child {
    background: color-mix(in srgb, var(--primary) 6%, var(--surface));
  }
  /* Corner cell sits above both axes and needs edge + hairline shadows */
  [data-page="reports"] .table-responsive thead th:first-child {
    z-index: 3;
    box-shadow: inset -1px 0 0 var(--gray-200), inset 0 -1px 0 var(--gray-200);
  }
  /* box-shadow has no logical form — flip the separation edge in RTL */
  [dir="rtl"] [data-page="reports"] .table-responsive tbody td:first-child {
    box-shadow: inset 1px 0 0 var(--gray-200);
  }
  [dir="rtl"] [data-page="reports"] .table-responsive thead th:first-child {
    box-shadow: inset 1px 0 0 var(--gray-200), inset 0 -1px 0 var(--gray-200);
  }

  /* Explicit first-col widths (table-layout is fixed: th width defines the
     column) so the pinned date/name column is neither squeezed nor dominant */
  [data-page="reports"] #report-detail-table thead th:first-child { width: 7rem; }
  [data-page="reports"] #report-summary-table thead th:first-child,
  [data-page="reports"] #report-payroll-table thead th:first-child { width: 8.5rem; }

  /* Leave's 6 narrow columns fit a phone — drop the global 800px min-width so
     it doesn't force pointless horizontal scrolling. The other views keep the
     global minimum; table-layout auto grows them beyond it to fit content. */
  [data-page="reports"] #report-leave-table table { min-width: 0; }

  /* Known quirk (unchanged from before): the pager lives inside the wrapper
     and pans out of view under horizontal scroll. position:sticky cannot pin
     it — its containing block (#report-*-pager) sizes to the scrollport, so
     there is no slide room; only table cells get the full-width table as
     their containing block. Pinning it would need the pager moved outside
     the wrapper, which changes the desktop layout. */
}

/* Attendance Summary header labels wrap at spaces on desktop too. The table
   runs 14+ equal-width columns under DataTable's inline table-layout:fixed, so
   a full label ("Attendance on Weekend Days") is ~2.5x its cell; the global
   th{white-space:nowrap} then spills it across the neighbour and the table edge
   clips it mid-word, leaving adjacent columns reading the same truncated text.
   Wrap at spaces ONLY - never inside a word ("الرسمية" must stay intact) -
   which is what the ≤640px block already does for every reports table.
   table-layout auto (!important beats DataTable's inline 'fixed', which only
   exists for desktop mouse column-resizing) is the other half: under fixed,
   equal columns are narrower than the longest single word, so wrapping alone
   still clips "ATTENDANCE"/"WORKDAYS". Under auto no column can be narrower
   than its longest word and the wrapper h-scrolls instead - the same pairing
   #report-payroll-table already uses on desktop. */
#report-summary-table .table-responsive table {
  overflow: visible;
  table-layout: auto !important;
}
#report-summary-table thead th {
  white-space: normal;
  word-break: normal;
  overflow-wrap: normal;
  hyphens: none;
}

/* ─────────────────────────────────────────────────────────
   Payroll Summary table — grouped 2-row sticky header,
   pinned first column + totals row, at EVERY viewport (the
   mobile block above already does this for reports tables at
   ≤640px; the payroll table needs it on desktop too — it is
   the one report wide enough to h-scroll there). ID selectors
   deliberately outrank the mobile block so both agree.
   ───────────────────────────────────────────────────────── */
#report-payroll-table {
  --ps-ghead-h: 2.25rem;   /* fixed group-row height = leaf row's sticky top */
}
#report-payroll-table .table-responsive {
  max-height: max(320px, calc(100vh - 13rem));
  max-height: max(320px, calc(100dvh - 13rem)); /* dvh after vh: graceful fallback */
  background: var(--surface); /* gradient scroll-hint bg forces main-thread scrolling */
}
/* The global table{overflow:hidden} (radius clipping) would make the table the
   sticky containing clip; table-layout auto (!important beats DataTable's
   inline 'fixed') lets every column fit its longest header/cell so MONEY
   NEVER TRUNCATES — the wrapper h-scrolls instead. */
#report-payroll-table .table-responsive table {
  overflow: visible;
  table-layout: auto !important;
}
#report-payroll-table .table-responsive td {
  overflow: visible;
  text-overflow: clip;
}

/* Sticky header (both rows). !important beats DataTable's inline
   th.style.position='relative' (col-resize anchor; sticky is also a positioned
   state so the handle keeps anchoring). Collapsed borders don't travel with
   sticky cells → inset box-shadow hairline. */
#report-payroll-table thead th {
  position: sticky !important;
  z-index: 2;
  background: var(--gray-50);
  box-shadow: inset 0 -1px 0 var(--gray-200);
  white-space: normal;    /* wrap at spaces only — never inside a word */
  word-break: normal;
  overflow-wrap: normal;
  hyphens: none;
}
#report-payroll-table thead .ps-group-row th {
  top: 0;
  height: var(--ps-ghead-h);
  padding-block: 0;
  text-align: center;
  vertical-align: middle;
  font-size: var(--fs-2xs);
  letter-spacing: 0.08em;
  color: var(--gray-400);
  white-space: nowrap;    /* single short word; must not grow past the fixed height */
}
#report-payroll-table thead #report-payroll-head th {
  top: var(--ps-ghead-h);
  vertical-align: middle;
}

/* Pinned first column: leaf th + group corner cell + body + totals cells.
   (The pre-render placeholder row's colspan cell also matches — harmless.) */
#report-payroll-table tbody td:first-child,
#report-payroll-table tfoot td:first-child,
#report-payroll-table thead th:first-child {
  position: sticky !important;
  inset-inline-start: 0;
}
#report-payroll-table tbody td:first-child {
  z-index: 1;
  background: var(--surface);                 /* odd rows: td is transparent */
  box-shadow: inset -1px 0 0 var(--gray-200); /* separation edge; RTL flip below */
}
/* Re-state zebra + hover: the pinned-cell background above outranks the
   global rules. Zebra before hover — equal specificity, source order wins. */
#report-payroll-table tbody tr:nth-child(even) td:first-child {
  background: var(--gray-50);
}
#report-payroll-table tbody tr:hover td:first-child {
  background: color-mix(in srgb, var(--primary) 6%, var(--surface));
}
/* Corner cells sit above both axes and need edge + hairline shadows */
#report-payroll-table thead th:first-child {
  z-index: 3;
  box-shadow: inset -1px 0 0 var(--gray-200), inset 0 -1px 0 var(--gray-200);
}

/* Totals row pinned to the bottom edge — the answer row stays in view.
   border-top can't travel with a sticky cell → 2px inset shadow stand-in. */
#report-payroll-table tfoot td {
  position: sticky;
  bottom: 0;
  z-index: 2;
  background: var(--gray-100);
  box-shadow: inset 0 2px 0 var(--gray-300);
}
#report-payroll-table tfoot td:first-child {
  z-index: 3;
  box-shadow: inset -1px 0 0 var(--gray-200), inset 0 2px 0 var(--gray-300);
}

/* box-shadow has no logical form — flip the separation edges in RTL */
[dir="rtl"] #report-payroll-table tbody td:first-child {
  box-shadow: inset 1px 0 0 var(--gray-200);
}
[dir="rtl"] #report-payroll-table thead th:first-child {
  box-shadow: inset 1px 0 0 var(--gray-200), inset 0 -1px 0 var(--gray-200);
}
[dir="rtl"] #report-payroll-table tfoot td:first-child {
  box-shadow: inset 1px 0 0 var(--gray-200), inset 0 2px 0 var(--gray-300);
}

/* ─────────────────────────────────────────────────────────
   Export / Print buttons — colored icon chip enhancement
   Adds a tinted square behind the leading icon so the
   action is instantly recognizable at a glance.
   ───────────────────────────────────────────────────────── */
.report-actions-bar .btn-export-main,
.report-actions-bar .report-print-btn {
  padding-inline-start: 0.4rem;
  padding-inline-end: 0.7rem;
  font-size: 0.82rem;
  font-weight: 600;
}
.report-actions-bar .btn-action-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 7px;
  flex-shrink: 0;
  transition: color 0.18s ease;
}
.report-actions-bar .btn-action-icon svg {
  width: 13px;
  height: 13px;
}
/* Export chip — neutral at rest, primary on hover */
.report-actions-bar .btn-export-main .btn-action-icon {
  color: var(--gray-700);
}
.report-actions-bar .btn-export-main:hover .btn-action-icon {
  color: var(--primary);
}
/* Print chip — slate-tinted */
.report-actions-bar .report-print-btn .btn-action-icon {
  color: var(--gray-600);
}
.report-actions-bar .report-print-btn:hover .btn-action-icon {
  color: var(--primary);
}
[data-theme="dark"] .report-actions-bar .btn-export-main .btn-action-icon {
  color: var(--primary);
}
[data-theme="dark"] .report-actions-bar .report-print-btn .btn-action-icon {
  color: var(--gray-300);
}

/* ─── Data Quality Banner ────────────────────────────────────────────── */
/* Tri-state report-quality indicator rendered between report header and   */
/* the data table. Three trust levels: verified (green), review (amber),   */
/* action (red). Border accent uses border-inline-start so RTL flips it    */
/* visually to the right side automatically.                               */

.dq-banner {
  --dq-color: #6b7280;
  --dq-bg: #f9fafb;
  position: relative;
  display: block;
  padding: 0.55rem 0.85rem;
  margin: 0 0 0.6rem;
  border-radius: 8px;
  border: 1px solid color-mix(in srgb, var(--dq-color) 22%, transparent);
  border-inline-start: 4px solid var(--dq-color);
  background: var(--dq-bg);
  color: var(--text-primary, #111827);
  opacity: 0;
  transform: translateY(-6px);
  animation: dq-banner-in 300ms ease forwards;
}

@keyframes dq-banner-in {
  to { opacity: 1; transform: translateY(0); }
}

.dq-banner--verified {
  --dq-color: #16a34a;
  --dq-bg: #f0fdf4;
}
.dq-banner--review {
  --dq-color: #d97706;
  --dq-bg: #fffbeb;
}
.dq-banner--action {
  --dq-color: #dc2626;
  --dq-bg: #fef2f2;
}

.dq-banner__head {
  display: flex;
  align-items: flex-start;
  gap: 0.35rem;
}

.dq-banner__icon {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  color: var(--dq-color);
}
.dq-banner__icon svg { width: 18px; height: 18px; }

.dq-banner--verified .dq-banner__icon {
  animation: dq-pulse 1200ms ease-out 1;
}
@keyframes dq-pulse {
  0%   { transform: scale(0.85); opacity: 0.6; }
  60%  { transform: scale(1.15); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .dq-banner { animation: none; opacity: 1; transform: none; }
  .dq-banner--verified .dq-banner__icon { animation: none; }
}

.dq-banner__body { flex: 1; min-width: 0; }

.dq-banner__heading {
  margin: 0 0 0.1rem;
  font-size: 0.88rem;
  line-height: 1.4;
  font-weight: 700;
  color: var(--dq-color);
}

.dq-banner__subtext {
  margin: 0;
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--text-secondary, #4b5563);
}

.dq-banner__actions {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  gap: 0.45rem;
  align-self: center;
}

.dq-banner__toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0;
  padding: 0.3rem 0.7rem;
  line-height: 1.4;
  border-radius: 6px;
  border: 1px solid color-mix(in srgb, var(--dq-color) 35%, transparent);
  background: transparent;
  color: var(--dq-color);
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.dq-banner__toggle:hover {
  background: color-mix(in srgb, var(--dq-color) 10%, transparent);
}

.dq-banner__chevron {
  transition: transform 0.2s ease;
}
.dq-banner__toggle[aria-expanded="true"] .dq-banner__chevron {
  transform: rotate(180deg);
}

.dq-banner__details {
  overflow: hidden;
  max-height: 0;
  transition: max-height 0.3s ease, margin-top 0.3s ease;
}
.dq-banner__details.is-open {
  max-height: 1200px;       /* large enough for the cap of 50 rows */
  margin-top: 0.85rem;
}

.dq-banner__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: 0.75rem;
}

.dq-chip {
  display: inline-flex;
  align-items: center;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--dq-color) 14%, transparent);
  color: var(--dq-color);
  border: 1px solid color-mix(in srgb, var(--dq-color) 28%, transparent);
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
}

.dq-banner__detail-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8rem;
  background: rgba(255, 255, 255, 0.55);
  border-radius: 6px;
  overflow: hidden;
}
.dq-banner__detail-table th,
.dq-banner__detail-table td {
  padding: 0.45rem 0.65rem;
  text-align: start;
  border-bottom: 1px solid color-mix(in srgb, var(--dq-color) 14%, transparent);
}
.dq-banner__detail-table th {
  background: color-mix(in srgb, var(--dq-color) 10%, transparent);
  color: var(--dq-color);
  font-weight: 600;
  font-size: 0.75rem;
  text-transform: none;
}
.dq-banner__detail-table tr:last-child td { border-bottom: 0; }

.dq-banner__truncated {
  margin-top: 0.5rem;
  font-size: 0.75rem;
  color: var(--text-secondary, #4b5563);
  font-style: italic;
}

.dq-banner__cta {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.7rem;
  line-height: 1.4;
  border-radius: 6px;
  border: 1px solid color-mix(in srgb, var(--dq-color) 35%, transparent);
  background: transparent;
  color: var(--dq-color);
  font-size: 0.8rem;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.dq-banner__cta:hover {
  background: color-mix(in srgb, var(--dq-color) 10%, transparent);
}

[data-theme="dark"] .dq-banner {
  background: color-mix(in srgb, var(--dq-color) 12%, var(--surface, #1f2937));
  color: var(--text-primary, #f9fafb);
}
[data-theme="dark"] .dq-banner__detail-table {
  background: rgba(0, 0, 0, 0.25);
}

@media print {
  .dq-banner { display: none !important; }
}

/* ── Action (red) banner — enhanced urgency design ────────────── */

.dq-banner--action {
  --dq-color: #e03535;
  --dq-bg:    #fff1f1;
  background: linear-gradient(155deg, #fff1f1 0%, #fde8e8 55%, #fbd5d5 100%);
  border-inline-start-width: 3px;
  border-top: 2px solid color-mix(in srgb, #e03535 28%, transparent);
  box-shadow:
    0 2px 6px  color-mix(in srgb, #e03535 10%, transparent),
    0 6px 18px color-mix(in srgb, #e03535 7%,  transparent),
    inset 0 1px 0 rgba(255, 255, 255, 0.65);
}

/* Pulsing icon badge */
.dq-banner--action .dq-banner__icon {
  width: 28px;
  height: 28px;
  background: color-mix(in srgb, #e03535 16%, transparent);
  border-radius: 50%;
  animation: dq-action-ripple 2.6s ease-in-out infinite;
}
.dq-banner--action .dq-banner__icon svg { width: 16px; height: 16px; }

@keyframes dq-action-ripple {
  0%   { box-shadow: 0 0 0 0   color-mix(in srgb, #e03535 38%, transparent); }
  50%  { box-shadow: 0 0 0 7px color-mix(in srgb, #e03535 0%,  transparent); }
  100% { box-shadow: 0 0 0 0   color-mix(in srgb, #e03535 0%,  transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .dq-banner--action .dq-banner__icon { animation: none; }
}

/* Heading — slightly larger & tighter tracking */
.dq-banner--action .dq-banner__heading {
  font-size: 0.93rem;
  letter-spacing: -0.01em;
}

/* Subtext — slightly stronger contrast */
.dq-banner--action .dq-banner__subtext {
  color: #374151;
}

/* Chips — more vivid fill */
.dq-banner--action .dq-chip {
  background: color-mix(in srgb, #e03535 18%, transparent);
  border-color: color-mix(in srgb, #e03535 36%, transparent);
  font-weight: 700;
}

/* Toggle button — subtle filled tint */
.dq-banner--action .dq-banner__toggle {
  background: color-mix(in srgb, #e03535 8%, transparent);
  border-color: color-mix(in srgb, #e03535 30%, transparent);
}
.dq-banner--action .dq-banner__toggle:hover {
  background: color-mix(in srgb, #e03535 16%, transparent);
}

/* Detail table header — more vivid */
.dq-banner--action .dq-banner__detail-table th {
  background: color-mix(in srgb, #e03535 14%, transparent);
  font-weight: 700;
}

/* Row hover — red tint */
.dq-banner--action .dq-banner__detail-table tbody tr:hover td {
  background: color-mix(in srgb, #e03535 6%, transparent);
}

/* Dark mode */
[data-theme="dark"] .dq-banner--action {
  background: color-mix(in srgb, #e03535 18%, var(--surface, #1f2937));
  border-top-color: color-mix(in srgb, #e03535 40%, transparent);
  box-shadow:
    0 2px 6px  color-mix(in srgb, #e03535 18%, transparent),
    0 6px 18px color-mix(in srgb, #e03535 10%, transparent);
}
[data-theme="dark"] .dq-banner--action .dq-banner__subtext { color: #d1d5db; }

/* ─── Warning-to-Record Highlight ─────────────────────────────────────────
   Visual link from a warning row (.warning-row in #raw-warnings-body) to
   the matching raw-punch rows (.raw-punch-row in #raw-punches-body). The
   table is never filtered by this feature — only the affected rows pick
   up a tinted background and an inline-start accent so they are findable
   at a glance. RTL-safe via logical properties.
   ─────────────────────────────────────────────────────────────────────── */

/* Base hook on every raw punch row. Default transparent border keeps row
   height + width stable when the highlight border activates. */
.raw-punch-row {
  border-inline-start: 3px solid transparent;
  box-sizing: border-box;
  transition: background-color 0.2s ease;
}

/* Active highlight: subtle tint + inline-start accent. CSS variables make
   the issue-specific tones a single override below. */
.raw-punch-row--highlight {
  background-color: var(--warning-highlight-bg, rgba(245, 158, 11, 0.10));
  border-inline-start-color: var(--warning-highlight-accent, #f59e0b);
}

/* Apply highlight to <td> directly so the global alternating-row rule
   (tbody tr:nth-child(even) td { background: var(--gray-50) }) and the
   hover rule (tr:hover td { background: … }) cannot hide the tint. The
   selector specificity (0,1,3) intentionally matches or exceeds both
   competing rules; source-order places this block after them. */
tbody tr.raw-punch-row--highlight td {
  background-color: var(--warning-highlight-bg, rgba(245, 158, 11, 0.10));
}

/* Issue-specific tones */
.raw-punch-row--issue-consecutive {
  --warning-highlight-bg: rgba(251, 146, 60, 0.12);
  --warning-highlight-accent: #f97316;
}
.raw-punch-row--issue-unknown {
  --warning-highlight-bg: rgba(239, 68, 68, 0.10);
  --warning-highlight-accent: #ef4444;
}
.raw-punch-row--issue-missing {
  --warning-highlight-bg: rgba(234, 179, 8, 0.08);
  --warning-highlight-accent: #eab308;
}
.raw-punch-row--issue-short-session {
  --warning-highlight-bg: rgba(168, 85, 247, 0.08);
  --warning-highlight-accent: #a855f7;
}
.raw-punch-row--issue-break {
  --warning-highlight-bg: rgba(59, 130, 246, 0.08);
  --warning-highlight-accent: #3b82f6;
}
.raw-punch-row--issue-ot {
  --warning-highlight-bg: rgba(20, 184, 166, 0.08);
  --warning-highlight-accent: #14b8a6;
}
.raw-punch-row--issue-default {
  --warning-highlight-bg: rgba(245, 158, 11, 0.10);
  --warning-highlight-accent: #f59e0b;
}
/* Not an issue bucket: the row the user just created. Success-green so "this is
   the thing you asked for" reads differently from the amber/red problem tones.
   RTL is free - the accent is the shared border-inline-start on .raw-punch-row. */
.raw-punch-row--issue-added {
  --warning-highlight-bg: rgba(16, 185, 129, 0.12);
  --warning-highlight-accent: #10b981;
}

/* One-time pulse animation on activation. Animates background-color only so
   layout/border do not shift mid-animation. */
.raw-punch-row--pulse {
  animation: kx-warning-pulse 600ms ease-out;
}
/* Pulse on <td> level mirrors the <tr> animation so even rows (which have a
   solid alternating-row background) also flash the accent colour. */
tbody tr.raw-punch-row--pulse td {
  animation: kx-warning-pulse 600ms ease-out;
}
@keyframes kx-warning-pulse {
  0%   { background-color: var(--warning-highlight-accent, #f59e0b); }
  100% { background-color: var(--warning-highlight-bg, rgba(245, 158, 11, 0.10)); }
}

/* Active / selected state on the warning card (a <li> inside #raw-warnings-body). */
.warning-row.warning-card--active,
.dash-review-item.warning-card--active {
  background: color-mix(in srgb, var(--warning-highlight-accent, #f59e0b) 10%, var(--surface));
  border-color: var(--warning-highlight-accent, #f59e0b);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--warning-highlight-accent, #f59e0b) 25%, transparent);
}

/* Dark mode — warm tint readable on dark surfaces. */
[data-theme="dark"] .raw-punch-row--highlight {
  background-color: color-mix(in srgb, var(--warning-highlight-accent, #f59e0b) 14%, transparent);
}
[data-theme="dark"] tbody tr.raw-punch-row--highlight td {
  background-color: color-mix(in srgb, var(--warning-highlight-accent, #f59e0b) 14%, transparent);
}
/* Dark mode: the added-row green needs to lift off a dark surface - the tint
   above is color-mixed from this accent, so raising it here fixes both. */
[data-theme="dark"] .raw-punch-row--issue-added {
  --warning-highlight-accent: #34d399;
}
[data-theme="dark"] .warning-row.warning-card--active,
[data-theme="dark"] .dash-review-item.warning-card--active {
  background: color-mix(in srgb, var(--warning-highlight-accent, #f59e0b) 14%, var(--surface));
  border-color: var(--warning-highlight-accent, #f59e0b);
}

/* Reduced motion: no pulse, no transition. */
@media (prefers-reduced-motion: reduce) {
  .raw-punch-row { transition: none; }
  .raw-punch-row--pulse { animation: none; }
}

/* Mobile: keep the same row tint + inline-start accent — no extra columns,
   no layout shift. The 3px border is preserved so the affected rows stay
   findable on narrow viewports. */
@media (max-width: 600px) {
  .raw-punch-row { border-inline-start-width: 3px; }
}

/* Print: drop the highlight chrome so printouts look clean. */
@media print {
  .raw-punch-row--highlight { background: transparent !important; border-inline-start-color: transparent !important; }
  .warning-row.warning-card--active,
  .dash-review-item.warning-card--active { box-shadow: none !important; }
}

/* ── Punch-request "View surrounding punches" dialog — requested-punch row ──
   Marks the synthetic, not-yet-applied requested punch slotted into the punch
   table. Reuses the canonical primary-accent idiom (cf. .admin-notice-banner):
   leading-edge accent (RTL-safe via the logical property) + a subtle primary
   tint. The tint is applied at td level so the global alternating-row
   (tbody tr:nth-child(even) td) and tr:hover td rules cannot mask it. --primary
   is redefined under [data-theme="dark"], so the tint auto-adapts. */
.pr-requested-row { border-inline-start: 3px solid var(--primary); }
tbody tr.pr-requested-row td {
  background: color-mix(in srgb, var(--primary) 7%, transparent);
}

/* The dialog is just a table + a Close button, so trim the default
   .modal-actions top margin to keep the gap under the table tight. */
#pr-view-modal .modal-actions { margin-top: 0.75rem; }

/* ── Punch-request list tables — let cell content wrap ──
   Both lists use table-layout:fixed with narrow fixed-width columns, so the
   global `td { white-space:nowrap; overflow:hidden; text-overflow:ellipsis }`
   clips the secondary detail lines (approved-via note, rejection/cancellation
   reason, reason notes) instead of wrapping them — the approved status text
   was being cut off mid-word. Let these cells wrap within their fixed width and
   top-align so the badge sits above its detail line. overflow-wrap guards
   against an unbroken free-text reason overflowing the column. Mirrors the
   emp-day-detail modal table override. */
#pr-mine-list td,
#pr-inbox-list td {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  vertical-align: top;
  overflow-wrap: anywhere;
}

/* Salary-adjustments type cell holds up to three badges (type + deferred +
   debt-linked); the global td nowrap/ellipsis clips the trailing badge in EN.
   Let the badges wrap as whole units (.badge itself stays nowrap). */
#adj-body td.adj-type-cell {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}

/* Apply-date cell: same escape from the global td nowrap/ellipsis, because the
   locked-state chip sits UNDER the date. Without it the chip is a couple of
   pixels too wide in EN at this column's share and gets ellipsed away
   entirely - the disclosure would vanish silently, which is the one failure
   mode this feature exists to remove. */
#adj-body td.adj-date-cell {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}
#adj-body td.adj-date-cell .adj-date-value {
  display: block;
  white-space: nowrap;
}
#adj-body td.adj-date-cell .badge {
  margin-block-start: 0.25rem;
  font-size: var(--fs-2xs);
}

/* A disabled field in the adjustment modal must LOOK disabled: the carry-over
   case locks type/amount/apply date while description stays editable, and the
   UA default (a shade of grey text on the same white background) does not read
   as "you cannot change this" against this design's already-light inputs.
   Scoped to this form on purpose - there is no global disabled-input rule in
   this stylesheet, and adding one would repaint every page at once. */
#adj-form input:disabled,
#adj-form select:disabled {
  background: var(--gray-100);
  color: var(--gray-500);
  cursor: not-allowed;
}

/* The ledger twin, for the same reason and with the same declarations: editing
   a charge that was MOVED here from a locked slip's carry-over locks entry
   type/amount/date and leaves description editable, so the two states have to
   be distinguishable at a glance. Without it the locked Amount box was pixel
   for pixel the editable Description box beside it. Scoped to this form,
   matching the rule above - still no global disabled-input rule. */
#debt-form input:disabled,
#debt-form select:disabled {
  background: var(--gray-100);
  color: var(--gray-500);
  cursor: not-allowed;
}

/* Same reasoning for the employee modal, where editing an existing employee
   locks Device User ID (it is the biometric device's key for that person -
   repointing it would re-attribute their punch history). It sits beside the
   read-only Standard Hours/Day field, which carries these exact two
   declarations inline, so the pair now reads as one state instead of one
   greyed box next to a white "disabled" one. --gray-50 and not --gray-100 for
   that reason. Scoped to this form, matching the rule above: there is still no
   global disabled-input rule here on purpose. */
#employee-form input:disabled {
  background: var(--gray-50);
  cursor: not-allowed;
}
/* ─── i18n FOUC prevention ─── */
/* Body is invisible from first paint until applyLanguage() completes.       */
/* opacity keeps layout intact — no shift when the page fades in.            */
body.i18n-pending { opacity: 0; }
body:not(.i18n-pending) { transition: opacity 0.05s ease; }

/* ── Slip modal tabs + navigation ── */
.slip-tabs {
  margin: 0 1.25rem;
  margin-bottom: 0;
  flex-shrink: 0;
}
.slip-tabs .view-tab {
  padding: 0.55rem 1rem;
  font-size: 0.88rem;
}
#slip-modal {
  align-items: flex-start;
  padding-block-start: 5vh;
  overflow-y: auto;
}
#slip-modal .modal-body { padding-top: 0.85rem; }
.slip-panel { outline: none; }

.slip-nav {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-inline-end: 0.5rem;
}
.slip-nav-btn {
  width: 1.85rem; height: 1.85rem;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent;
  border: 1px solid var(--gray-200);
  border-radius: var(--radius);
  color: var(--gray-600);
  cursor: pointer;
  font-size: 1.05rem;
  line-height: 1;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.slip-nav-btn:hover:not(:disabled) {
  background: var(--gray-50);
  color: var(--primary);
  border-color: var(--primary-100, var(--gray-300));
}
.slip-nav-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* No RTL transform here on purpose. The button's only content is a directional
   chevron (‹ / › = U+2039/U+203A), and those are Bidi_Mirrored characters — the
   browser already mirrors them inside an RTL run, so prev/next point the right
   way with no CSS help. A `[dir="rtl"] { transform: scaleX(-1) }` here mirrored
   them a SECOND time, cancelling out and leaving both arrows pointing inward.
   Contrast .dash-review-cta svg, an SVG icon, which is not bidi-mirrored and
   does need the explicit flip. */

.slip-nav-employee {
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--gray-800);
  padding: 0 0.25rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 16rem;
}

.slip-attendance-loading,
.slip-attendance-error {
  padding: 2rem 1rem;
  text-align: center;
  color: var(--gray-500);
}

/* ── Slip attendance tab — daily table ── */
.slip-att-table-wrap {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border: 1px solid var(--gray-200);
  border-radius: var(--radius);
}
.slip-att-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
}
.slip-att-table th,
.slip-att-table td {
  padding: 0.35rem 0.55rem;
  border-bottom: 1px solid var(--gray-100);
  text-align: start;
  white-space: nowrap;
}
.slip-att-table thead th {
  background: var(--gray-50);
  font-weight: 600;
  color: var(--gray-600);
  font-size: var(--fs-2xs);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  position: sticky;
  top: 0;
  z-index: 2;
  box-shadow: inset 0 -1px 0 var(--gray-200);
}

/* Zebra striping — uniform across all row states. */
.slip-att-table tbody tr:nth-child(even) td { background: var(--gray-50); }
.slip-att-table tbody tr:nth-child(odd)  td { background: var(--surface, #fff); }

/* Row-state: no full-row background tinting; semantic weight lives in the
   Status column pills so the table reads cleanly without colour noise. */
.slip-att-row.is-weekend td,
.slip-att-row.is-holiday td { color: var(--text-muted); }
/* Absent rows: subtle danger left-border accent only — no background flood.
   Half-day absence rows (unpunched half-day leave) carry the same accent —
   they contribute to the absence deduction too. */
.slip-att-row.is-absent td:first-child,
.slip-att-row.is-absent-half td:first-child {
  border-inline-start: 3px solid var(--danger);
  padding-inline-start: calc(0.55rem - 3px);
}
.slip-att-row.is-virtual td { font-style: italic; }

/* Date cell: weekday chip + date */
.slip-att-date-cell { min-width: 120px; }
.slip-att-date-wrap { display: inline-flex; align-items: baseline; gap: 0.45rem; }
.slip-att-dow {
  display: inline-block;
  min-width: 2.2rem;
  padding: 0.05rem 0.35rem;
  border-radius: 999px;
  background: transparent;
  color: var(--gray-600);
  font-size: var(--fs-2xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  text-align: center;
}
.slip-att-date { font-weight: 500; }

/* Muted placeholder dash — quiet, hairline character that recedes. */
.slip-att-empty { color: var(--gray-300); font-weight: 400; }

/* Variance pill chips */
.slip-att-chip {
  display: inline-block;
  padding: 0.08rem 0.45rem;
  border-radius: 999px;
  font-size: var(--fs-2xs);
  font-weight: 600;
  line-height: 1.5;
}
.slip-att-chip-surplus {
  background: var(--success-light);
  color: var(--success);
}
.slip-att-chip-deficit {
  background: var(--danger-light, #fee2e2);
  color: var(--danger);
}
.slip-att-chip-deficit + .slip-att-chip-surplus { margin-inline-start: 0.35rem; }

/* Status pill (single-tone, one per row). Separate from the per-flag
   pills rendered by renderFlagPills — those stack after the status pill. */
.slip-att-pill {
  display: inline-block;
  padding: 0.08rem 0.5rem;
  border-radius: 999px;
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1.5;
  margin-inline-end: 0.25rem;
}
.slip-att-pill-present { background: transparent; color: inherit; }
.slip-att-pill-absent  { background: var(--danger-light, #fee2e2); color: var(--danger); }
.slip-att-pill-weekend { background: transparent; color: inherit; }
.slip-att-pill-holiday { background: transparent; color: inherit; }
.slip-att-pill-leave   { background: transparent; color: inherit; }

.slip-att-totals-row td {
  background: var(--gray-50);
  font-weight: 700;
  border-top: 2px solid var(--gray-200);
  border-bottom: none;
  position: sticky;
  bottom: 0;
  z-index: 1;
  box-shadow: inset 0 1px 0 var(--gray-200);
}
.slip-att-col-actions { width: 2rem; }
.slip-att-row-kebab {
  width: 1.5rem; height: 1.5rem;
  background: transparent; border: none;
  color: var(--gray-400); cursor: not-allowed;
  border-radius: var(--radius);
}
.slip-att-row-kebab:disabled { opacity: 0.3; }
.slip-attendance-empty {
  padding: 2.5rem 1rem;
  text-align: center;
  color: var(--gray-500);
}

/* Column prioritization for narrow widths (modal is up to 900px; hide Notes below ~620px) */
@media (max-width: 620px) {
  .slip-att-col-notes { display: none; }
}

/* ── Deficit-to-Leave Offset (Phase 6) ──
   Contextual trigger sits inside the attendance-tab tfoot deficit total
   cell. The dialog itself reuses the global .modal-overlay / .modal styles
   and only specializes structural pieces inside .do-modal. All layout uses
   CSS logical properties so RTL mirrors cleanly. */

.slip-att-deficit-total-wrap {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  white-space: nowrap;
}
.slip-att-deficit-total-value {
  flex: 0 0 auto;
}
.slip-att-deficit-total-wrap.is-offset-applied .slip-att-deficit-total-value {
  color: var(--primary);
  background: var(--primary-50, rgba(99, 102, 241, 0.08));
  border: 1px solid var(--primary-200, rgba(99, 102, 241, 0.2));
  border-radius: 999px;
  padding: 0.1rem 0.45rem;
  cursor: help;
}
.slip-att-deficit-total-wrap.is-offset-applied .slip-att-deficit-total-value:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}
.do-trigger-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  padding: 0;
  border: 1px solid var(--gray-300);
  background: var(--surface, #fff);
  color: var(--gray-600);
  border-radius: var(--radius);
  cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
  flex: 0 0 auto;
}
.do-trigger-icon:hover {
  background: var(--primary-50, rgba(99, 102, 241, 0.07));
  color: var(--primary);
  border-color: var(--primary-200, rgba(99, 102, 241, 0.2));
}
.do-trigger-icon:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}
.do-trigger-icon.is-active {
  background: var(--primary-50, rgba(99, 102, 241, 0.08));
  color: var(--primary);
  border-color: var(--primary);
}
.do-trigger-icon.is-active:hover {
  background: var(--primary-100, rgba(99, 102, 241, 0.16));
}
.do-trigger-icon svg {
  width: 14px;
  height: 14px;
}

/* Period-OT: the attendance-tab footer deficit total is the sum of daily
   deficits — informational only (payroll settles on the period net). Mute it
   and caption it; the actionable offset trigger lives on the Salary Slip tab. */
.slip-att-deficit-total-wrap.is-informational .slip-att-deficit-total-value {
  color: var(--gray-500);
  cursor: help;
}
.slip-att-period-note {
  margin-top: 0.4rem;
  padding-inline: 0.25rem;
  font-size: 0.75rem;
  line-height: 1.35;
  color: var(--gray-500);
}
/* Period-OT: deficit-to-leave trigger anchored next to the slip's net-deficit
   row label. Inline with the text; never printed (wrapped in .no-print). */
.slip-period-offset-trigger {
  display: inline-flex;
  vertical-align: middle;
  margin-inline-start: 0.4rem;
}
.slip-period-offset-trigger .do-trigger-icon {
  width: 1.4rem;
  height: 1.4rem;
}

/* ── Dialog body ── */
.do-modal .do-subtitle {
  margin: 0 0 0.85rem;
  color: var(--gray-600);
  font-size: 0.875rem;
  line-height: 1.4;
}
.do-modal .do-employee-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.25rem;
  padding: 0.65rem 0.85rem;
  margin-bottom: 1rem;
  background: var(--gray-50, #f9fafb);
  border-radius: var(--radius);
  font-size: 0.85rem;
  color: var(--gray-700);
}
.do-modal .do-employee-strip strong {
  color: var(--gray-800);
}
.do-modal .do-loading,
.do-modal .do-error {
  padding: 1.5rem 0.5rem;
  text-align: center;
  color: var(--gray-500);
}
.do-modal .do-error {
  color: var(--danger);
}

.do-modal .do-section {
  margin-bottom: 1.1rem;
}
.do-modal .do-section:last-of-type {
  margin-bottom: 0;
}
.do-modal .do-section-title {
  margin: 0 0 0.35rem;
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--gray-800);
}
.do-modal .do-section-hint {
  margin: 0 0 0.6rem;
  font-size: 0.8rem;
  color: var(--gray-600);
}

/* ── Existing-offsets summary ── */
.do-modal .do-offset-list {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}
.do-modal .do-offset-card {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.65rem 0.85rem;
  border: 1px solid var(--gray-200);
  border-radius: var(--radius);
  background: var(--surface, #fff);
}
.do-modal .do-offset-card-main {
  flex: 1 1 auto;
  min-width: 0;
}
.do-modal .do-offset-line {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.35rem;
  font-size: 0.95rem;
}
.do-modal .do-offset-line strong {
  font-size: 1rem;
  color: var(--gray-900);
}
.do-modal .do-offset-sub {
  color: var(--gray-600);
  font-size: 0.85rem;
}
.do-modal .do-offset-sep {
  color: var(--gray-300);
}
.do-modal .do-offset-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1rem;
  margin-top: 0.25rem;
  font-size: 0.78rem;
  color: var(--gray-600);
}
.do-modal .do-offset-meta strong {
  color: var(--gray-700);
  font-weight: 600;
}
.do-modal .do-offset-card-actions {
  flex: 0 0 auto;
}

/* ── Create form ── */
.do-modal .do-stat-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.5rem;
  margin-bottom: 0.85rem;
}
@media (max-width: 500px) {
  .do-modal .do-stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.do-modal .do-stat {
  padding: 0.55rem 0.7rem;
  background: var(--gray-50, #f9fafb);
  border: 1px solid var(--gray-100);
  border-radius: var(--radius);
}
.do-modal .do-stat-label {
  font-size: var(--fs-2xs);
  letter-spacing: 0.02em;
  color: var(--gray-500);
  text-transform: uppercase;
  margin-bottom: 0.2rem;
}
.do-modal .do-stat-value {
  font-size: 1rem;
  font-weight: 700;
  color: var(--gray-900);
  font-variant-numeric: tabular-nums;
}
.do-modal .do-stat-accent {
  background: var(--warning-light, #fef3c7);
  border-color: var(--warning, #d97706);
}
.do-modal .do-stat-accent .do-stat-label {
  color: var(--warning, #d97706);
}

.do-modal .do-create-form .form-group {
  margin-bottom: 0.75rem;
}
.do-modal .do-days-label {
  display: block;
  margin-bottom: 0.3rem;
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--gray-700);
}
.do-modal .do-days-input {
  width: 100%;
  padding: 0.5rem 0.65rem;
  font-size: 1rem;
  border: 1px solid var(--gray-300);
  border-radius: var(--radius);
  font-variant-numeric: tabular-nums;
}
.do-modal .do-days-input:focus {
  outline: none;
  border-color: var(--primary);
  box-shadow: 0 0 0 3px var(--primary-50, rgba(99, 102, 241, 0.15));
}
.do-modal .do-days-hint {
  margin-top: 0.25rem;
  font-size: 0.75rem;
  color: var(--gray-500);
}
.do-modal .do-net-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.55rem 0.75rem;
  margin-bottom: 0.85rem;
  background: var(--primary-50, rgba(99, 102, 241, 0.06));
  border-radius: var(--radius);
}
.do-modal .do-net-label {
  font-size: 0.85rem;
  color: var(--gray-700);
}
.do-modal .do-net-deficit {
  font-size: 1rem;
  color: var(--gray-900);
  font-variant-numeric: tabular-nums;
}
.do-modal .do-form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}

/* ── Status / messages ── */
.do-modal .do-warning {
  padding: 0.55rem 0.75rem;
  margin-bottom: 0.85rem;
  background: var(--warning-light, #fef3c7);
  color: var(--warning, #b45309);
  border-radius: var(--radius);
  font-size: 0.85rem;
}
.do-modal .do-blocker {
  padding: 0.85rem;
  background: var(--gray-50, #f9fafb);
  border: 1px dashed var(--gray-300);
  border-radius: var(--radius);
  color: var(--gray-700);
}
.do-modal .do-blocker strong {
  display: block;
  margin-bottom: 0.25rem;
  color: var(--gray-800);
}
.do-modal .do-blocker p {
  margin: 0;
  font-size: 0.875rem;
}
.do-modal .do-covered {
  padding: 0.85rem;
  background: var(--success-light);
  border-radius: var(--radius);
  color: var(--success);
}
.do-modal .do-covered strong {
  display: block;
  margin-bottom: 0.25rem;
}
.do-modal .do-covered p {
  margin: 0;
  font-size: 0.875rem;
}

.do-modal .do-footer {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

/* ─── Employee self-service leaves (migrated from the legacy employee leaves pages'
   inline styles in Phase 2 so the shared leaves page keeps them).
   Uses logical properties only, so RTL is preserved. ─── */
.ml-balance-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:0.85rem}
.ml-balance-card{padding:0.85rem 1rem;background:var(--gray-50,#f8fafc);border:1px solid var(--gray-200,#e2e8f0);border-radius:10px}
.ml-balance-name{font-weight:600;color:var(--text-primary);margin-bottom:0.45rem}
.ml-balance-row{display:flex;justify-content:space-between;font-size:0.82rem;padding:0.15rem 0;color:var(--text-secondary)}
.ml-balance-row .v{font-weight:600;color:var(--text-primary)}
.ml-balance-bar{height:6px;background:var(--gray-200,#e2e8f0);border-radius:3px;overflow:hidden;margin:0.55rem 0}
.ml-balance-bar-fill{height:100%;background:var(--primary,#4f46e5);transition:width 0.3s ease}
.ml-balance-pending{font-size:0.76rem;color:#b45309;background:rgba(245,158,11,0.12);border-radius:6px;padding:0.18rem 0.5rem;margin-top:0.35rem;display:inline-block}
.ml-status-pending{background:rgba(245,158,11,0.15);color:#b45309;border-radius:999px;padding:0.05rem 0.55rem;font-size:0.75rem;font-weight:600}
.ml-status-approved{background:rgba(16,185,129,0.15);color:#047857;border-radius:999px;padding:0.05rem 0.55rem;font-size:0.75rem;font-weight:600}
.ml-status-rejected{background:rgba(239,68,68,0.12);color:#b91c1c;border-radius:999px;padding:0.05rem 0.55rem;font-size:0.75rem;font-weight:600}
.ml-rejection{font-size:0.75rem;color:var(--gray-600,#475569);margin-top:0.25rem}
.mtl-half-badge{font-size: var(--fs-2xs);background:rgba(79,70,229,0.12);color:var(--primary,#4f46e5);border-radius:999px;padding:0.05rem 0.4rem;margin-inline-start:0.3rem}
.mtl-attachment-icon{display:inline-flex;align-items:center;color:var(--text-secondary)}
.mtl-actions{display:flex;gap:0.4rem;flex-wrap:wrap}

/* ─── Kronex Connector pairing wizard (connector-wizard.js) ───────────────────
   Logical properties throughout, so RTL needs no override. The one directional
   thing on the page - the pairing code and the device serial - is pinned LTR in
   the MARKUP (dir="ltr") rather than here, because it is a property of the
   value (D17) and not of the layout. */
.cw-lede{margin:0 0 1rem;color:var(--text-secondary);font-size:0.9rem;line-height:1.55;max-width:60ch}

/* The step rail. Purely an orientation aid, so it is aria-hidden by the JS that
   drives it; the screen's own <h4> is what a screen reader announces. */
.cw-rail{display:flex;flex-wrap:wrap;gap:0.25rem 1.1rem;list-style:none;margin:0 0 1.35rem;padding:0;font-size:0.76rem;color:var(--text-secondary)}
.cw-rail-step{display:flex;align-items:center;gap:0.4rem}
.cw-rail-dot{width:8px;height:8px;border-radius:50%;background:var(--gray-300,#cbd5e1);flex-shrink:0}
.cw-rail-step.is-done .cw-rail-dot{background:var(--success,#10b981)}
.cw-rail-step.is-current .cw-rail-dot{background:var(--primary,#4f46e5);box-shadow:0 0 0 3px rgba(79,70,229,0.18)}
.cw-rail-step.is-current{color:var(--text-primary);font-weight:600}

.cw-screen[hidden]{display:none}
.cw-screen h4{margin:0 0 0.5rem;font-size:1.02rem;color:var(--text-primary)}
.cw-screen>p{margin:0 0 0.9rem;color:var(--text-secondary);font-size:0.88rem;line-height:1.6;max-width:62ch}
.cw-note{background:var(--gray-50,#f8fafc);border:1px solid var(--gray-200,#e2e8f0);border-radius:var(--radius);padding:0.65rem 0.85rem}
.cw-muted{color:var(--text-secondary);font-size:0.82rem}
.cw-actions{display:flex;flex-wrap:wrap;gap:0.6rem;margin-top:1.1rem}

/* The pairing code. Monospace and widely tracked because it is TRANSCRIBED:
   the alphabet already drops 0/O/1/I/L for the same reason.
   `.form-group input.cw-code-input` and not a bare class: the base rule is
   `.form-group input:not([type="radio"]) { max-width: 100% }`, which outranks a
   single class and left the field full-width. `text-indent` compensates for the
   trailing letter-space, which otherwise pushes the centred run left. */
.form-group input.cw-code-input{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:1.3rem;letter-spacing:0.3em;text-indent:0.3em;text-transform:uppercase;text-align:center;max-width:11rem}

/* The live checklist. One row per step; the state lives in a class so the JS
   never writes a colour. */
.cw-checklist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:0.5rem}
.cw-step{display:flex;align-items:flex-start;gap:0.6rem;font-size:0.88rem;line-height:1.5;color:var(--text-primary)}
.cw-step-mark{flex-shrink:0;width:18px;height:18px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size: var(--fs-2xs);font-weight:700;margin-top:0.1rem;background:var(--gray-200,#e2e8f0);color:var(--text-secondary)}
.cw-step.is-ok .cw-step-mark{background:rgba(16,185,129,0.15);color:#047857}
.cw-step.is-running .cw-step-mark{background:rgba(79,70,229,0.15);color:var(--primary,#4f46e5)}
.cw-step.is-warn .cw-step-mark{background:rgba(245,158,11,0.18);color:#b45309}
.cw-step.is-error .cw-step-mark{background:rgba(239,68,68,0.15);color:#b91c1c}
.cw-step.is-pending{color:var(--text-secondary)}
.cw-step-text{min-width:0}

/* A problem, rendered from the message layer: title, then the optional detail
   and the recommended action. Never a toast - each of these has an action the
   customer is supposed to take. */
.cw-problem{border:1px solid var(--gray-200,#e2e8f0);border-inline-start:3px solid var(--warning,#d97706);background:var(--gray-50,#f8fafc);border-radius:var(--radius);padding:0.8rem 0.95rem;margin-block:0.9rem}
.cw-problem[hidden]{display:none}
.cw-problem.is-error{border-inline-start-color:var(--danger,#dc2626)}
.cw-problem.is-info{border-inline-start-color:var(--primary,#4f46e5)}
.cw-problem-title{font-weight:600;color:var(--text-primary);font-size:0.92rem;line-height:1.5}
.cw-problem-detail{margin-top:0.4rem;color:var(--text-secondary);font-size:0.85rem;line-height:1.6}
.cw-problem-action-label{margin-top:0.7rem;font-size: var(--fs-2xs);font-weight:700;letter-spacing:0.04em;text-transform:uppercase;color:var(--text-secondary)}
.cw-problem-action{margin-top:0.2rem;font-size:0.85rem;line-height:1.6;color:var(--text-primary)}
.cw-problem-lg{margin-block-start:0}
.cw-problem-lg .cw-problem-title{font-size:1rem}

/* The device facts (§4 step 5) and the first-sync payoff. A <dl> because these
   really are label/value pairs, and the VALUE side is a <bdi> written by the JS
   so a serial or an IPv4 address keeps its order inside an Arabic page (D6). */
.cw-facts{display:grid;grid-template-columns:minmax(0,auto) minmax(0,1fr);gap:0.45rem 1.1rem;margin:0 0 1.1rem;font-size:0.88rem}
.cw-facts dt{color:var(--text-secondary)}
.cw-facts dd{margin:0;color:var(--text-primary);font-weight:600;overflow-wrap:anywhere}
.cw-facts dd bdi{font-variant-numeric:tabular-nums}
/* The serial is compared character-for-character against a sticker (D17). */
.cw-facts dd.cw-mono bdi{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:0.04em}

/* The `multiple_devices_found` choice list (Task 4). Logical properties
   throughout, so the radio sits on the start side in both directions and no
   rule has to know which side that is. */
.cw-choice-set{border:1px solid var(--gray-200,#e2e8f0);border-radius:var(--radius);padding:0.75rem 0.9rem;margin:0 0 1.1rem;min-inline-size:0}
.cw-choice-set[hidden]{display:none}
.cw-choice-set legend{padding-inline:0.35rem;font-size:0.8rem;font-weight:600;color:var(--text-secondary)}
.cw-choice-set>.cw-muted{margin:0 0 0.6rem}
.cw-choice{display:flex;align-items:center;gap:0.6rem;padding:0.45rem 0.5rem;border-radius:var(--radius);cursor:pointer;font-size:0.88rem;color:var(--text-primary)}
.cw-choice:hover{background:var(--gray-50,#f8fafc)}
.cw-choice input{flex-shrink:0;margin:0}
.cw-choice-text{min-width:0;overflow-wrap:anywhere}
/* D17 again: the serial on a radio label is read against the sticker on the
   device, so it is monospaced and its <bdi> keeps it LTR in both languages. */
.cw-choice-text.cw-mono bdi{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:0.04em;font-variant-numeric:tabular-nums}
/* A swept candidate (F19): three stacked lines, so `align-items:center` from
   `.cw-choice` would float the radio against the middle line instead of the
   name it selects. Logical properties throughout - this list renders in Arabic
   with the two transcribed values still LTR inside their own <bdi>. */
.cw-candidate{align-items:flex-start;padding-block:0.55rem}
.cw-candidate input{margin-block-start:0.15rem}
.cw-candidate-body{display:flex;flex-direction:column;gap:0.15rem;min-width:0}
.cw-candidate-name{font-weight:600;overflow-wrap:anywhere}
.cw-candidate-meta{font-size:0.8rem;color:var(--text-secondary);overflow-wrap:anywhere}
.cw-candidate-meta.cw-mono bdi{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:0.03em;font-variant-numeric:tabular-nums}
/* Password-protected: listed and legible, never selectable. It is dimmed rather
   than hidden because a refusing clock is a device that WAS found, and leaving
   it off the list is the same false negative as denying it exists. */
.cw-candidate.is-disabled{cursor:not-allowed;opacity:0.62}
.cw-candidate.is-disabled:hover{background:none}
.cw-candidate-badge{display:inline-block;font-size: var(--fs-2xs);font-weight:600;padding:0.1rem 0.4rem;border-radius:999px;background:var(--warning-bg,#fef3c7);color:var(--warning-text,#92400e);white-space:nowrap;vertical-align:middle}
/* Address beside port. `minmax(0,...)` on both tracks so a long placeholder
   cannot push the row wider than the modal - the page body must never scroll
   sideways, and a nested grid child needs its own min-width:0 to honour that. */
.cw-field-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,7rem);gap:0 1rem;align-items:start}
/* A `display` of any kind outranks the UA's `[hidden]{display:none}`, so a row
   that sets one has to opt back in - the same trap `.dash-hero-status` shipped
   with, where `el.hidden` read true while the element was on screen. Both are
   set by JS (the address block is asked for only when the pick does not already
   carry an address), so the property alone would prove nothing. */
.cw-field-row[hidden],.cw-note[hidden]{display:none}
.cw-field-row>.form-group{min-inline-size:0}
@media (max-width:520px){.cw-field-row{grid-template-columns:minmax(0,1fr)}}
.form-group input.cw-serial-input{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:0.06em}

.cw-details{margin-top:0.8rem;font-size:0.8rem;color:var(--text-secondary)}
.cw-details summary{cursor:pointer}
.cw-details pre{margin:0.5rem 0 0;padding:0.6rem 0.75rem;background:var(--gray-50,#f8fafc);border:1px solid var(--gray-200,#e2e8f0);border-radius:var(--radius);overflow-x:auto;white-space:pre-wrap;overflow-wrap:anywhere;font-size:0.76rem;direction:ltr;text-align:start}

/* ─── Kronex Connector: the Device & Connector panel (connector-panel.js) ─────
   §9's layout: one plain sentence, then progressive disclosure.

   Every offset is a LOGICAL property, so the whole card mirrors in Arabic with
   no RTL-specific rule. The one glyph in here is the top-line mark (✓ / ⚠ / ℹ),
   which is deliberately NOT a chevron: `‹`/`›` are Bidi_Mirrored and the browser
   already flips them in an RTL run, so a CSS transform would double-mirror.
   These three are not mirrored and are safe as text. */
.cp-topline{display:flex;align-items:flex-start;gap:0.7rem;padding:0.85rem 1rem;border-radius:var(--radius);border:1px solid var(--gray-200,#e2e8f0);border-inline-start:3px solid var(--gray-300,#cbd5e1);background:var(--gray-50,#f8fafc);margin-bottom:1.1rem}
.cp-topline[data-severity="ok"]{border-inline-start-color:var(--success,#10b981)}
.cp-topline[data-severity="info"]{border-inline-start-color:var(--primary,#4f46e5)}
.cp-topline[data-severity="warn"]{border-inline-start-color:var(--warning,#d97706)}
.cp-topline[data-severity="error"]{border-inline-start-color:var(--danger,#dc2626)}
.cp-topline-icon{flex-shrink:0;font-size:1rem;line-height:1.5;color:var(--text-secondary)}
.cp-topline[data-severity="ok"] .cp-topline-icon{color:var(--success,#10b981)}
.cp-topline[data-severity="warn"] .cp-topline-icon{color:var(--warning,#d97706)}
.cp-topline[data-severity="error"] .cp-topline-icon{color:var(--danger,#dc2626)}
.cp-topline-text{min-inline-size:0}
.cp-topline-title{margin:0;font-size:0.95rem;font-weight:600;line-height:1.5;color:var(--text-primary)}
.cp-topline-detail{margin:0.3rem 0 0;font-size:0.85rem;line-height:1.6;color:var(--text-secondary)}
.cp-topline-detail[hidden]{display:none}
.cp-topline-action{margin:0.45rem 0 0;font-size:0.85rem;line-height:1.6;color:var(--text-primary)}
.cp-topline-action[hidden]{display:none}

/* The five rows. `minmax(0,...)` on the value track so a long model name or a
   verbatim timestamp wraps instead of widening the card. */
.cp-rows{display:grid;grid-template-columns:minmax(0,11rem) minmax(0,1fr);gap:0.55rem 1rem;margin:0 0 1.1rem}
.cp-rows[hidden]{display:none}
.cp-rows dt{margin:0;font-size:0.82rem;font-weight:600;color:var(--text-secondary)}
.cp-rows dd{margin:0;min-inline-size:0}
.cp-row-value{font-size:0.88rem;line-height:1.5;color:var(--text-primary);overflow-wrap:anywhere}
.cp-row-sub{margin-top:0.2rem;font-size:0.82rem;line-height:1.5;color:var(--text-secondary);overflow-wrap:anywhere}
@media (max-width:600px){.cp-rows{grid-template-columns:minmax(0,1fr);gap:0.15rem}.cp-rows dd{margin-bottom:0.55rem}}

/* One block per standing problem, each carrying its own recommended action
   quantified in the customer's own data (§9). */
.cp-issues{display:flex;flex-direction:column;gap:0.7rem;margin-bottom:1.1rem}
.cp-issues[hidden]{display:none}
.cp-issue{border:1px solid var(--gray-200,#e2e8f0);border-inline-start:3px solid var(--warning,#d97706);background:var(--gray-50,#f8fafc);border-radius:var(--radius);padding:0.75rem 0.9rem}
.cp-issue[data-severity="error"]{border-inline-start-color:var(--danger,#dc2626)}
.cp-issue-title{margin:0;font-size:0.9rem;font-weight:600;line-height:1.5;color:var(--text-primary)}
.cp-issue-detail{margin:0.35rem 0 0;font-size:0.84rem;line-height:1.6;color:var(--text-secondary)}
.cp-issue-action{margin:0.5rem 0 0;font-size:0.84rem;line-height:1.6;color:var(--text-primary)}

.cp-actions{display:flex;flex-wrap:wrap;gap:0.6rem;margin-bottom:1rem}
.cp-actions button[hidden]{display:none}

.cp-disclosure{border-top:1px solid var(--gray-200,#e2e8f0);padding:0.6rem 0;font-size:0.85rem}
.cp-disclosure[hidden]{display:none}
.cp-disclosure>summary{cursor:pointer;font-weight:600;color:var(--text-primary);list-style-position:inside}
.cp-disclosure>*:not(summary){margin-top:0.7rem}
.cp-muted{color:var(--text-secondary);font-size:0.82rem;line-height:1.6}

.cp-facts{display:grid;grid-template-columns:minmax(0,14rem) minmax(0,1fr);gap:0.35rem 1rem;margin:0.7rem 0 0;font-size:0.82rem}
.cp-facts dt{margin:0;color:var(--text-secondary)}
.cp-facts dd{margin:0;min-inline-size:0;color:var(--text-primary);overflow-wrap:anywhere}
.cp-facts dd bdi{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.78rem}
@media (max-width:600px){.cp-facts{grid-template-columns:minmax(0,1fr);gap:0.1rem}.cp-facts dd{margin-bottom:0.45rem}}

.cp-activity{list-style:none;margin:0.6rem 0 0;padding:0;display:flex;flex-direction:column;gap:0.4rem;max-block-size:22rem;overflow-y:auto}
.cp-activity-row{display:grid;grid-template-columns:minmax(0,11rem) minmax(0,1fr);gap:0 0.9rem;font-size:0.82rem;line-height:1.5;padding-inline-start:0.6rem;border-inline-start:2px solid var(--gray-200,#e2e8f0)}
.cp-activity-row[data-severity="ok"]{border-inline-start-color:var(--success,#10b981)}
.cp-activity-row[data-severity="warn"]{border-inline-start-color:var(--warning,#d97706)}
.cp-activity-row[data-severity="error"]{border-inline-start-color:var(--danger,#dc2626)}
.cp-activity-at{color:var(--text-secondary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.75rem}
.cp-activity-text{color:var(--text-primary);min-inline-size:0;overflow-wrap:anywhere}
@media (max-width:600px){.cp-activity-row{grid-template-columns:minmax(0,1fr)}}

.cp-divider{border:none;border-top:1px solid var(--gray-200,#e2e8f0);margin:1.25rem 0 1rem}
.cp-manual-heading{margin:0 0 0.3rem;font-size:0.95rem;color:var(--text-primary)}
.cp-manual-hint{margin:0 0 1rem;max-inline-size:62ch}

/* ─── End of Service: computed breakdown ───
   Shared by the settle wizard's step 2 and the settlement detail modal, which
   show the SAME record at two moments (about to be frozen / already frozen) and
   so must read identically. Three movements, in the order HR reads them: WHO is
   leaving, what the figure was computed FROM, and what is PAYABLE — the loose
   `label: value` prose lines these replace gave every fact the same weight, so
   the one number HR acts on (the suggested net) was indistinguishable from the
   wage basis. Logical properties only: one markup renders EN (LTR) and AR (RTL). */
.eos-breakdown {
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}

/* Identity header — name prominent, exit date + reason chip beside it. */
.eos-ident {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.3rem 0.75rem;
  padding-block-end: 0.6rem;
  border-block-end: 1px solid var(--border-color);
}
.eos-ident-name {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--text-primary);
  min-inline-size: 0;
  overflow-wrap: anywhere;
}
.eos-ident-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 0.6rem;
  font-size: 0.8rem;
  color: var(--text-secondary);
}
.eos-ident-date strong {
  color: var(--text-primary);
  font-weight: 600;
}

.eos-section-title {
  margin: 0 0 0.35rem;
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-secondary);
  text-align: start;
}
/* Arabic is caseless and cursive: uppercasing is a no-op and tracking pulls the
   joined glyphs apart, so the label keeps its natural spacing in RTL. */
[dir="rtl"] .eos-section-title {
  letter-spacing: normal;
  text-transform: none;
}

/* Basis: an aligned label/value grid, not a dot-joined sentence. */
.eos-card {
  background: var(--surface-alt);
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  padding: 0.7rem 0.85rem;
}
.eos-dl {
  display: grid;
  grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
  gap: 0.4rem 1.1rem;
  align-items: baseline;
  font-size: 0.85rem;
}
.eos-dt {
  color: var(--text-secondary);
  min-inline-size: 0;
}
.eos-dd {
  min-inline-size: 0;
  font-weight: 600;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}
@media (max-width: 520px) {
  .eos-dl { grid-template-columns: minmax(0, 1fr); gap: 0.1rem; }
  .eos-dd { margin-block-end: 0.45rem; }
}

/* Allowance lines sit UNDER the wage they are part of, indented behind a rule —
   they qualify one figure and are not three more facts of equal rank. */
.eos-allow {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  margin-block-start: 0.4rem;
  padding-inline-start: 0.6rem;
  border-inline-start: 2px solid var(--border-color);
  font-weight: 400;
}
.eos-allow-title {
  font-size: var(--fs-2xs);
  font-weight: 600;
  color: var(--text-secondary);
}
.eos-allow-row {
  display: flex;
  justify-content: space-between;
  gap: 0.75rem;
  font-size: 0.8rem;
  color: var(--text-secondary);
}
.eos-allow-name { min-inline-size: 0; overflow-wrap: anywhere; }
.eos-allow-amt {
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.eos-alert { margin: 0; }

/* Scenario table — the amount column reads as a money column. */
.eos-scenarios { overflow-x: auto; }
.eos-scenarios table {
  margin: 0;
  border-radius: var(--radius);
}
.eos-scenarios th,
.eos-scenarios td {
  padding: 0.45rem 0.7rem;
  font-size: 0.84rem;
}
.eos-scenarios td:first-child { white-space: normal; }
.eos-scenarios th:last-child,
.eos-scenarios td:last-child {
  text-align: end;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Out-specifies the global `tbody tr:nth-child(even) td` stripe (0,1,3) and the
   `tr:hover td` wash (0,1,2) — at a plain `.class > td` the even-row stripe won
   and the selected scenario lost its tint on half the rows. */
.eos-scenarios tbody tr.eos-scenario-selected > td {
  background: var(--primary-50);
  font-weight: 600;
  color: var(--text-primary);
}

/* Totals — the money settles here: indemnity leads, the two disclosures sit
   under it, and the suggested net is separated by a rule as the bottom line. */
.eos-totals {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  background: var(--surface-alt);
  border: 1px solid var(--border-color);
  border-radius: var(--radius);
  padding: 0.7rem 0.9rem;
}
.eos-total-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  font-size: 0.86rem;
}
.eos-total-label {
  color: var(--text-secondary);
  min-inline-size: 0;
}
.eos-total-value {
  font-weight: 600;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.eos-total-row--lead .eos-total-label {
  color: var(--text-primary);
  font-weight: 600;
  font-size: 0.92rem;
}
.eos-total-row--lead .eos-total-value {
  font-size: 1.05rem;
  font-weight: 700;
}
.eos-total-note {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.75rem;
  font-size: 0.76rem;
  line-height: 1.4;
  color: var(--text-muted);
}
.eos-total-note span:first-child { min-inline-size: 0; }
.eos-total-note span:last-child { white-space: nowrap; font-variant-numeric: tabular-nums; }
.eos-total-row--net {
  margin-block-start: 0.3rem;
  padding-block-start: 0.55rem;
  border-block-start: 2px solid var(--border-color);
}
.eos-total-row--net .eos-total-label {
  color: var(--text-primary);
  font-weight: 700;
  font-size: 0.95rem;
}
.eos-total-row--net .eos-total-value {
  color: var(--primary);
  font-size: 1.2rem;
  font-weight: 700;
}

/* Settlement detail modal: provenance footer + the warnings frozen at settle. */
.eos-detail-foot {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--text-muted);
}
.eos-detail-foot strong { color: var(--text-secondary); font-weight: 600; }
.eos-acked-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.eos-acked-item {
  padding-inline-start: 0.6rem;
  border-inline-start: 2px solid var(--warning);
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--text-secondary);
}

/* ─── Offline banner (shell-owned, js/offline-banner.js) ─── */
/* Bottom-anchored, and it collides with nothing: .toast-container is
   top-right (top: 1.5rem, z-index 200) and its mobile override only
   re-centers it horizontally, so toasts stay above; main.css has no fixed
   bottom action bar (the one fixed `bottom: 0` element is the sidebar rail).
   z-index sits above page chrome (.mobile-topbar 60) and below the modal
   overlay (100) and toasts (200) — a modal must never render under it. */
.offline-banner {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 90;
  display: none;
  padding: 0.5rem 1rem;
  text-align: center;
  font-size: 0.85rem;
  background: var(--surface);
  color: var(--warning-text);
  border-top: 2px solid var(--warning);
  box-shadow: var(--shadow-md);
}
.offline-banner.visible { display: block; }
/* Keyboard up → hidden rather than floating mid-screen (see the JS comment). */
.offline-banner.keyboard-open { display: none; }

/* ═══ Mobile reduced-column tables ═══
   Spec: docs/superpowers/specs/2026-08-18-mobile-tabular-data-design.md.
   Opt-in per table: DataTable adds .dtm/.dtm-<tbody-id> to the table and
   .dtm-ready to its wrapper when a `mobile` config is passed. Untreated
   tables keep the global 800px min-width + horizontal scroll — flipping
   min-width:0 system-wide is the final migration step, not this one. */
.dtm-group-row { display: none; }

@media (max-width: 640px) {
  .table-responsive.dtm-ready {
    /* A reduced table never scrolls sideways, so the wrapper stops being a
       scroll/clip box — which is also what lets sticky headers (Task 8) work
       against the page scroll. The scroll-hint gradients assume a scrollport;
       swap for plain surface (same move as the reports mobile block). */
    overflow: visible;
    background: var(--surface);
  }
  .table-responsive.dtm-ready table {
    min-width: 0;
    /* The global table{overflow:hidden} (radius clipping) would clip sticky
       positioning; radius is already 0 inside the wrapper. auto !important
       beats DataTable's inline table-layout:fixed (desktop col-resize only). */
    overflow: visible;
    table-layout: auto !important;
  }
  .dtm td {
    /* word-break:break-word (the 768px block) is a legacy alias of
       overflow-wrap:anywhere whose soft breaks shrink min-content and split
       Arabic words mid-word; break-word keeps whole words in the sizing math. */
    white-space: normal;
    word-break: normal;
    overflow-wrap: break-word;
    hyphens: none;
    padding: 0.6rem 0.65rem;
  }
  .dtm td.num { white-space: nowrap; word-break: normal; }
  /* Pills are `white-space: nowrap` globally, which makes their min-content the
     WHOLE label — so a pill wider than its share of a 375px table paints
     outside the cell (spec §4.5's ancestor-clipping mechanism, which reads
     false to a scrollWidth check). Letting them wrap is what makes the
     no-truncation guarantee hold by construction rather than by measurement:
     a long badge ("Manually Added") takes a second line INSIDE its pill
     instead of overpainting the next column. The tighter inline padding is
     what keeps that from costing density where it would actually bite — the
     Arabic punch pills (بصمة دخول / بصمة خروج) run 4px past their column at
     the shared 0.5rem and would wrap every single row; at 0.35rem they fit on
     one line, with the wrap still underneath as the guarantee. */
  .dtm td .flag-pill { white-space: normal; padding-inline: 0.35rem; }
  .dtm .col-resize-handle { display: none; }
  /* The hidden columns are hidden per-CELL (`display: none` on every th/td),
     which removes their cells but NOT their column from the auto layout — so
     the table's leftover width was handed to columns that paint nothing, and
     the raw table ended 35.8px short of its own right edge in English and
     87px short in Arabic (the strip lands at the inline END, so RTL made it
     unmissable). Making the first column greedy hands that space to the one
     column that wants it: names and the "Manually Added" badge stop wrapping,
     and the median row drops 65px → 44px, back onto the spec's measured
     41-46px target. Inert on the daily table, whose columns already fill. */
  .dtm thead th:first-child { width: 100%; }
  .dtm-group-row { display: table-row; }
  /* `tbody tr.dtm-group-row td` (0,1,3), not `.dtm-group-row td` (0,1,1): the
     global zebra stripe is `tbody tr:nth-child(even) td` (also (0,1,3) — the
     :nth-child counts in the class column), so the short selector lost on
     every even-positioned band and painted it plain grey. The upgrade only
     TIES the zebra; source order breaks the tie, so this rule must stay
     AFTER the zebra rule (~line 1243) — do not hoist it.
     --gray-200, not --gray-100: in dark mode --gray-100 IS --surface, so the
     band had zero contrast against the rows it separates. */
  tbody tr.dtm-group-row td {
    background: var(--gray-200);
    color: var(--gray-700);
    font-size: 0.78rem;
    font-weight: 600;
    padding: 0.4rem 0.65rem;
  }
  /* One step darker than the band it sits on — it used to be --gray-200, which
     is now the band itself, and the pill would vanish into it in both themes. */
  .dtm-group-row .dtm-group-count {
    margin-inline-start: 0.5rem;
    background: var(--gray-300);
    color: var(--gray-600);
    border-radius: 999px;
    padding: 0.05rem 0.5rem;
    font-size: 0.72rem;
    font-weight: 600;
  }
  /* Grouped tables carry the date in the band; per-row date parts collapse. */
  .dtm.dtm-grouped .dtm-date-part { display: none; }

  /* Sticky header + day band, stacked under the sticky .mobile-topbar (52px).
     !important beats DataTable's inline th position:relative (col-resize
     anchor); collapsed borders don't travel with sticky cells → inset
     hairline (same technique as the reports mobile block). The wrapper and
     table are already overflow:visible above, which is what lets these stick
     against the PAGE scroll rather than a wrapper clip box. */
  .dtm thead th {
    position: sticky !important;
    top: 52px;
    z-index: 2;
    background: var(--gray-50);
    box-shadow: inset 0 -1px 0 var(--gray-200);
  }
  .dtm-group-row td {
    position: sticky;
    /* topbar + one header row. The header row measures 40.5px here (0.6rem
       block padding + the uppercase 0.75rem label line), so 2.3rem parked the
       band 3.7px UNDER the header, where the header's own opaque background
       ate its top edge. 2.55rem lands it flush. Verified by MT09 and by
       scrolled screenshots taken at the breakpoint. */
    top: calc(52px + 2.55rem);
    z-index: 1;
  }
}

/* ── Mobile detail sheet (mobile-table-sheet.js) ── */
.mts-overlay {
  position: fixed;
  inset: 0;
  z-index: 95; /* above page chrome (topbar 60), below modals (100+) */
  display: none;
  background: rgba(0, 0, 0, 0.45);
}
.mts-overlay.active { display: flex; align-items: flex-end; }
.mts-sheet {
  inline-size: 100%;
  max-block-size: 80vh;
  overflow-y: auto;
  background: var(--surface);
  border-start-start-radius: 16px;
  border-start-end-radius: 16px;
  /* Block-end clears the Android gesture bar / iOS home indicator; max() keeps
     the 1.25rem floor on devices that report no inset. */
  padding: 0.4rem 1.1rem max(1.25rem, env(safe-area-inset-bottom));
  /* Literal, not --shadow-lg: that token is `0 10px 15px -3px` — a DOWNWARD
     shadow, which on a sheet pinned to the bottom edge paints entirely
     off-screen and lifts nothing. The sheet's only visible edge is its top,
     so the shadow has to point up. */
  box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.25);
}
.mts-grabber {
  inline-size: 2.4rem;
  block-size: 4px;
  border-radius: 2px;
  background: var(--gray-300);
  margin: 0.4rem auto 0.8rem;
}
.mts-title { font-size: 1.02rem; font-weight: 600; margin: 0 0 0.8rem; }
.mts-fields {
  display: grid;
  grid-template-columns: minmax(0, 8.5rem) minmax(0, 1fr);
  row-gap: 0.55rem;
  column-gap: 0.75rem;
  margin: 0 0 1rem;
}
.mts-field { display: contents; }
.mts-field dt {
  color: var(--gray-500);
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  align-self: center;
}
.mts-field dd {
  margin: 0;
  font-size: 0.9rem;
  color: var(--gray-800);
  min-width: 0;
  overflow-wrap: break-word;
}
.mts-field dd .num, .mts-field dd.num { white-space: nowrap; }
/* Actions sit side by side (owner call 2026-08-21, superseding the spec's
   stacked full-width buttons); flex row follows dir so RTL order is free. */
.mts-actions { display: flex; flex-direction: row; gap: 0.5rem; }
.mts-actions .mts-action { flex: 1; min-width: 0; justify-content: center; min-height: 2.6rem; }
.mts-reason { margin: 0.45rem 0 0; font-size: 0.8rem; color: var(--gray-500); text-align: center; }

/* ── Mobile filter sheet (mobile-table-sheet.js) ── */
.mfs-trigger { display: none; align-items: center; gap: 0.4rem; }
.mfs-trigger .mfs-count {
  background: var(--primary);
  color: #fff;
  border-radius: 999px;
  min-inline-size: 1.15rem;
  block-size: 1.15rem;
  line-height: 1.15rem;
  text-align: center;
  font-size: 0.7rem;
  font-weight: 700;
  padding-inline: 0.2rem;
}
.mfs-trigger .mfs-count:empty { display: none; }
.mfs-bar .mfs-head { display: none; }

@media (max-width: 640px) {
  .mfs-trigger { display: inline-flex; }
  .mfs-bar { display: none; }
  body.mfs-backdrop-on::after {
    content: '';
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    z-index: 89;
  }
  .mfs-bar.mfs-open {
    display: flex;
    flex-direction: column;
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    /* Fixed boxes honor margins: the bar's flow margin-bottom floated the
       whole sheet above the viewport bottom edge. !important because the
       attendance page's own `#toolbar-raw ~ .filter-bar` margin tweak
       (~line 6690) carries an ID and out-specifies any class stack. */
    margin: 0 !important;
    z-index: 90; /* above the ::after backdrop, below .mts-overlay (95) and modals (100+) */
    max-block-size: 80vh;
    overflow-y: auto;
    background: var(--surface);
    border: none; /* the card border/shadow are desktop chrome; the sheet has its own */
    border-start-start-radius: 16px;
    border-start-end-radius: 16px;
    /* The base .filter-bar card rounds ALL corners; a bottom-anchored sheet
       must sit square on the viewport edge or the page peeks through. */
    border-end-start-radius: 0;
    border-end-end-radius: 0;
    padding: 0.75rem 1.1rem max(1.25rem, env(safe-area-inset-bottom));
    box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.25);
    /* Sheet-density rhythm: the desktop bar spaces groups TWICE (flex gap +
       each .form-group's own margin-bottom), which pushed four controls past
       the 80vh cap on short viewports and forced a needless scrollbar. The
       gap alone carries the rhythm here. */
    gap: 0.75rem;
  }
  .mfs-bar.mfs-open .form-group {
    margin: 0;
  }
  /* A closed .ms-dropdown hides with visibility:hidden (kept in layout for
     the fade), and its ~500px option list dangles below the trigger. On
     desktop the bar is not a scroll container so nobody noticed; the sheet
     IS one (overflow-y:auto), so the invisible panel inflated scrollHeight
     and produced a scrollbar with nothing to scroll to. Inside the sheet a
     closed panel leaves layout entirely (the fade is sacrificed there —
     display cannot transition). */
  .mfs-bar.mfs-open .ms-container:not(.ms-open) .ms-dropdown {
    display: none;
  }
  .mfs-bar.mfs-open .mfs-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-weight: 600;
    margin-block-end: 0.5rem;
  }
  .mfs-bar.mfs-open .form-group { width: 100%; }
}

/* The offline banner is also bottom-anchored at z-index 90 — the same layer the
   open filter sheet claims — so while the sheet is up it would paint over the
   sheet's bottom band (its own controls) on a tie. The banner is a passive
   status line; the sheet is the thing the user is touching, so the banner
   stands down for as long as the sheet is open. Not scoped to the breakpoint:
   `mfs-backdrop-on` is only ever set under it. */
body.mfs-backdrop-on .offline-banner { display: none; }
