/* ==========================================================================
   Dissolvex HRMS — design system
   --------------------------------------------------------------------------
   Self-hosted. No CDN, no web fonts, no external requests of any kind.

   Two ideas carry the whole interface:

   1. Neutrals do the work. The primary action colour is graphite (inverted in
      dark), not a brand hue — so dense payroll and attendance tables stay
      readable and colour keeps its meaning.
   2. The tenant's brand colour is an ACCENT, not a paint bucket. It marks the
      active page, links, focus, and data. --brand-raw is injected per tenant on
      <html>; the dark variant is derived from it with color-mix so any customer
      hex stays legible on a dark ground.
   ========================================================================== */

:root {
  color-scheme: light;

  /* Injected per tenant; this is only the fallback. */
  --brand-raw: #3E63DD;
  --brand: var(--brand-raw);
  --brand-soft: color-mix(in srgb, var(--brand-raw) 10%, transparent);
  --brand-line: color-mix(in srgb, var(--brand-raw) 32%, transparent);

  --bg: #FAFAFB;
  --surface: #FFFFFF;
  --surface-2: #F4F5F7;
  --surface-3: #EBEDF1;
  --border: #E3E5EA;
  --border-strong: #D0D4DB;
  --border-soft: #EDEFF3;

  --ink: #14161A;
  --ink-soft: #565C68;
  /* Secondary text carries real content (dates, totals, hints), so it is held
     at WCAG AA against --surface rather than being decoratively pale. */
  --ink-faint: #6F7683;

  /* Primary action: near-black on light, near-white on dark. */
  --action: #191C21;
  --action-hover: #2A2E36;
  --action-ink: #FFFFFF;

  --ok: #1B6E45;      --ok-bg: #E4F2EA;      --ok-line: #BEDDCC;
  --warn: #8A5A0B;    --warn-bg: #FAF0DC;    --warn-line: #EBD7AB;
  --bad: #A93226;     --bad-bg: #FAE7E4;     --bad-line: #EFC7C1;
  --info: #1F5A96;    --info-bg: #E4EEF8;    --info-line: #C2D8ED;
  --muted-bg: #EDEFF3; --muted-line: #DDE0E6;

  /* Categorical series. Ordered so neighbours stay distinguishable, and held
     dark enough to carry white text and to survive being printed in greyscale. */
  --series-1: #3E63DD;
  --series-2: #1B8A6B;
  --series-3: #C2691E;
  --series-4: #8A4FBF;
  --series-5: #1F7A9E;
  --series-6: #B03A5B;
  --series-7: #6B7A2E;
  --series-8: #5A6472;


  --r-sm: 4px;
  --r: 6px;
  --r-lg: 9px;

  --shadow-pop: 0 10px 32px -8px rgba(15, 20, 28, .22), 0 2px 6px rgba(15, 20, 28, .06);
  /* Resting elevation. A card should sit on the page, not be drawn onto it —
     one hairline plus a shadow this soft reads as a surface where a 1px box
     reads as a wireframe. */
  --shadow-card: 0 1px 2px rgba(15, 20, 28, .04), 0 1px 3px -1px rgba(15, 20, 28, .05);

  /* Motion. Everything that moves in this product picks from these five
     durations and three curves — the same discipline the colours get, applied
     to the one subsystem that previously had none. Eight ad-hoc durations and
     the browser default `ease` is what "generic" feels like.

     The curves are stronger than the CSS built-ins on purpose: `ease-out` at
     200ms reads as instant, where the built-in at the same duration reads as
     soft. Never `ease-in` on UI — it holds still through the exact moment the
     user is looking at it. */
  --ease-out: cubic-bezier(.23, 1, .32, 1);
  --ease-in-out: cubic-bezier(.77, 0, .175, 1);
  /* The iOS drawer curve: leaves fast, arrives with almost no tail. */
  --ease-drawer: cubic-bezier(.32, .72, 0, 1);
  /* --ease-out reflected through the diagonal, which is what makes a surface
     leave along the path it arrived on instead of merely reversing position.
     Only ever for exits: a thing on its way out is not being waited for, and
     this curve is slow at the start, which on the way *in* would hold still
     through the exact moment the user is looking. */
  --ease-out-reverse: cubic-bezier(.68, 0, .77, 0);

  --dur-press: .14s;   /* a press must land inside the time a finger rests */
  --dur-fast: .16s;    /* hover, colour, border */
  --dur-pop: .2s;      /* menus and popovers */
  --dur-modal: .25s;   /* the one surface allowed to take its time */
  --dur-drawer: .42s;  /* travels the whole screen width, so it earns more */

  /* Materials. Held as tokens because three different media queries need to
     override them together — reduced transparency, forced contrast, and print
     all want the same surfaces to go solid.

     There are three weights, and which one a surface gets is decided by what
     the surface is for rather than by where it sits:

     --chrome        The lightest. The utility bar: a thin strip that should
                     draw the eye to the controls on it without becoming a
                     region of its own.
     --material      Heavier, for a structural region — the navigation drawer,
                     which is a different place rather than a layer over this
                     one, and has to read as separate from the page it covers.
     --material-pop  Heaviest, and the reason is legibility rather than
                     hierarchy: a menu opens *over* the utility bar, and a
                     light translucent surface on top of another light
                     translucent surface stops being readable. Stacking
                     materials means going thicker, not thinner.

     Blur scales with the surface. A bigger pane of glass is a thicker pane. */
  --chrome: color-mix(in srgb, var(--surface) 72%, transparent);
  --chrome-blur: saturate(180%) blur(20px);

  --material: color-mix(in srgb, var(--surface-2) 82%, transparent);
  --material-blur: saturate(180%) blur(30px);

  --material-pop: color-mix(in srgb, var(--surface) 88%, transparent);
  --material-pop-blur: saturate(200%) blur(30px);

  /* Depth follows size for the same reason. The drawer covers most of a phone
     screen and has to separate from everything behind it; a menu is small and
     close to the surface it came from. */
  --shadow-drawer: 0 24px 64px -16px rgba(15, 20, 28, .38), 0 4px 12px rgba(15, 20, 28, .1);

  --sidebar-w: 244px; /* fits the longest group name, "Performance Management" */
  --utility-h: 44px;

  --font: "Segoe UI Variable Text", "Segoe UI", -apple-system, BlinkMacSystemFont,
          "SF Pro Text", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-display: "Segoe UI Variable Display", "Segoe UI", -apple-system,
          BlinkMacSystemFont, "SF Pro Display", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "Cascadia Mono", "SF Mono", "JetBrains Mono", Consolas,
          "Liberation Mono", monospace;
}

/* Dark tokens, defined once and applied in two situations: the OS asks for dark
   and the user has not overridden it, or the user explicitly chose dark. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --brand: color-mix(in srgb, var(--brand-raw) 62%, #E9EFFF);
    --brand-soft: color-mix(in srgb, var(--brand-raw) 22%, transparent);
    --brand-line: color-mix(in srgb, var(--brand-raw) 45%, transparent);

    --bg: #0C0E11;
    --surface: #14171C;
    --surface-2: #1A1E24;
    --surface-3: #222730;
    --border: #262B33;
    --border-strong: #363D48;
    --border-soft: #1E232B;

    --ink: #E7EAEF;
    --ink-soft: #9BA3B1;
    --ink-faint: #8A92A0;

    --action: #E7EAEF;
    --action-hover: #FFFFFF;

    /* Lifted and desaturated for a dark ground, where the light-mode values
       read as muddy against #14171C. */
    --series-1: #7B96F0;
    --series-2: #4FC49C;
    --series-3: #E9A05C;
    --series-4: #B98AE0;
    --series-5: #5FB6D6;
    --series-6: #E0788F;
    --series-7: #A8B85F;
    --series-8: #98A2B3;

    --action-ink: #14161A;

    --ok: #63C48D;      --ok-bg: #12251A;      --ok-line: #1F4A34;
    --warn: #D8A85A;    --warn-bg: #241D10;    --warn-line: #4A3B1C;
    --bad: #E08579;     --bad-bg: #261412;     --bad-line: #4E2822;
    --info: #6FA8DC;    --info-bg: #10202E;    --info-line: #1F3D57;
    --muted-bg: #1E232A; --muted-line: #2C323B;

    --shadow-pop: 0 12px 34px -8px rgba(0, 0, 0, .6), 0 2px 8px rgba(0, 0, 0, .35);
    --shadow-drawer: 0 24px 64px -16px rgba(0, 0, 0, .72), 0 4px 12px rgba(0, 0, 0, .4);
    --shadow-card: 0 1px 2px rgba(0, 0, 0, .3);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --brand: color-mix(in srgb, var(--brand-raw) 62%, #E9EFFF);
  --brand-soft: color-mix(in srgb, var(--brand-raw) 22%, transparent);
  --brand-line: color-mix(in srgb, var(--brand-raw) 45%, transparent);

  --bg: #0C0E11;
  --surface: #14171C;
  --surface-2: #1A1E24;
  --surface-3: #222730;
  --border: #262B33;
  --border-strong: #363D48;
  --border-soft: #1E232B;

  --ink: #E7EAEF;
  --ink-soft: #9BA3B1;
  --ink-faint: #8A92A0;

  --action: #E7EAEF;
  --action-hover: #FFFFFF;

  /* Lifted and desaturated for a dark ground, where the light-mode values
     read as muddy against #14171C. */
  --series-1: #7B96F0;
  --series-2: #4FC49C;
  --series-3: #E9A05C;
  --series-4: #B98AE0;
  --series-5: #5FB6D6;
  --series-6: #E0788F;
  --series-7: #A8B85F;
  --series-8: #98A2B3;

  --action-ink: #14161A;

  --ok: #63C48D;      --ok-bg: #12251A;      --ok-line: #1F4A34;
  --warn: #D8A85A;    --warn-bg: #241D10;    --warn-line: #4A3B1C;
  --bad: #E08579;     --bad-bg: #261412;     --bad-line: #4E2822;
  --info: #6FA8DC;    --info-bg: #10202E;    --info-line: #1F3D57;
  --muted-bg: #1E232A; --muted-line: #2C323B;

  --shadow-pop: 0 12px 34px -8px rgba(0, 0, 0, .6), 0 2px 8px rgba(0, 0, 0, .35);
  --shadow-drawer: 0 24px 64px -16px rgba(0, 0, 0, .72), 0 4px 12px rgba(0, 0, 0, .4);
  --shadow-card: 0 1px 2px rgba(0, 0, 0, .3);
}

/* ==========================================================================
   Base
   ========================================================================== */

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

html { font-size: 16px; }
html, body { height: 100%; }

/* --------------------------------------------------------------------------
   Touch platform baseline. Each of these removes one tell that gives away a
   web page on a phone; none of them reproduce in a desktop browser's device
   emulator, which is why they survived this long.
   -------------------------------------------------------------------------- */
html {
  /* Mobile browsers paint a grey wash over anything tappable. It is the
     loudest "this is a website" signal there is, and it fights the press
     states defined further down. Removing it makes those states the only
     feedback, so every tappable thing below has one. */
  -webkit-tap-highlight-color: transparent;
  /* Stops Safari inflating body copy when the phone is turned sideways. */
  -webkit-text-size-adjust: 100%;
}

/* Pull-to-refresh belongs to a document you are reading, not to an app you are
   working in: dragging down at the top of the leave list should not reload it.
   The page keeps its own overscroll; only the chaining to the browser goes. */
html, body { overscroll-behavior-y: none; }

/* Inner scrollers stop handing their momentum to the page behind them once
   they hit the end — the difference between a sheet that scrolls and a sheet
   that drags the whole page with it. */
.panel, .table-wrap, .pop-list { overscroll-behavior: contain; }

/* Controls are controls. Long-pressing a button should not select its label or
   raise the copy callout, and no control should ever wait 300ms to see whether
   a second tap is coming. `a` gets the delay removed but stays selectable —
   people copy links, employee codes and error text out of this product. */
button, [role="button"], .btn, .icon-btn, .tabs a, .pager-page, .pager-step,
.pop-item, .navgroup-head, .navgroup-links a, .sidebar-signout {
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
}
button, a, [role="button"], summary, label, .btn, .icon-btn {
  touch-action: manipulation;
}

body {
  margin: 0;
  font-family: var(--font);
  font-size: .875rem;
  line-height: 1.5;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-variant-numeric: tabular-nums;
}

a { color: var(--brand); text-decoration: none; }
@media (hover: hover) and (pointer: fine) {
  a:hover { text-decoration: underline; text-underline-offset: 2px; }
}


h1, h2, h3, h4 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: -.014em;
  color: var(--ink);
}
h1 { font-size: 1.375rem; line-height: 1.25; }
h2 { font-size: 1rem; line-height: 1.35; }
h3 { font-size: .875rem; }

p { margin: 0 0 .625rem; }
p:last-child { margin-bottom: 0; }

:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
  border-radius: 3px;
}

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

.icon {
  width: 16px; height: 16px;
  flex: 0 0 auto;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
}
.icon-sm { width: 14px; height: 14px; }
.icon-lg { width: 20px; height: 20px; }

/* ==========================================================================
   Shell
   ========================================================================== */

.shell { display: flex; min-height: 100vh; min-height: 100dvh; }

/* The content column. min-width:0 is load-bearing: without it a flex child
   refuses to shrink below the width of its widest content, so one wide table
   pushes the whole page into horizontal scroll. */
.main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* --------------------------------------------------------------------------
   Navigation: one sidebar, sections as folding groups. Nine headings is a
   short list; forty links is not, so only the group you are in is unfolded.

   This was two columns — a rail of icons, then a panel of links. It read as
   two menus rather than one, and a 68px icon has to be decoded before it says
   anything a word would have said outright.
   -------------------------------------------------------------------------- */

.sidebar {
  width: var(--sidebar-w);
  flex: 0 0 var(--sidebar-w);
  background: var(--surface-2);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  /* dvh is the visible viewport, vh is the viewport as if no browser chrome
     existed. On a phone that difference is about 90px of Safari toolbar
     sitting on top of whatever is at the bottom of this element — which is
     Sign out. The vh line stays as the fallback for browsers without dvh. */
  height: 100vh;
  height: 100dvh;
  /* The sidebar itself never scrolls; the group list inside it does. Sign out
     and the organization name stay put because they are siblings of the
     scrollport rather than sticky children of it — an earlier version pinned
     them with position:sticky and, once the list was long enough to overflow,
     `margin-top:auto` put the footer nowhere near the bottom and it drew
     straight over the links. */
  overflow: hidden;
}
.sidebar-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  flex: 0 0 auto;
  padding: .625rem .75rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.sidebar-brand {
  display: flex;
  align-items: center;
  gap: .5rem;
  min-width: 0;
  flex: 1 1 auto;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
@media (hover: hover) and (pointer: fine) {
  .sidebar-brand:hover { text-decoration: none; }
}

/* A keyboard has to be able to see where it is; the ring was taken away
   with nothing put in its place. :focus-visible keeps it off mouse clicks. */
.sidebar-brand:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.sidebar-mark, .sidebar-brand img {
  width: 28px; height: 28px; flex: 0 0 28px;
  border-radius: 7px; object-fit: cover;
}
.sidebar-mark {
  background: var(--brand); color: #fff;
  display: flex; align-items: center; justify-content: center;
  font-size: .6875rem; font-weight: 700; letter-spacing: .02em;
}
/* An uploaded logo gets its own frame. It was drawn on a transparent square
   with no edge, so a white mark vanished on the light theme and a black one on
   the dark theme; `cover` also cropped the tips off wide marks. */
.sidebar-brand img {
  display: block;
  padding: 3px;
  object-fit: contain;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  box-sizing: border-box;
}
.sidebar-names { min-width: 0; line-height: 1.2; }
.sidebar-org {
  display: block; font-weight: 600; font-size: .8125rem; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sidebar-kind { display: block; font-size: .625rem; color: var(--ink-faint); }
.sidebar-head .menu-toggle {
  background: var(--surface);
  border-color: var(--border);
}
@media (hover: hover) and (pointer: fine) {
  .sidebar-head .menu-toggle:hover {
    background: var(--surface-3);
    border-color: var(--border-strong);
  }
}


.sidebar-groups {
  flex: 1 1 auto;
  /* Load-bearing: without it a flex child refuses to shrink below its content
     height, so the list would push the footer off the bottom instead of
     scrolling. Same rule as .main and for the same reason. */
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: .375rem .5rem;
}
.sidebar-groups::-webkit-scrollbar { width: 8px; }
.sidebar-groups::-webkit-scrollbar-thumb { background: var(--border); border-radius: 8px; }

.navgroup + .navgroup { margin-top: 1px; }
.navgroup-head {
  display: flex; align-items: center; gap: .5rem;
  padding: .4375rem .5rem;
  border-radius: 7px;
  cursor: pointer;
  color: var(--ink-soft);
  font-size: .8125rem;
  font-weight: 600;
  list-style: none;
  user-select: none;
}
/* Safari still draws its own triangle without this. */
.navgroup-head::-webkit-details-marker { display: none; }
@media (hover: hover) and (pointer: fine) {
  .navgroup-head:hover { background: var(--surface-3); color: var(--ink); }
}

.navgroup-head .icon { width: 17px; height: 17px; flex: 0 0 17px; }
.navgroup-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.navgroup-caret {
  width: 13px; height: 13px; flex: 0 0 13px;
  color: var(--ink-faint);
  transition: transform .15s ease;
}
.navgroup[open] > .navgroup-head { color: var(--ink); }
.navgroup[open] > .navgroup-head .navgroup-caret { transform: rotate(180deg); }
.navgroup-head:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

/* The rule marks the group as one run of pages rather than loose links. */
.navgroup-links {
  display: flex; flex-direction: column; gap: 1px;
  margin: 1px 0 .25rem;
  padding-left: 1.5rem;
  border-left: 1px solid var(--border);
  margin-left: 1.0625rem;
}
.navgroup-links a {
  display: flex; align-items: center; gap: .375rem;
  padding: .34375rem .5rem;
  border-radius: 6px;
  font-size: .78125rem;
  color: var(--ink-soft);
}
@media (hover: hover) and (pointer: fine) {
  .navgroup-links a:hover { background: var(--surface-3); color: var(--ink); text-decoration: none; }
}

.navgroup-links a.active { background: var(--brand-soft); color: var(--brand); font-weight: 600; }
.navgroup-link-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.sidebar-foot {
  flex: 0 0 auto;
  padding: .375rem .5rem .5rem;
  padding-bottom: calc(.5rem + env(safe-area-inset-bottom));
  border-top: 1px solid var(--border);
  background: var(--surface-2);
}
.sidebar-signout {
  display: flex; align-items: center; gap: .5rem;
  padding: .4375rem .5rem;
  border-radius: 7px;
  font-size: .8125rem;
  color: var(--ink-soft);
  width: 100%;
  border: 0;
  background: transparent;
  font-family: inherit;
  cursor: pointer;
  text-align: left;
}
@media (hover: hover) and (pointer: fine) {
  .sidebar-signout:hover { background: var(--surface-3); color: var(--ink); text-decoration: none; }
}

.sidebar-signout .icon { width: 17px; height: 17px; }

.nav-count {
  font-size: .625rem;
  font-weight: 600;
  background: var(--muted-bg);
  color: var(--ink-soft);
  padding: .0625rem .3125rem;
  border-radius: 20px;
  min-width: 17px;
  text-align: center;
}
.navgroup-links a.active .nav-count { background: var(--brand); color: #fff; }

/* ==========================================================================
   Content
   ========================================================================== */

.content { padding: 1.25rem; flex: 1; }
.content-narrow { max-width: 46rem; }

.grid { display: grid; gap: .875rem; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(11.5rem, 1fr)); }
.grid-5 { grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr)); }
.stack { display: flex; flex-direction: column; gap: .875rem; }
.row { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: .625rem; flex-wrap: wrap; }
.spacer { flex: 1; }

/* ==========================================================================
   Cards
   ========================================================================== */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  overflow: hidden;
}

.card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .625rem;
  padding: .6875rem .875rem;
  border-bottom: 1px solid var(--border);
}
.card-head h2 { font-size: .8125rem; font-weight: 600; letter-spacing: -.005em; }
.card-head h3 { font-size: .8125rem; }

.card-body { padding: .875rem; }

.card-foot {
  padding: .625rem .875rem;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  font-size: .75rem;
  color: var(--ink-soft);
}

/* ==========================================================================
   Stats
   ========================================================================== */

.stat { padding: .875rem; }
.stat-label {
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .085em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.stat-value {
  font-family: var(--font-display);
  font-size: 1.75rem;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -.03em;
  margin-top: .25rem;
  color: var(--ink);
}
.stat-note { font-size: .75rem; color: var(--ink-faint); margin-top: .1875rem; }

/* ==========================================================================
   Tables — ledger-like: hairline rows, no zebra, tabular figures
   ========================================================================== */

.table-wrap { overflow-x: auto; }

table.data {
  width: 100%;
  border-collapse: collapse;
  font-size: .8125rem;
}
table.data th {
  text-align: left;
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .075em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding: .5rem .75rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
  white-space: nowrap;
  position: sticky;
  top: 0;
}
table.data td,
table.data th,
.payslip-table td,
.payslip-table th {
  display: table-cell !important;
}

table.data td {
  padding: .5625rem .75rem;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
table.data tbody tr:last-child td { border-bottom: 0; }
@media (hover: hover) and (pointer: fine) {
  table.data tbody tr:hover { background: var(--surface-2); }
}

table.data td.num, table.data th.num { text-align: right; }

/* Key/value tables inside cards read better without the sticky header chrome. */
.card-body table.data th {
  background: transparent;
  position: static;
  text-transform: none;
  letter-spacing: 0;
  font-size: .75rem;
  font-weight: 500;
  color: var(--ink-faint);
  width: 40%;
}
.card-body table.data tbody tr:last-child td {
  border-bottom: 1px solid var(--border);
}

.empty {
  padding: 2rem 1rem;
  text-align: center;
  color: var(--ink-faint);
  font-size: .8125rem;
}
.empty strong {
  display: block;
  color: var(--ink);
  font-weight: 600;
  margin-bottom: .25rem;
  font-size: .875rem;
}

/* ==========================================================================
   Pills
   ========================================================================== */

.pill {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  font-size: .6875rem;
  font-weight: 600;
  line-height: 1.4;
  padding: .0625rem .375rem;
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  white-space: nowrap;
}
.pill-ok    { background: var(--ok-bg);    color: var(--ok);    border-color: var(--ok-line); }
.pill-warn  { background: var(--warn-bg);  color: var(--warn);  border-color: var(--warn-line); }
.pill-bad, .pill-danger { background: var(--bad-bg); color: var(--bad); border-color: var(--bad-line); }
.pill-info  { background: var(--info-bg);  color: var(--info);  border-color: var(--info-line); }
.pill-muted { background: var(--muted-bg); color: var(--ink-soft); border-color: var(--muted-line); }
.pill-brand { background: var(--brand-soft); color: var(--brand); border-color: var(--brand-line); }

/* ==========================================================================
   Buttons — graphite primary, quiet secondary
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .375rem;
  padding: .375rem .6875rem;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--ink);
  font-size: .8125rem;
  font-weight: 500;
  font-family: inherit;
  line-height: 1.4;
  cursor: pointer;
  white-space: nowrap;
  transition: background .12s ease, border-color .12s ease;
}
@media (hover: hover) and (pointer: fine) {
  .btn:hover { background: var(--surface-2); text-decoration: none; }
}


.btn-primary {
  background: var(--action);
  border-color: var(--action);
  color: var(--action-ink);
  font-weight: 600;
}
@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover { background: var(--action-hover); border-color: var(--action-hover); }
}


.btn-danger { color: var(--bad); border-color: var(--bad-line); background: var(--surface); }
@media (hover: hover) and (pointer: fine) {
  .btn-danger:hover { background: var(--bad-bg); }
}


.btn-ghost { border-color: transparent; background: transparent; color: var(--ink-soft); }
@media (hover: hover) and (pointer: fine) {
  .btn-ghost:hover { background: var(--surface-2); color: var(--ink); }
}


.btn-sm { padding: .1875rem .4375rem; font-size: .75rem; }
.btn-block { width: 100%; }
.btn:disabled { opacity: .45; cursor: not-allowed; }

/* ==========================================================================
   Forms
   ========================================================================== */

/* A 12-column grid so each field can take the width its content deserves.
   Spans are chosen by core/templatetags/form_extras.py from the widget type —
   dates and numbers stay narrow, prose runs full width, checkboxes get a row. */
.form-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: .875rem 1rem;
  max-width: 40rem;
}
.form-grid.is-wide { max-width: 58rem; }

.field { display: flex; flex-direction: column; gap: .3125rem; min-width: 0; }

/* The grid is twelve columns, so a field with no span class was taking one of
   them — a 40px cell holding a select that needed 98. That is what a hand-rolled
   row looks like when it forgets the class partials/form.html adds for it, and
   it is silent: the page still renders, just unreadably. Half width is the
   default now, so forgetting it degrades instead of collapsing.

   This rule must stay above the modifiers: it has the same specificity, so
   order is the only thing letting `is-full` win. */
.form-grid > .field { grid-column: span 6; }

/* Two even columns by default, three when the form is wide. Mixing 4- and
   6-column spans wrapped raggedly and left orphan fields stranded on their own
   row; even columns always pair up. Individual controls are capped below so a
   date still looks like a date inside its cell. */
.field.is-narrow,
.field.is-mid    { grid-column: span 6; }
.form-grid.is-wide .field.is-narrow,
.form-grid.is-wide .field.is-mid { grid-column: span 4; }

.field.is-full   { grid-column: span 12; }
.field.is-check  { grid-column: span 12; }
.field-wide      { grid-column: 1 / -1; }

/* Short, known-length values should not stretch to fill their column. */
.field input[type="date"],
.field input[type="time"],
.field input[type="datetime-local"],
.field input[type="number"] { max-width: 13rem; }

.field label {
  font-size: .75rem;
  font-weight: 600;
  color: var(--ink-soft);
  letter-spacing: .005em;
}
.field .req { color: var(--bad); margin-left: .125rem; font-weight: 700; }
.field .help { font-size: .6875rem; color: var(--ink-faint); line-height: 1.45; }
.field .error { font-size: .6875rem; color: var(--bad); font-weight: 500; }

.form-input, .form-select {
  width: 100%;
  min-height: 2.25rem;
  padding: .4375rem .625rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font-size: .8125rem;
  font-family: inherit;
  line-height: 1.45;
  transition: border-color .12s ease, box-shadow .12s ease;
}
.form-input::placeholder { color: var(--ink-faint); }

@media (hover: hover) and (pointer: fine) {
  .form-input:hover, .form-select:hover { border-color: var(--ink-faint); }
}

.form-input:focus, .form-select:focus {
  outline: none;
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-soft);
}
.field.has-error .form-input,
.field.has-error .form-select { border-color: var(--bad); }

/* Native select chevrons are inconsistent across platforms; this one is drawn
   inline so it matches the rest of the UI and needs no network request. */
.form-select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 1.875rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.75 6 6.5l5-4.75' fill='none' stroke='%23808894' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .625rem center;
  background-size: 11px 8px;
}
select[multiple].form-select { background-image: none; padding-right: .625rem; }

textarea.form-input { resize: vertical; min-height: 5.5rem; line-height: 1.55; }

input[type="color"].form-input {
  padding: .1875rem;
  height: 2.25rem;
  cursor: pointer;
  max-width: 5rem;
}

/* The date picker glyph is near-invisible on a dark ground. */
:root[data-theme="dark"] input::-webkit-calendar-picker-indicator { filter: invert(.75); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) input::-webkit-calendar-picker-indicator { filter: invert(.75); }
}

input[type="file"].form-input { padding: .3125rem .5rem; cursor: pointer; }

/* A file that already has a value: what is there, and the two things you can
   do about it, on one row — not "Currently / Clear / Change" down the page. */
.file-field { display: flex; flex-direction: column; gap: .4375rem; }
.file-current {
  display: flex;
  align-items: center;
  gap: .5625rem;
  padding: .4375rem .5rem;
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface-2);
}
.file-thumb {
  flex: 0 0 auto;
  width: 34px; height: 34px;
  object-fit: contain;
  border-radius: var(--r-sm);
  background: var(--surface);
  border: 1px solid var(--border);
}
.file-thumb-doc {
  display: flex; align-items: center; justify-content: center;
  color: var(--ink-faint);
}
.file-name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: .8125rem;
  color: var(--ink-soft);
  text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
@media (hover: hover) and (pointer: fine) {
  .file-name:hover { text-decoration: underline; }
}
.file-clear {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: .375rem;
  font-size: .75rem;
  color: var(--ink-faint);
  cursor: pointer;
}
.file-clear input { width: 14px; height: 14px; }

/* The browser's own "Choose File" button is the one control on the page that
   ignores the theme, so it is restyled to match the secondary buttons. */
input[type="file"].form-input::file-selector-button {
  margin-right: .5625rem;
  padding: .25rem .5625rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--ink);
  font: inherit;
  font-size: .75rem;
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  input[type="file"].form-input::file-selector-button:hover { background: var(--surface); }
}

/* Password visibility toggle wrapper & button */
.password-wrap {
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;
}
.password-wrap .form-input,
.password-wrap input {
  width: 100%;
  padding-right: 2.5rem;
}
.password-toggle-btn {
  /* Centred with auto margins, not translateY: the press effect scales the
     button, and a transform there replaced the centring — the button dropped
     half its height mid-click and the click landed outside it. */
  position: absolute;
  right: .375rem;
  top: 0;
  bottom: 0;
  margin: auto 0;
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
  border-radius: var(--r-sm);
  transition: color .15s ease, background-color .15s ease;
  z-index: 2;
}
@media (hover: hover) and (pointer: fine) {
  .password-toggle-btn:hover {
    color: var(--ink);
    background-color: var(--surface-2);
  }
}

.password-toggle-btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}
.password-toggle-btn .icon,
.icon-eye,
.icon-eye-off {
  pointer-events: none;
  flex-shrink: 0;
}

/* Checkboxes read as a decision, not as a field with a stray box next to it. */
.form-check {
  width: 16px; height: 16px;
  accent-color: var(--brand);
  cursor: pointer;
  flex: 0 0 auto;
  margin: 0;
}
.check-row {
  display: flex;
  align-items: flex-start;
  gap: .5625rem;
  padding: .5625rem .6875rem;
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface-2);
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .check-row:hover { border-color: var(--border-strong); }
}

.check-row:has(:checked) {
  border-color: var(--brand-line);
  background: var(--brand-soft);
}
.check-text { display: flex; flex-direction: column; gap: .0625rem; min-width: 0; }
.check-title { font-size: .8125rem; font-weight: 500; color: var(--ink); line-height: 1.35; }

ul.checkbox-list {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
  gap: .375rem;
}
ul.checkbox-list li { display: flex; align-items: center; gap: .4375rem; font-size: .8125rem; }

/* A long checkbox list — every employee, every component — is only workable
   if it stays one box tall and can be searched. */
.picker {
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface-2);
  overflow: hidden;
}
.picker-bar {
  display: flex;
  gap: .375rem;
  align-items: center;
  padding: .5rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
}
.picker-search { flex: 1 1 auto; min-width: 0; }
.picker-bar .btn { flex: 0 0 auto; white-space: nowrap; }
.picker-list {
  list-style: none; margin: 0; padding: .375rem;
  max-height: 15rem;
  overflow-y: auto;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
  gap: .125rem;
  align-content: start;
}
.picker-row label {
  display: flex;
  align-items: center;
  gap: .4375rem;
  padding: .3125rem .4375rem;
  border-radius: var(--r-sm);
  font-size: .8125rem;
  font-weight: 400;
  color: var(--ink);
  cursor: pointer;
  min-width: 0;
}
@media (hover: hover) and (pointer: fine) {
  .picker-row label:hover { background: var(--surface-2); }
}
.picker-row label:has(:checked) { background: var(--brand-soft); color: var(--ink); }
.picker-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picker-row.is-hidden { display: none; }
.picker-none { padding: .625rem .4375rem; font-size: .8125rem; }
.picker-foot {
  margin: 0;
  padding: .4375rem .5625rem;
  border-top: 1px solid var(--border);
  background: var(--surface);
  font-size: .75rem;
}
@media (max-width: 640px) {
  .picker-bar { flex-wrap: wrap; }
  .picker-search { flex: 1 0 100%; }
  .picker-list { grid-template-columns: 1fr; max-height: 13rem; }
}

/* Actions sit on a tinted footer rather than floating under a hairline. */
.form-actions {
  display: flex;
  gap: .5rem;
  align-items: center;
  padding: .75rem .875rem;
  margin: .875rem -.875rem -.875rem;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
}
.form-actions .spacer { flex: 1; }

/* Inline QR. The generated SVG carries its own mm dimensions, so they are
   overridden here and the shapes are recoloured to follow the theme — a black
   QR on a dark surface is unscannable. */
.qr {
  flex: 0 0 auto;
  width: 140px; height: 140px;
  padding: .5rem;
  background: #FFFFFF;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
.qr svg { width: 100%; height: 100%; display: block; }
.qr svg path { fill: #14161A; }

@media (max-width: 720px) {
  .form-grid { grid-template-columns: repeat(4, 1fr); }
  .field.is-narrow, .field.is-mid, .field.is-full, .field.is-check { grid-column: span 4; }
}

.filters {
  display: flex;
  gap: .5rem;
  flex-wrap: wrap;
  align-items: flex-end;
  margin-bottom: .875rem;
}
.filters .form-input, .filters .form-select { width: auto; min-width: 9rem; }

/* ==========================================================================
   Messages
   ========================================================================== */

.messages { display: flex; flex-direction: column; gap: .4375rem; margin-bottom: .875rem; }
.msg {
  padding: .5625rem .75rem;
  border-radius: var(--r-sm);
  font-size: .8125rem;
  border: 1px solid transparent;
  display: flex;
  gap: .5rem;
  align-items: flex-start;
  transition: opacity .15s ease, transform .15s ease;
}
.msg > span {
  flex: 1 1 auto;
  min-width: 0;
  line-height: 1.4;
}
.msg-close {
  margin-left: auto;
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 0 .25rem;
  font-size: 1.125rem;
  line-height: 1;
  color: currentColor;
  opacity: .55;
  border-radius: 3px;
  transition: opacity .12s ease;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  align-self: flex-start;
  margin-top: -1px;
}
@media (hover: hover) and (pointer: fine) {
  .msg-close:hover {
    opacity: 1;
  }
}

.msg-close:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 1px;
}
.msg-success { background: var(--ok-bg);   color: var(--ok);   border-color: var(--ok-line); }
.msg-error   { background: var(--bad-bg);  color: var(--bad);  border-color: var(--bad-line); }
.msg-warning { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-line); }
.msg-info    { background: var(--info-bg); color: var(--info); border-color: var(--info-line); }

/* ==========================================================================
   Data viz
   ========================================================================== */

.bar-track {
  height: 5px;
  background: var(--muted-bg);
  border-radius: 20px;
  overflow: hidden;
}
.bar-fill { height: 100%; background: var(--brand); border-radius: 20px; }
.bar-fill.is-warn { background: var(--warn); }
.bar-fill.is-bad { background: var(--bad); }

.chart {
  display: flex;
  align-items: flex-end;
  gap: .375rem;
  height: 6.5rem;
  padding-top: .375rem;
}
/* The column must be full height or the bar's percentage height has nothing to
   resolve against and every bar collapses to its min-height. */
.chart-col {
  flex: 1;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: center;
  gap: .25rem;
}
.chart-bar {
  width: 100%;
  max-width: 2.5rem;
  background: var(--brand);
  border-radius: 3px 3px 0 0;
  min-height: 3px;
}
.chart-label { font-size: .625rem; color: var(--ink-faint); }
.chart-value { font-size: .6875rem; font-weight: 600; color: var(--ink-soft); }

/* ==========================================================================
   Lists, meta, misc
   ========================================================================== */

.list-plain { list-style: none; margin: 0; padding: 0; }
.list-plain li {
  padding: .5rem 0;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: .5rem;
}
.list-plain li:first-child { padding-top: 0; }
.list-plain li:last-child { border-bottom: 0; padding-bottom: 0; }

.meta { font-size: .75rem; color: var(--ink-faint); }
.mono { font-family: var(--mono); font-size: .78em; letter-spacing: -.01em; }
.strong { font-weight: 600; color: var(--ink); }
.tiny { font-size: .6875rem; }
.muted { color: var(--ink-faint); }
.nowrap { white-space: nowrap; }
.tabular { font-variant-numeric: tabular-nums; }

.person { display: flex; align-items: center; gap: .4375rem; min-width: 0; }
.person-name { font-weight: 500; color: var(--ink); }
@media (hover: hover) and (pointer: fine) {
  a.person-name:hover { color: var(--brand); }
}


/* In tables, cells must retain table-cell display so row borders and heights collapse continuously */
td.person,
th.person {
  display: table-cell;
  vertical-align: middle;
  white-space: nowrap;
}
td.person > *,
th.person > * {
  display: inline-block;
  vertical-align: middle;
}
td.person > .avatar,
th.person > .avatar {
  display: inline-grid;
  margin-right: .4375rem;
}

td.row,
th.row {
  display: table-cell;
  vertical-align: middle;
  white-space: nowrap;
}
td.row > *,
th.row > * {
  display: inline-block;
  vertical-align: middle;
}
td.row > *:not(:last-child),
th.row > *:not(:last-child) {
  margin-right: .45rem;
}

.tabs {
  display: flex;
  gap: .125rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: .875rem;
  flex-wrap: wrap;
}
.tabs a {
  padding: .4375rem .625rem;
  font-size: .8125rem;
  font-weight: 500;
  color: var(--ink-soft);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  display: flex;
  align-items: center;
  gap: .3125rem;
}
@media (hover: hover) and (pointer: fine) {
  .tabs a:hover { color: var(--ink); text-decoration: none; }
}

.tabs a.active { color: var(--ink); border-bottom-color: var(--brand); font-weight: 600; }

.board { display: grid; grid-template-columns: repeat(5, minmax(10rem, 1fr)); gap: .5rem; overflow-x: auto; }
.board-col {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: .5rem;
  min-width: 10rem;
}
.board-col h4 {
  font-size: .625rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .075em;
  color: var(--ink-faint);
  margin-bottom: .4375rem;
}
.board-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: .4375rem .5rem;
  margin-bottom: .3125rem;
  font-size: .75rem;
}
.board-card a { color: var(--ink); font-weight: 500; }

/* ==========================================================================
   Notification bell + theme menu
   ========================================================================== */

.pop-wrap { position: relative; }

.bell-dot {
  position: absolute;
  top: -4px; right: -4px;
  min-width: 15px; height: 15px;
  padding: 0 3px;
  border-radius: 20px;
  background: var(--bad);
  color: #fff;
  font-size: .5625rem;
  font-weight: 700;
  display: grid;
  place-items: center;
  border: 1.5px solid var(--surface);
}

/* Hidden by visibility rather than by `display`, so it has two states to
   animate between. `visibility` matters as much as `opacity` here: it is what
   takes the menu out of the tab order and off the hit-testing map while it is
   shut, which `opacity: 0` alone does not do — an invisible menu that still
   catches clicks is worse than one that never animates.

   The pair transitions together, with visibility stepping at the end of the
   close so the menu stays hittable for exactly as long as it is visible. */
.pop {
  visibility: hidden;
  opacity: 0;
  position: absolute;
  right: 0; top: calc(100% + 6px);
  min-width: 12rem;
  /* Solid, not a translucent material. The menus sit over dense pages —
     balance bars, headings, buttons — and even blurred, that content read
     straight through the notification list. */
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  box-shadow: var(--shadow-pop);
  z-index: 60;
  overflow: hidden;
  /* Out of the corner nearest its button. A menu that scales out of its own
     centre looks like it arrived from somewhere else; this one looks like it
     came out of the thing that was pressed, which is the whole point. */
  transform: scale(.96);
  transform-origin: top right;
  /* The closing curve is --ease-out reflected, so the menu leaves along the
     path it arrived on. Declared here rather than on the open state because
     this is the rule that applies once the class comes off — which is exactly
     when the menu is closing. */
  transition: opacity var(--dur-pop) var(--ease-out-reverse),
              transform var(--dur-pop) var(--ease-out-reverse),
              visibility var(--dur-pop);
}
.pop-wide { width: 20rem; }

/* Opened by click, never by hover.
   These menus contain actions that change state — the appearance menu writes a
   preference. On hover, simply moving the pointer across the appearance button
   on the way to the bell drops the menu open underneath the cursor, and the
   click meant for the bell lands on a theme option instead. */
.pop-wrap.is-open .pop {
  visibility: visible;
  opacity: 1;
  transform: scale(1);
  /* Opening takes the forward curve. The pair is what makes the motion
     reversible rather than merely two-way. */
  transition: opacity var(--dur-pop) var(--ease-out),
              transform var(--dur-pop) var(--ease-out),
              visibility var(--dur-pop);
}

.pop-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: .5rem .625rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
  font-size: .75rem;
}

.pop-item {
  display: flex;
  gap: .4375rem;
  align-items: flex-start;
  padding: .5rem .625rem;
  border-bottom: 1px solid var(--border);
  color: var(--ink);
  font-size: .8125rem;
  width: 100%;
  text-align: left;
  background: none;
  border-left: 0; border-right: 0; border-top: 0;
  font-family: inherit;
  cursor: pointer;
}
.pop-item:last-child { border-bottom: 0; }
@media (hover: hover) and (pointer: fine) {
  .pop-item:hover { background: var(--surface-2); text-decoration: none; }
}

.pop-item.is-unread { background: var(--brand-soft); }
.pop-item.is-current { color: var(--brand); font-weight: 600; }
.pop-item .icon { color: var(--ink-faint); margin-top: 1px; }
.pop-item.is-current .icon { color: var(--brand); }
.pop-item .theme-check { display: none; margin-left: auto; flex-shrink: 0; }
.pop-item.is-current .theme-check { display: inline-block; }
.pop-item-body { display: flex; flex-direction: column; gap: .0625rem; min-width: 0; flex: 1; }

/* ==========================================================================
   Timeline
   ========================================================================== */

.timeline { list-style: none; margin: 0; padding: 0 0 0 1rem; position: relative; }
.timeline::before {
  content: "";
  position: absolute;
  left: 3px; top: .4rem; bottom: .4rem;
  width: 1px;
  background: var(--border-strong);
}
.timeline li { position: relative; padding: 0 0 .875rem .5rem; }
.timeline li::before {
  content: "";
  position: absolute;
  left: -1rem; top: .4rem;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--brand);
  box-shadow: 0 0 0 3px var(--surface);
}
.timeline li:last-child { padding-bottom: 0; }

/* ==========================================================================
   Formset rows
   ========================================================================== */

.line-rows { display: flex; flex-direction: column; gap: .75rem; }
.line-row {
  border: 1px solid var(--border);
  border-radius: var(--r);
  padding: .75rem;
  background: var(--surface-2);
  /* Named properties, not `all`: `all` animates whatever happens to change,
     including the layout properties a formset row adds and removes as lines
     are inserted, which is how a row ends up sliding when it should appear. */
  transition: border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .line-row:hover {
    border-color: var(--border-strong, var(--border));
  }
}

.line-row-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: .5rem;
  padding-bottom: .375rem;
  border-bottom: 1px solid var(--border);
}
.line-row-title {
  font-size: .8125rem;
  color: var(--ink-faint);
}
.line-number {
  font-variant-numeric: tabular-nums;
}
.remove-line-btn {
  color: var(--bad);
  cursor: pointer;
  border: none;
  background: transparent;
  display: inline-flex;
  align-items: center;
  font-size: .8rem;
  padding: 2px 6px;
  border-radius: 4px;
  transition: background 0.12s ease;
}
@media (hover: hover) and (pointer: fine) {
  .remove-line-btn:hover {
    background: var(--bad-bg);
  }
}


/* ==========================================================================
   Auth screen
   ========================================================================== */

.auth {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 1.5rem;
  background: var(--bg);
}
.auth-inner { width: 100%; max-width: 21rem; }
.auth-head { text-align: center; margin-bottom: 1.25rem; }
.auth-head .brand-mark { width: 38px; height: 38px; margin: 0 auto .625rem; font-size: .875rem; }
.auth-head h1 { font-size: 1.25rem; }

/* ==========================================================================
   Payslip
   ========================================================================== */

.payslip-table { width: 100%; border-collapse: collapse; font-size: .8125rem; }
.payslip-table th {
  text-align: left;
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .075em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding: .375rem .5rem;
  border-bottom: 1px solid var(--border-strong);
  white-space: nowrap;
}
.payslip-table td { padding: .375rem .5rem; border-bottom: 1px solid var(--border); }
.payslip-table td.num, .payslip-table th.num { text-align: right; }
.payslip-total td {
  font-weight: 600;
  border-top: 1px solid var(--border-strong);
  border-bottom: 0;
  color: var(--ink);
}

/* ==========================================================================
   Responsive & print
   ========================================================================== */


/* Narrow: the sidebar becomes an overlay that starts away and is summoned.
   The only class that means anything here is `panel-open`; `panel-hidden` is a
   wide-screen concept and app.js strips it at this width, so the two can never
   interact. */
@media (max-width: 1000px) {
  .sidebar {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 50;
    transform: translateX(-100%);
    transition: transform var(--dur-drawer) var(--ease-drawer);
    /* Deeper than a menu's shadow because it is a far bigger surface sitting
       much further off the page. */
    box-shadow: var(--shadow-drawer);
    /* The drawer owns horizontal; the browser keeps vertical so the list of
       links inside it still scrolls under a finger. */
    touch-action: pan-y;
    /* At this width the drawer is over the page rather than beside it, so the
       page showing through is real depth and not a flat tint on nothing. The
       structural weight, not the chrome weight: this is a different place, not
       a strip laid over the current one. */
    background: var(--material);
    -webkit-backdrop-filter: var(--material-blur);
    backdrop-filter: var(--material-blur);
  }
  .shell.panel-open .sidebar { transform: translateX(0); }

  /* Where the drawer's motion comes from depends on whether JavaScript is
     here. The transition above is the fallback and it is genuinely used: with
     scripting off, or before app.js has run, the drawer still opens and shuts
     on that curve.

     Once app.js takes over it sets `is-drawer-live` and owns every pixel — one
     per pixel of finger while a finger is down, then a spring carrying that
     finger's speed once it lifts. A transition on the same property would be a
     second opinion about where the drawer is, arriving a frame late. */
  .shell.is-drawer-live .sidebar,
  .shell.is-drawer-live::before { transition: none; }

  /* Something has to say "the page behind this is not the thing you are
     using". Without it the sidebar reads as part of the page rather than over
     it. app.js already closes on a click outside, and this catches those
     clicks.

     It is present at all times now and hidden by opacity, so it can fade in
     step with the drawer instead of appearing whole the moment the class
     lands. --drawer-p is the drag progress app.js writes while a finger is
     down, 0 shut to 1 open; without JS the class alone still drives it. */
  .shell::before {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 49;
    background: rgba(0, 0, 0, .45);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-drawer) var(--ease-drawer);
  }
  .shell.panel-open::before {
    opacity: 1;
    pointer-events: auto;
  }
  /* --drawer-p is how far open the drawer is, 0 shut to 1 open, written by
     app.js on every frame it moves the panel. The scrim reading the same
     number is what makes the tint and the panel one surface rather than two
     things that happen to be animating at once. */
  .shell.is-drawer-moving::before {
    opacity: var(--drawer-p, 0);
    pointer-events: auto;
    transition: none;
  }

  /* A promise that this element is about to move, so the compositor gives it
     its own layer before the first frame rather than during it. Only while it
     is actually moving: left on permanently it costs memory and, on a long
     drawer, can cost text rendering quality for nothing. */
  .shell.is-drawer-moving .sidebar { will-change: transform; }

  .board { grid-template-columns: repeat(5, 12rem); }
  .content { padding: 1rem; }
  .page-head-title h1 { font-size: 1.1875rem; }
}

/* --------------------------------------------------------------------------
   Touch. Everything above this is sized for a cursor, which is a pixel; a
   fingertip is about nine millimetres, and the guideline everyone converged on
   is a 44px target.

   The font-size rule is not cosmetic. Safari on iOS zooms the page in when you
   focus an input whose text is under 16px, and it does not zoom back out
   afterwards — so tapping the first field of a form leaves you stranded at 1.3x
   with the rest of the page off the side of the screen. Every control in this
   product was 13px.
   -------------------------------------------------------------------------- */
@media (max-width: 720px) {
  .form-input,
  .form-select,
  .form-textarea,
  textarea,
  input[type="text"],
  input[type="email"],
  input[type="password"],
  input[type="number"],
  input[type="search"],
  input[type="tel"],
  input[type="url"],
  input[type="date"],
  input[type="time"],
  input[type="datetime-local"],
  select {
    font-size: 1rem;
  }

  /* Room for a fingertip, in the places a finger actually goes. */
  .navgroup-head { padding: .75rem .5rem; }
  .navgroup-links a { padding: .75rem .5rem; }
  .sidebar-signout { padding: .75rem .5rem; }
  .btn { min-height: 2.5rem; }
  /* A two-word button is tall enough and still too narrow to hit. */
  .btn-sm { min-height: 2.25rem; min-width: 2.75rem; }
  .tabs a { padding: .5rem .625rem; }

  /* A row of actions that wrapped into a stack is easier to hit than one that
     shrank to fit. */
  .form-actions { flex-wrap: wrap; }
  .form-actions .btn { flex: 1 1 auto; justify-content: center; }
}

@media (max-width: 640px) {
  .sidebar { width: min(84vw, 20rem); flex-basis: auto; }
  .content { padding: 1rem .875rem; }
  .figure { flex-basis: 50%; }

  /* A page heading and its buttons stop sharing a line long before this. */
  .page-head { flex-wrap: wrap; gap: .625rem; }
  .page-head-actions { width: 100%; }
  .page-head-actions .btn { flex: 1 1 auto; justify-content: center; }

  /* Tabs are a horizontal list and there are sometimes six of them. Let them
     scroll on their own rather than wrapping into three cramped rows. */
  .tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .tabs::-webkit-scrollbar { display: none; }
  .tabs a { flex: 0 0 auto; }

  /* Stat cards read as one column here; three across is unreadable. */
  .grid-2, .grid-3, .grid-4, .grid-5 { grid-template-columns: 1fr; }

  /* The last column of a table is usually the action, and it was landing
     off the right edge with nothing suggesting the table scrolls. */
  .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
}

/* Reduced motion asks for less movement, not less interface. The rule that used
   to live here flattened transition-duration to zero for every element, which
   also took out the colour and opacity changes that tell you a row is
   highlighted or a message is dismissing — those carry meaning and cost nobody
   anything, because they do not move.

   So: looping and decorative animation stops, everything that travels across
   the screen stops travelling, and fades stay. The transform removals are
   listed one by one rather than swept with `transform: none !important`, which
   would pin the navigation drawer open — its transform is what hides it. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0ms !important;
  }

  /* Presses answer with colour instead of a squash. */
  .btn:active, .icon-btn:active, .pop-item:active, .tabs a:active,
  .pager-page:active, .pager-step:active, .cal-nav-btn:active,
  .cal-tab-btn:active, .modal-close-btn:active, .password-toggle-btn:active,
  .msg-close:active, .remove-line-btn:active, .navgroup-head:active,
  .navgroup-links a:active, .sidebar-signout:active, .check-row:active,
  .card.is-linked:active, .figure.is-linked:active, a.figure:active,
  .announcement-item:active, .announcement-card:active, .filterbox-btn:active,
  .filterbox-head:active, .rate-chip:active span, .chip-clear:active {
    transform: none;
  }

  /* Surfaces arrive by fading rather than by scaling out of their trigger. */
  .modal-dialog, .pop {
    transform: none !important;
    transition: opacity var(--dur-pop) ease, visibility var(--dur-pop);
  }

  /* The drawer still has to be somewhere off-screen, so it keeps its transform
     and simply stops animating between the two positions. */
  .sidebar { transition: none !important; }

  @media (hover: hover) and (pointer: fine) {
    .is-linked:hover .card-go { transform: none; }
  }
}

/* A printed payslip or report is the record, so everything that only helps you
   move around the app comes off the page. These names track the shell; the
   navigation has now been renamed twice, and both times this block was the
   thing that quietly kept pointing at the old one. */
@media print {
  .sidebar,
  .utilitybar,
  .filterbar,
  .routebar,
  .page-head-actions,
  .form-actions,
  .no-print,
  .impersonation-bar,
  .bootscreen { display: none !important; }

  body { background: #fff; color: #000; }
  .shell { display: block; }
  .main { display: block; }
  .content { padding: 0; }
  .card, .figures { border: 0; box-shadow: none; break-inside: avoid; }
  .table-wrap { overflow: visible; max-height: none; }
  table.data th { position: static; }
  a[href]::after { content: ""; }
}

/* ==========================================================================
   Impersonation banner
   ========================================================================== */

.impersonation-bar {
  background: var(--warn-bg);
  color: var(--warn);
  padding: .4375rem 1.25rem;
  font-size: .75rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  border-bottom: 1px solid var(--warn-line);
  flex: 0 0 auto;
}
/* The sentence wraps; the way out of impersonation must not be what gives.
   Without this the button kept its place on the row and shrank instead, until
   "Return to console" was clipped down to about one word. */
.impersonation-bar .btn { flex: 0 0 auto; }
.impersonation-bar span { min-width: 0; }

@media (max-width: 720px) {
  /* Two lines of warning and a button do not share a row at this width, so
     they stop trying to. */
  .impersonation-bar {
    flex-direction: column;
    align-items: flex-start;
    gap: .5rem;
    padding: .5rem .875rem;
  }
}

/* ==========================================================================
   Appraisals, timesheets, projects
   ========================================================================== */

/* Rating scale — a row of radio buttons that read as one segmented control. */
.rate-chip { position: relative; cursor: pointer; }
.rate-chip input {
  position: absolute;
  opacity: 0;
  width: 100%; height: 100%;
  margin: 0;
  cursor: pointer;
}
.rate-chip span {
  display: grid;
  place-items: center;
  min-width: 2.25rem;
  height: 2.25rem;
  padding: 0 .5rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  font-size: .8125rem;
  color: var(--ink-soft);
  transition: background .12s, border-color .12s, color .12s;
}
@media (hover: hover) and (pointer: fine) {
  .rate-chip:hover span { border-color: var(--action); color: var(--ink); }
}

.rate-chip input:checked + span {
  background: var(--action);
  border-color: var(--action);
  color: var(--action-ink);
}
.rate-chip input:focus-visible + span {
  outline: 2px solid var(--action);
  outline-offset: 2px;
}

/* Key result areas inside the template form. */
.grid-kra { grid-template-columns: 1fr 7rem 5rem auto; align-items: end; gap: .625rem; }
.sub-row {
  padding: .75rem;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.stack-sm { display: flex; flex-direction: column; gap: .625rem; }

/* Weekly timesheet. */
.week-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: .5rem;
}
.week-day {
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  padding: .5rem;
  min-height: 7rem;
}
.week-day.has-time { background: var(--surface); border-color: var(--border-strong); }
.week-day-head {
  display: flex;
  flex-direction: column;
  gap: .125rem;
  padding-bottom: .375rem;
  margin-bottom: .375rem;
  border-bottom: 1px solid var(--border);
}
.week-day .list-plain li { padding: .375rem 0; font-size: .75rem; }

/* Read-only people chips. */
.chip-row { display: flex; flex-wrap: wrap; gap: .375rem; }
.chip {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  padding: .1875rem .5rem .1875rem .1875rem;
  border: 1px solid var(--border);
  border-radius: 20px;
  background: var(--surface-2);
  font-size: .75rem;
}

.avatar-xs { width: 20px; height: 20px; font-size: .5rem; }
.btn-xs { padding: .0625rem .3125rem; font-size: .6875rem; }

.stat-value.accent { color: var(--brand); }
.stat-value.danger { color: var(--bad); }
.stat-value.warn { color: var(--warn); }

@media (max-width: 1100px) {
  .week-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-kra { grid-template-columns: 1fr 6rem; }
}
@media (max-width: 640px) {
  .week-grid { grid-template-columns: 1fr; }
  .grid-kra { grid-template-columns: 1fr; }
}

/* Permission matrix ------------------------------------------------------- */
.perm-grid td { vertical-align: middle; }
@media (hover: hover) and (pointer: fine) {
  .perm-grid tbody tr:hover { background: var(--surface-2); }
}

.check-bare {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.check-bare input { width: 16px; height: 16px; cursor: pointer; }

/* Salary package table ---------------------------------------------------- */
.row-section td {
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .085em;
  text-transform: uppercase;
  color: var(--ink-faint);
  background: var(--surface-2);
}
.row-total td { border-top: 1px solid var(--border-strong); }

/* ==========================================================================
   Filter bar — one control per thing you might search by
   ========================================================================== */

.filterbar {
  position: relative;
  margin-bottom: .875rem;
  border: none;
  box-shadow: none;
}

/* Search on the left, the filter panel's button on the right. */
.filterbar-row {
  display: flex;
  align-items: center;
  gap: .5rem;
}
.filterbar-search {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: .4375rem;
  height: 2rem;
  padding: 0 .5rem;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink-faint);
}
.filterbar-search:focus-within { border-color: var(--brand-line); }
.filterbar-search input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: .8125rem;
  color: var(--ink);
  outline: none;
  padding: 0;
}
.filterbar-search input::placeholder { color: var(--ink-faint); }
.filterbar-search input::-webkit-search-cancel-button { display: none; }
.filterbar-search-clear {
  flex: 0 0 auto;
  padding: 0 .1875rem;
  color: var(--ink-faint);
  font-size: 1rem;
  line-height: 1;
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .filterbar-search-clear:hover { color: var(--ink); text-decoration: none; }
}
.filterbar-row > .filterbox { flex: 0 0 auto; margin-left: auto; }

@media (max-width: 560px) {
  .filterbar-search input { font-size: 16px; }
}

/* Folded away until asked for. Only the filter button itself is clickable. */
.filterbox {
  position: static;
  border: none;
  box-shadow: none;
}

.filterbox-btn,
.filterbox-head {
  display: inline-flex;
  align-items: center;
  gap: .4375rem;
  padding: .375rem .625rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink);
  font-size: .8125rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  list-style: none;
  user-select: none;
  width: fit-content;
  max-width: fit-content;
  transition: border-color .12s, background .12s;
}
.filterbox-btn::-webkit-details-marker,
.filterbox-head::-webkit-details-marker { display: none; }
.filterbox-btn::marker,
.filterbox-head::marker { content: ""; }

@media (hover: hover) and (pointer: fine) {
  .filterbox-btn:hover,
  .filterbox-head:hover {
    background: var(--surface-2);
    border-color: var(--ink-faint);
  }
}

.filterbox-btn:focus-visible,
.filterbox-head:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}
.filterbox[open] > .filterbox-btn,
.filterbox[open] > .filterbox-head {
  background: var(--surface-2);
  border-color: var(--border-strong);
}

.filterbox-btn .icon { width: .9375rem; height: .9375rem; }
.filterbox-caret {
  opacity: .55;
  transition: transform .15s ease;
}
.filterbox[open] .filterbox-caret { transform: rotate(180deg); }

.filter-count {
  display: inline-grid;
  place-items: center;
  min-width: 1.125rem;
  height: 1.125rem;
  padding: 0 .3125rem;
  border-radius: 999px;
  background: var(--action);
  color: var(--action-ink);
  font-size: .6875rem;
  font-variant-numeric: tabular-nums;
}

/* What you could search by, without having to open it to find out. */
.filterbox-hint {
  display: none;
}

.filterbar-fields {
  display: flex;
  flex-wrap: wrap;
  gap: .625rem .75rem;
  align-items: flex-end;
}
.filterbox .filterbar-fields {
  position: absolute;
  top: calc(100% + .375rem);
  left: 0;
  right: 0;
  z-index: 40;
  padding: .875rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface);
  box-shadow: var(--shadow-pop);
}
.filterbar .field { margin: 0; }
.filterbar .field > label {
  display: block;
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .085em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: .25rem;
}
.filterbar .form-input,
.filterbar .form-select { width: 100%; }

.filter-narrow { flex: 0 1 9rem; min-width: 8rem; }
.filter-normal { flex: 1 1 13rem; min-width: 11rem; max-width: 20rem; }
.filter-wide   { flex: 1 1 19rem; min-width: 16rem; }

.filterbar .range { display: flex; align-items: center; gap: .375rem; }
.filterbar .range .form-input { min-width: 0; }
.range-sep { font-size: .75rem; color: var(--ink-faint); flex: 0 0 auto; }

.filter-actions {
  display: flex;
  /* This element carries `field` too, and `.field` is a column. Without saying
     otherwise the two buttons stacked — Search above Clear — which made the
     actions 70px tall and pushed Search a row's height above the controls it
     belongs beside. */
  flex-direction: row;
  gap: .375rem;
  flex: 0 0 auto;
  align-self: flex-end;
}

.filterbar .field.has-error .form-input,
.filterbar .field.has-error .form-select { border-color: var(--bad); }

/* What is currently narrowing the list, each removable on its own. */
.filterbar-active {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .375rem;
  margin-top: .5rem;
}
.chip-clear {
  text-decoration: none;
  color: var(--ink);
  gap: .3125rem;
  transition: border-color .12s, background .12s;
}
@media (hover: hover) and (pointer: fine) {
  .chip-clear:hover {
    border-color: var(--bad-line);
    background: var(--bad-bg);
    color: var(--bad);
  }
}

.chip-clear .meta { font-size: .6875rem; }

.filterbar-clear {
  font-size: .75rem;
  color: var(--ink-faint);
  text-decoration: none;
  border-bottom: 1px solid var(--border-strong);
  margin-left: .25rem;
}
@media (hover: hover) and (pointer: fine) {
  .filterbar-clear:hover { color: var(--bad); border-bottom-color: var(--bad-line); }
}


@media (max-width: 720px) {
  .filter-narrow, .filter-normal, .filter-wide { flex: 1 1 100%; max-width: none; }
  .filter-actions { flex: 1 1 100%; }
  .filter-actions .btn { flex: 1; }
}

/* What an access role actually grants ------------------------------------- */
.role-preview {
  margin-top: .5rem;
  padding: .625rem .75rem;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.role-preview-open { border-color: var(--warn-line); background: var(--warn-bg); }
.role-preview-open .stat-label,
.role-preview-open .help { color: var(--warn); }
.role-preview-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .5rem;
  margin-bottom: .4375rem;
}
.role-preview .help { margin: .4375rem 0 0; }

/* ==========================================================================
   Data surfaces
   ==========================================================================
   A list is the page, not a widget sitting on it. Cards that hold a table get
   treated as a surface rather than a box, and the clipping is removed, since an
   ancestor with overflow:hidden silently disables position:sticky.

   The header sticks to the top of the wrapper, not to the utility bar. A sticky
   element resolves against its nearest scrolling ancestor, and overflow-x:auto
   makes the wrapper exactly that — so an offset here does not hold the header
   below the utility bar, it just pushes it down that far and leaves a gap above
   every table. The wrapper is given a height instead, which makes it a scroller
   in its own right: long lists scroll inside the card with the header pinned,
   and short ones never reach the height, so they are untouched.
   -------------------------------------------------------------------------- */

.card:has(> .table-wrap) { overflow: visible; }
.card:has(> .table-wrap) > .table-wrap {
  border-radius: 0 0 var(--r) var(--r);
  overflow: auto;
  /* Leaves room for the pager beneath, so paging controls never sit just
     below the fold on a short screen. */
  max-height: calc(100vh - var(--utility-h) - 15rem);
}
.card:has(> .table-wrap) > .card-head + .table-wrap { border-radius: 0 0 var(--r) var(--r); }
.card:has(> .table-wrap) table.data th {
  top: 0;
  z-index: 3;
  box-shadow: inset 0 -1px 0 var(--border);
}

/* Breathing room at the edges so the first column lines up with the header
   above it rather than hugging the border. */
table.data th:first-child,
table.data td:first-child { padding-left: .875rem; }
table.data th:last-child,
table.data td:last-child { padding-right: .875rem; }

/* Rows are read in bulk all day, so they are tighter than a marketing table
   and the hover is a whole-row cue rather than a per-cell one. */
table.data tbody tr { transition: background .08s; }
@media (hover: hover) and (pointer: fine) {
  table.data tbody tr:hover { background: var(--surface-2); }
}


/* A quiet header for lists that do not need a title bar at all. */
.card-head.is-quiet { border-bottom: 0; padding-bottom: .375rem; }

/* ==========================================================================
   Detail layout — the record, and the facts about it beside it
   ========================================================================== */

.detail {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 17rem;
  gap: 1rem;
  align-items: start;
}
.detail-main { min-width: 0; display: flex; flex-direction: column; gap: .875rem; }
.detail-rail { display: flex; flex-direction: column; gap: .875rem; position: sticky; top: calc(var(--utility-h) + 1rem); }

/* Key/value pairs in the rail, without a table's chrome. */
.facts { display: flex; flex-direction: column; }
.fact {
  display: flex;
  justify-content: space-between;
  gap: .75rem;
  padding: .4375rem 0;
  border-bottom: 1px solid var(--border-soft);
  font-size: .8125rem;
}
.fact:last-child { border-bottom: 0; }
.fact-label { color: var(--ink-faint); flex: 0 0 auto; }
.fact-value { text-align: right; min-width: 0; }

@media (max-width: 1080px) {
  .detail { grid-template-columns: minmax(0, 1fr); }
  .detail-rail { position: static; }
}

/* ==========================================================================
   The utility strip and the page header
   ==========================================================================
   The strip carries only what is global — breadcrumb, appearance,
   notifications, account. A page's own title is not global, so it lives in the
   content column where it can sit beside the figures and actions it describes.
   -------------------------------------------------------------------------- */

.icon-btn {
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink-soft);
  cursor: pointer;
  position: relative;
  padding: 0;
  font-family: inherit;
  flex: 0 0 auto;
}
@media (hover: hover) and (pointer: fine) {
  .icon-btn:hover {
    background: var(--surface-2);
    color: var(--ink);
    border-color: var(--border-strong);
    text-decoration: none;
  }
}


.avatar {
  width: 28px; height: 28px;
  border-radius: 50%;
  background: var(--surface-3);
  color: var(--ink-soft);
  display: grid;
  place-items: center;
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .02em;
  border: 1px solid var(--border);
  overflow: hidden;
  flex: 0 0 auto;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar-lg { width: 52px; height: 52px; font-size: 1rem; }
.avatar-sm { width: 28px; height: 28px; font-size: .75rem; }

/* Touch sizes for the two controls in the utility bar. This block has to sit
   *after* the rules above rather than with the other touch rules further up:
   a media query adds no specificity, so the plain `.icon-btn { width: 30px }`
   declared here would otherwise win on source order alone — which is exactly
   what happened the first time. */
@media (max-width: 720px) {
  .icon-btn { width: 2.5rem; height: 2.5rem; }
  .avatar { width: 2.5rem; height: 2.5rem; font-size: .75rem; }
}
.avatar-xs { width: 20px; height: 20px; font-size: .5rem; }

/* The bar content actually scrolls under, so it is built as a material rather
   than as an opaque strip that happens to be pinned: a blur over whatever is
   passing beneath plus a saturation lift, which is what keeps a colour pill or
   an avatar from going grey as it slides under.

   The opaque background stays as the first declaration. Browsers without
   backdrop-filter — and the print stylesheet, and the two accessibility
   queries at the bottom of this file — land on it and lose nothing but the
   translucency. */
.utilitybar {
  display: flex;
  align-items: center;
  gap: .5rem;
  height: var(--utility-h);
  min-height: var(--utility-h);
  padding: 0 1.25rem;
  /* Notch and Dynamic Island: the app paints edge to edge and pads its content
     back out. Zero everywhere the inset does not exist. */
  padding-top: env(safe-area-inset-top, 0px);
  height: calc(var(--utility-h) + env(safe-area-inset-top, 0px));
  min-height: calc(var(--utility-h) + env(safe-area-inset-top, 0px));
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  position: sticky;
  top: 0;
  z-index: 20;
  flex: 0 0 auto;
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .utilitybar {
    background: var(--chrome);
    -webkit-backdrop-filter: var(--chrome-blur);
    backdrop-filter: var(--chrome-blur);
  }
}
.utilitybar-crumb { flex: 1 1 auto; min-width: 0; font-size: .6875rem; color: var(--ink-faint); }
.utilitybar-crumb .crumb a { color: var(--ink-faint); }
@media (hover: hover) and (pointer: fine) {
  .utilitybar-crumb .crumb a:hover { color: var(--ink-soft); }
}


/* ==========================================================================
   Workspace search
   ========================================================================== */

.quick-search {
  position: relative;
  flex: 0 1 22rem;
  display: flex;
  align-items: center;
  gap: .4375rem;
  height: 2rem;
  padding: 0 .5rem;
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface-2);
  min-width: 0;
}
.quick-search:focus-within {
  border-color: var(--brand-line);
  background: var(--surface);
}
.quick-search-icon { flex: 0 0 auto; color: var(--ink-faint); }
.quick-search-input {
  flex: 1 1 auto;
  min-width: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: .8125rem;
  color: var(--ink);
  outline: none;
  padding: 0;
}
.quick-search-input::placeholder { color: var(--ink-faint); }
.quick-search-input::-webkit-search-cancel-button { display: none; }
.quick-search-key {
  flex: 0 0 auto;
  padding: .0625rem .3125rem;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  font-family: var(--mono);
  font-size: .625rem;
  color: var(--ink-faint);
}
.quick-search:focus-within .quick-search-key { display: none; }

/* The results hang under the box, over the page. */
.quick-search-panel {
  position: absolute;
  top: calc(100% + .375rem);
  left: 0;
  right: 0;
  z-index: 60;
  max-height: 70vh;
  overflow-y: auto;
  padding: .375rem;
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface);
  box-shadow: var(--shadow-pop, 0 12px 32px rgba(0, 0, 0, .18));
}
.quick-search-panel[hidden] { display: none; }

.qs-groups { display: block; }
.qs-group + .qs-group { margin-top: .375rem; padding-top: .375rem; border-top: 1px solid var(--border-soft); }
.qs-group-label {
  padding: .25rem .4375rem;
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.qs-hit {
  display: flex;
  align-items: baseline;
  gap: .4375rem;
  padding: .375rem .4375rem;
  border-radius: var(--r-sm);
  color: var(--ink);
  font-size: .8125rem;
  text-decoration: none;
}
@media (hover: hover) and (pointer: fine) {
  .qs-hit:hover { background: var(--surface-2); text-decoration: none; }
}
.qs-hit:focus-visible,
.qs-hit:focus { background: var(--brand-soft); outline: none; }
.qs-hit-label { flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qs-hit-code { flex: 0 0 auto; font-size: .6875rem; color: var(--ink-faint); }
.qs-hit-detail {
  flex: 1 1 auto;
  text-align: right;
  font-size: .6875rem;
  color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.qs-empty { padding: .75rem .4375rem; font-size: .8125rem; color: var(--ink-faint); }

/* The payment step on a final settlement, set apart from the statement. */
.settle-pay { display: block; }

/* The full results page uses the same rows, with room to breathe. */
.qs-page-form .card-body { display: flex; gap: .5rem; align-items: center; }
.qs-page-form .form-input { flex: 1 1 auto; }
.qs-page-results .qs-hit { padding: .5rem .5625rem; font-size: .875rem; }

@media (max-width: 860px) {
  .quick-search { flex: 1 1 auto; }
  .quick-search-key { display: none; }
}
@media (max-width: 560px) {
  .quick-search-input { font-size: 16px; }
}

.utilitybar-actions { display: flex; align-items: center; gap: .375rem; flex: 0 0 auto; }
.utilitybar-actions > .pop-wrap { flex: 0 0 auto; }

.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding-bottom: .875rem;
  margin-bottom: 1rem;
  border-bottom: 1px solid var(--border);
}
.page-head-title { min-width: 0; flex: 1 1 16rem; }
.page-head-title h1 { font-size: 1.375rem; letter-spacing: -.02em; line-height: 1.15; }
.page-sub {
  margin-top: .1875rem;
  font-size: .8125rem;
  color: var(--ink-faint);
  max-width: 48rem;
}
.page-head-actions {
  display: flex;
  align-items: center;
  gap: .375rem;
  flex-wrap: wrap;
  flex: 0 0 auto;
}

/* A row of figures under the header — the numbers a page is about, stated once
   in a single frame rather than each wrapped in its own bordered box. */
.figures {
  display: flex;
  flex-wrap: wrap;
  margin: 0 0 1rem;
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface);
  overflow: hidden;
}
.figure {
  flex: 1 1 9rem;
  min-width: 0;
  padding: .6875rem .875rem;
  border-right: 1px solid var(--border);
}
.figure:last-child { border-right: 0; }
a.figure {
  text-decoration: none;
  color: inherit;
  display: block;
  transition: background 0.15s ease;
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  a.figure:hover {
    background: var(--surface-2);
  }
}

.figure-label {
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .085em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.figure-value {
  font-family: var(--font-display);
  font-size: 1.375rem;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -.025em;
  margin-top: .125rem;
}
.figure-value.accent { color: var(--brand); }
.figure-value.warn { color: var(--warn); }
.figure-value.danger { color: var(--bad); }
.figure-note { font-size: .6875rem; color: var(--ink-faint); margin-top: .125rem; }

/* ==========================================================================
   Charts
   ==========================================================================
   Every chart is inline SVG built from numbers worked out on the server, so it
   is in the HTML: it prints, it survives JavaScript being off, and its figures
   are readable as text in the legend beside it.
   -------------------------------------------------------------------------- */

.chart-donut {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  flex-wrap: wrap;
  padding: .25rem 0;
}
.donut { flex: 0 0 auto; transform: rotate(-90deg); }
.donut-track { stroke: var(--surface-3); }
.donut-seg {
  transition: stroke-width .12s;
  transform-origin: center;
}
@media (hover: hover) and (pointer: fine) {
  .donut-seg:hover { stroke-width: calc(var(--thickness) + 2px); }
}

.donut-total {
  transform: rotate(90deg);
  transform-origin: center;
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  fill: var(--ink);
  letter-spacing: -.02em;
}

.legend { list-style: none; margin: 0; padding: 0; flex: 1 1 11rem; min-width: 0; }
.legend li {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .25rem 0;
  font-size: .8125rem;
  border-bottom: 1px solid var(--border-soft);
}
.legend li:last-child { border-bottom: 0; }
.legend-dot { width: 9px; height: 9px; border-radius: 2px; flex: 0 0 auto; }
.legend-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legend-value { font-variant-numeric: tabular-nums; font-weight: 600; }
.legend-share { color: var(--ink-faint); font-size: .75rem; min-width: 3rem; text-align: right; }

.legend-inline { display: flex; flex-wrap: wrap; gap: .25rem 1rem; margin-top: .625rem; }
.legend-inline li { border-bottom: 0; padding: 0; }

/* Ranked bars ------------------------------------------------------------- */
.rankbars { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.rankbars li { display: flex; align-items: center; gap: .625rem; font-size: .8125rem; }
.rankbar-label { flex: 0 0 7.5rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rankbar-track { flex: 1; height: 8px; background: var(--surface-3); border-radius: 20px; overflow: hidden; }
.rankbar-fill {
  display: block;
  height: 100%;
  border-radius: 20px;
  transform-origin: left;
  animation: grow-x .5s cubic-bezier(.22,.61,.36,1) backwards;
  animation-delay: calc(var(--i, 0) * 55ms);
}
.rankbar-value {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  min-width: 2.5rem;
  text-align: right;
}

/* One bar in parts -------------------------------------------------------- */
.stackbar {
  display: flex;
  height: 12px;
  border-radius: 20px;
  overflow: hidden;
  background: var(--surface-3);
  gap: 2px;
}
.stackbar-seg {
  display: block;
  height: 100%;
  animation: grow-x .5s cubic-bezier(.22,.61,.36,1) backwards;
  animation-delay: calc(var(--i, 0) * 70ms);
  transform-origin: left;
}
.stackbar-seg:first-child { border-radius: 20px 0 0 20px; }
.stackbar-seg:last-child { border-radius: 0 20px 20px 0; }

/* Trend line -------------------------------------------------------------- */
.spark { width: 100%; height: 40px; overflow: visible; }
.spark-line { fill: none; stroke: var(--series-1); stroke-width: 1.75; vector-effect: non-scaling-stroke; }
.spark-area { fill: var(--series-1); opacity: .1; }
.spark-tip { fill: var(--series-1); }
.spark-up .spark-line, .spark-up .spark-tip { stroke: var(--ok); fill: var(--ok); }
.spark-up .spark-area { fill: var(--ok); }
.spark-up .spark-line { fill: none; }
.spark-down .spark-line, .spark-down .spark-tip { stroke: var(--bad); fill: var(--bad); }
.spark-down .spark-area { fill: var(--bad); }
.spark-down .spark-line { fill: none; }

@keyframes grow-x { from { transform: scaleX(0); } to { transform: scaleX(1); } }

@media (prefers-reduced-motion: reduce) {
  .rankbar-fill, .stackbar-seg { animation: none; }
}

/* ==========================================================================
   Waiting
   ==========================================================================
   Three different waits, three different answers. A page change gets a thread
   at the top of the window. A table that has not arrived gets a shimmer in the
   shape of the table, so the layout does not jump when it does. The first paint
   of the whole app gets the mark drawing itself.
   -------------------------------------------------------------------------- */

/* 1. Navigation — a thread across the top, brand coloured. */
.routebar {
  position: fixed;
  top: 0; left: 0;
  height: 2px;
  width: 0;
  z-index: 200;
  background: linear-gradient(90deg,
    var(--brand) 0%,
    color-mix(in srgb, var(--brand) 55%, #FFFFFF) 100%);
  box-shadow: 0 0 8px color-mix(in srgb, var(--brand) 60%, transparent);
  opacity: 0;
  transition: width .2s ease, opacity .2s ease;
  pointer-events: none;
}
.routebar.is-running { opacity: 1; }
.routebar.is-done { width: 100% !important; opacity: 0; transition: width .12s ease, opacity .3s ease .1s; }

/* 2. Content that has not arrived — the shape of what is coming. */
.skeleton {
  background: linear-gradient(
    90deg,
    var(--surface-2) 0%,
    var(--surface-3) 42%,
    var(--surface-2) 84%
  );
  background-size: 300% 100%;
  animation: shimmer 1.4s linear infinite;
  border-radius: var(--r-sm);
  color: transparent;
  user-select: none;
}
.skeleton-line { height: .75rem; margin: .3125rem 0; }
.skeleton-line.w-40 { width: 40%; }
.skeleton-line.w-60 { width: 60%; }
.skeleton-line.w-80 { width: 80%; }
.skeleton-block { height: 7rem; }
.skeleton-circle { border-radius: 50%; width: 28px; height: 28px; }

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

/* 3. First paint — the monogram drawing itself, then lifting away. */
.bootscreen {
  position: fixed;
  inset: 0;
  z-index: 300;
  display: grid;
  place-items: center;
  gap: 1rem;
  background: var(--bg);
  animation: boot-out .35s ease .15s forwards;
}
.bootmark {
  width: 46px; height: 46px;
  border-radius: 10px;
  background: var(--brand);
  color: #fff;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: .9375rem;
  letter-spacing: .02em;
  position: relative;
  animation: boot-mark .5s cubic-bezier(.34,1.4,.64,1) backwards;
}
/* A ring that sweeps once around the mark while the page settles. */
.bootmark::after {
  content: "";
  position: absolute;
  inset: -6px;
  border-radius: 14px;
  border: 2px solid transparent;
  border-top-color: var(--brand);
  animation: boot-spin .7s linear infinite;
}
.bootword {
  font-size: .75rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  animation: boot-word .5s ease .12s backwards;
}

@keyframes boot-mark {
  from { opacity: 0; transform: scale(.7); }
  to   { opacity: 1; transform: scale(1); }
}
@keyframes boot-word { from { opacity: 0; } to { opacity: 1; } }
@keyframes boot-spin { to { transform: rotate(360deg); } }
@keyframes boot-out {
  to { opacity: 0; visibility: hidden; }
}

/* A button that is doing something says so, without changing size. */
.btn.is-busy { position: relative; color: transparent !important; pointer-events: none; }
.btn.is-busy::after {
  content: "";
  position: absolute;
  width: 13px; height: 13px;
  top: 50%; left: 50%;
  margin: -6.5px 0 0 -6.5px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  color: var(--action-ink);
  animation: boot-spin .6s linear infinite;
}
.btn:not(.btn-primary).is-busy::after { color: var(--ink-soft); }

@media (prefers-reduced-motion: reduce) {
  .skeleton { animation: none; }
  .bootmark, .bootword, .bootscreen { animation: none; }
  .bootmark::after { animation: none; border-top-color: transparent; }
  .routebar { transition: none; }
}

/* A change against a previous period, stated with its direction. */
.delta { font-weight: 600; font-variant-numeric: tabular-nums; }
.delta-up { color: var(--ok); }
.delta-down { color: var(--bad); }
.delta-level { color: var(--ink-faint); }

/* Surfaces rest on the page rather than being outlined onto it. */
.card, .figures {
  box-shadow: var(--shadow-card);
}
.card { border-color: var(--border-soft); }
.card > .card-head { border-bottom-color: var(--border-soft); }

/* ==========================================================================
   Collapsing the section panel
   ==========================================================================
   One control, two jobs, decided by how much room there is: on a wide screen
   it folds the panel away to give a wide table another 196px, and on a narrow
   one it slides the panel over the content.

   This sits after .icon-btn deliberately — the button carries both classes,
   and the later rule of equal specificity is the one that applies.
   -------------------------------------------------------------------------- */

.menu-toggle { display: grid; }

/* Wide: the sidebar is part of the layout. The toggle sits inside the sidebar
   when it is open, and appears in the utilitybar only when the sidebar has been
   folded away. Scoped to this width on purpose — a media query adds no
   specificity, so an unscoped `display: none` here would also win inside the
   narrow block and leave the overlay permanently hidden. */
@media (min-width: 1001px) {
  .utilitybar .menu-toggle { display: none; }
  .shell.panel-hidden .utilitybar .menu-toggle { display: grid; }
  .shell.panel-hidden .sidebar { display: none; }
}

@media (min-width: 1001px) {
  html.panel-was-hidden .utilitybar .menu-toggle { display: grid; }
  html.panel-was-hidden .sidebar { display: none; }
}

/* ==========================================================================
   The permission grid
   ==========================================================================
   A wide table of checkboxes is easy to lose your place in, so the whole cell
   is the target, the checkbox is sized up from the browser default, and the
   row a pointer is on is highlighted across its full width.
   -------------------------------------------------------------------------- */

/* A checkbox with no surrounding card — used where the row and column already
   say what it means, and a label beside it would only repeat them. */
.check-bare {
  display: grid;
  place-items: center;
  width: 100%;
  min-height: 2rem;
  margin: 0;
  cursor: pointer;
  border-radius: var(--r-sm);
}
@media (hover: hover) and (pointer: fine) {
  .check-bare:hover { background: var(--surface-2); }
}

.check-bare input[type="checkbox"] {
  width: 17px;
  height: 17px;
  margin: 0;
  accent-color: var(--brand);
  cursor: pointer;
}
.check-bare input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}
/* Edit is disabled until View is on — say so rather than just ignoring clicks. */
.check-bare input[type="checkbox"]:disabled { cursor: not-allowed; opacity: .35; }
.check-bare:has(input:disabled) { cursor: not-allowed; }
@media (hover: hover) and (pointer: fine) {
  .check-bare:has(input:disabled):hover { background: transparent; }
}


.perm-grid td { vertical-align: middle; }
@media (hover: hover) and (pointer: fine) {
  .perm-grid tbody tr:hover { background: var(--surface-2); }
}

.perm-grid tbody tr:has(input:checked) td:first-child { box-shadow: inset 2px 0 0 var(--brand); }

/* A value the person is meant to take away, with the means to take it. */
.copy-row {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: .5rem 0 .875rem;
  padding: .4375rem .5rem .4375rem .625rem;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.copy-row code {
  flex: 1;
  min-width: 0;
  font-size: .8125rem;
  overflow-x: auto;
  white-space: nowrap;
  scrollbar-width: none;
}
.copy-row code::-webkit-scrollbar { display: none; }
.copy-row .btn { flex: 0 0 auto; }
.copy-row .btn.is-done { color: var(--ok); border-color: var(--ok-line); background: var(--ok-bg); }

/* Numbered instructions that read as steps rather than a paragraph. */
.steps {
  margin: 0;
  padding-left: 1.15rem;
  display: grid;
  gap: .3125rem;
  font-size: .8125rem;
  color: var(--ink-soft);
}
.steps strong { color: var(--ink); }

/* ==========================================================================
   Pager
   ==========================================================================
   Sits under a table rather than inside it, and only when there is more than
   one page — a list that fits needs no navigation.
   -------------------------------------------------------------------------- */

.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .625rem;
  margin-top: .75rem;
}
.pager-count { font-size: .75rem; color: var(--ink-faint); margin: 0; }
.pager-count strong { color: var(--ink); font-variant-numeric: tabular-nums; }

.pager-links { display: flex; align-items: center; gap: .1875rem; flex-wrap: wrap; }

.pager-step,
.pager-page {
  display: inline-grid;
  place-items: center;
  min-width: 1.875rem;
  height: 1.875rem;
  padding: 0 .5rem;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink-soft);
  font-size: .8125rem;
  text-decoration: none;
  font-variant-numeric: tabular-nums;
  transition: border-color .12s, background .12s, color .12s;
}
@media (hover: hover) and (pointer: fine) {
  .pager-step:hover,
  .pager-page:hover { background: var(--surface-2); color: var(--ink); }
}


.pager-page.is-current {
  background: var(--action);
  border-color: var(--action);
  color: var(--action-ink);
  font-weight: 600;
}
.pager-step.is-off { opacity: .4; }
.pager-gap { padding: 0 .25rem; color: var(--ink-faint); font-size: .8125rem; }

@media (max-width: 560px) {
  .pager { justify-content: center; }
  .pager-count { width: 100%; text-align: center; }
}

/* ==========================================================================
   Interactive Calendar View (Holidays & Schedule)
   ========================================================================== */

.cal-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: .75rem;
  margin-bottom: 1rem;
}

.cal-nav-group {
  display: flex;
  align-items: center;
  gap: .25rem;
}

.cal-nav-btn {
  display: inline-grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--ink-soft);
  text-decoration: none;
  transition: background .12s, border-color .12s, color .12s;
}
@media (hover: hover) and (pointer: fine) {
  .cal-nav-btn:hover {
    background: var(--surface-2);
    color: var(--ink);
    border-color: var(--border-strong);
    text-decoration: none;
  }
}


.cal-today-btn {
  margin-left: .25rem;
}

.cal-title h2 {
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: -.02em;
  margin: 0;
  color: var(--ink);
  text-align: center;
}

.cal-view-switcher {
  display: flex;
  align-items: center;
  background: var(--surface-2);
  padding: 3px;
  border-radius: var(--r-sm);
  border: 1px solid var(--border);
}

.cal-tab-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: .25rem .75rem;
  font-size: .8125rem;
  font-weight: 600;
  border-radius: calc(var(--r-sm) - 2px);
  color: var(--ink-soft);
  text-decoration: none;
  transition: background .12s, color .12s;
}
@media (hover: hover) and (pointer: fine) {
  .cal-tab-btn:hover {
    color: var(--ink);
    text-decoration: none;
  }
}

.cal-tab-btn.is-active {
  background: var(--surface);
  color: var(--ink);
  box-shadow: 0 1px 2px rgba(0,0,0,.06);
}

.cal-card {
  overflow: hidden;
  margin-bottom: 1.25rem;
}

.cal-weekdays {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  text-align: center;
}

.cal-weekday {
  padding: .5rem .25rem;
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.cal-grid {
  display: grid;
  /* minmax(0, …), not 1fr: a plain 1fr won't shrink below its content, so
     a long holiday name widened its own day and the week went lopsided. */
  grid-template-columns: repeat(7, minmax(0, 1fr));
  border-bottom: 1px solid var(--border);
}

.cal-cell {
  min-height: 6.5rem;
  padding: .4375rem .5rem;
  border-right: 1px solid var(--border-soft);
  border-bottom: 1px solid var(--border-soft);
  background: var(--surface);
  display: flex;
  flex-direction: column;
  gap: .3125rem;
  transition: background .12s;
}
.cal-cell:nth-child(7n) {
  border-right: none;
}

.cal-cell.is-weekend {
  background: color-mix(in srgb, var(--surface-2) 65%, var(--surface));
}

.cal-cell.is-other-month {
  background: color-mix(in srgb, var(--surface-2) 85%, var(--surface));
  opacity: .6;
}

.cal-day-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  line-height: 1;
}

.cal-day-num {
  font-size: .8125rem;
  font-weight: 600;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.cal-cell.is-today .cal-day-num {
  display: inline-grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: var(--brand);
  color: #fff;
  font-weight: 700;
  margin: -2px 0 0 -2px;
}

.cal-today-badge {
  font-size: .625rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--brand);
}

.cal-events {
  display: flex;
  flex-direction: column;
  gap: .25rem;
  min-width: 0;
}

.cal-event {
  display: flex;
  align-items: center;
  gap: .3125rem;
  padding: .1875rem .375rem;
  border-radius: 4px;
  font-size: .6875rem;
  font-weight: 600;
  line-height: 1.25;
  text-decoration: none;
  min-width: 0;
  word-break: break-word;
}

.cal-event-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex: 0 0 6px;
}

.cal-event-text {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Scheduled Holiday (Green/Emerald) matching reference */
.cal-event.is-holiday {
  background: var(--ok-bg);
  color: var(--ok);
  border: 1px solid var(--ok-line);
}
.cal-event.is-holiday .cal-event-dot {
  background: var(--ok);
}

/* Optional Holiday (Info/Blue) */
.cal-event.is-optional {
  background: var(--info-bg);
  color: var(--info);
  border: 1px solid var(--info-line);
}
.cal-event.is-optional .cal-event-dot {
  background: var(--info);
}

/* Legend */
.cal-legend {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  padding: .75rem 1rem;
  background: var(--surface-2);
  border-top: 1px solid var(--border);
  flex-wrap: wrap;
  font-size: .75rem;
  color: var(--ink-soft);
}

.cal-legend-item {
  display: flex;
  align-items: center;
  gap: .375rem;
  font-weight: 500;
}

.cal-legend-swatch {
  width: 12px;
  height: 12px;
  border-radius: 3px;
  flex: 0 0 12px;
}
.cal-legend-swatch.is-holiday {
  background: var(--ok);
}
.cal-legend-swatch.is-optional {
  background: var(--info);
}
.cal-legend-swatch.is-weekend {
  background: var(--border-strong);
}

@media (max-width: 720px) {
  .cal-cell {
    min-height: 4.5rem;
    padding: .25rem .3125rem;
  }
  .cal-weekday {
    font-size: .625rem;
    padding: .375rem .125rem;
  }
  .cal-event {
    padding: .125rem .25rem;
    font-size: .625rem;
  }
  .cal-day-num {
    font-size: .6875rem;
  }
  .cal-cell.is-today .cal-day-num {
    width: 1.25rem;
    height: 1.25rem;
  }
  .cal-today-badge {
    display: none;
  }
}

/* ==========================================================================
   Announcements — interactive cards & feed items
   ========================================================================== */

/* Scoped past `.list-plain li`, which is more specific than a bare class and
   was overriding the padding away — leaving every title flush against the edge
   of its own highlight. */
.list-plain li.announcement-item,
.announcement-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .25rem;
  /* The row's highlight bleeds a little past the card's padding so it reads as
     one target, but not so far that a pinned item's tint reaches the card
     border. The horizontal padding puts the title back where the card's own
     heading starts. */
  padding: .625rem .5rem;
  margin: -.25rem -.375rem;
  border-radius: var(--r);
  border: 1px solid transparent;
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease, transform .12s ease;
  text-align: left;
  background: transparent;
  width: calc(100% + .75rem);
  box-sizing: border-box;
}
@media (hover: hover) and (pointer: fine) {
  .announcement-item:hover {
    background: var(--surface-2);
    border-color: var(--border);
    transform: translateY(-1px);
  }
}

.announcement-item:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
  background: var(--surface-2);
}

.announcement-item-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  gap: .5rem;
}

.announcement-item-title {
  font-weight: 600;
  color: var(--ink);
  font-size: .875rem;
  line-height: 1.35;
}

.announcement-item-snippet {
  color: var(--ink-soft);
  font-size: .8125rem;
  line-height: 1.45;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.announcement-item-foot {
  display: flex;
  align-items: center;
  gap: .5rem;
  font-size: .75rem;
  color: var(--ink-faint);
  margin-top: .125rem;
}

.announcement-card {
  position: relative;
  transition: border-color .15s ease, box-shadow .15s ease;
}
@media (hover: hover) and (pointer: fine) {
  .announcement-card:hover {
    border-color: var(--border-strong);
    box-shadow: 0 4px 14px -2px rgba(15, 20, 28, .08);
  }
}


.announcement-open-btn {
  background: transparent;
  border: none;
  padding: 0;
  color: inherit;
  font: inherit;
  cursor: pointer;
  text-align: left;
  width: 100%;
}
@media (hover: hover) and (pointer: fine) {
  .announcement-open-btn:hover h2 {
    color: var(--brand);
  }
}


.announcement-body-src {
  display: none;
}

/* ==========================================================================
   Announcement Modal Popup
   ========================================================================== */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(10, 12, 16, .45);
  /* Zero while shut and animated up with the dialog, so the page behind goes
     soft as the dialog arrives rather than snapping out of focus a frame
     before it. */
  backdrop-filter: blur(0px);
  -webkit-backdrop-filter: blur(0px);
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.25rem;
  opacity: 0;
  visibility: hidden;
  /* Reflected curve: this rule is what applies while the modal is closing. */
  transition: opacity var(--dur-modal) var(--ease-out-reverse),
              -webkit-backdrop-filter var(--dur-modal) var(--ease-out-reverse),
              backdrop-filter var(--dur-modal) var(--ease-out-reverse),
              visibility var(--dur-modal);
}
.modal-backdrop.is-open {
  opacity: 1;
  visibility: visible;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  transition: opacity var(--dur-modal) var(--ease-out),
              -webkit-backdrop-filter var(--dur-modal) var(--ease-out),
              backdrop-filter var(--dur-modal) var(--ease-out),
              visibility var(--dur-modal);
}

[data-theme="dark"] .modal-backdrop {
  background: rgba(0, 0, 0, .75);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .modal-backdrop {
    background: rgba(0, 0, 0, .75);
  }
}

.modal-dialog {
  position: relative;
  max-width: 580px;
  width: 100%;
  max-height: 85vh;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-pop);
  overflow: hidden;
  /* A modal is the one surface with no trigger to come out of, so it keeps a
     centred origin — there is nowhere on screen it would be honest to say it
     came from. Everything anchored gets transform-origin instead. */
  transform: scale(.96) translateY(8px);
  will-change: transform;
  transition: transform var(--dur-modal) var(--ease-out-reverse);
}
.modal-dialog-sm {
  max-width: 440px;
}
.modal-backdrop.is-open .modal-dialog {
  transform: scale(1) translateY(0);
  transition: transform var(--dur-modal) var(--ease-out);
}

.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.125rem 1.25rem .75rem;
  gap: 1rem;
}

.modal-badge-row {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;
}

.modal-close-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: var(--r);
  border: 1px solid transparent;
  background: transparent;
  color: var(--ink-faint);
  cursor: pointer;
  transition: background .12s, color .12s, border-color .12s;
  flex-shrink: 0;
}
@media (hover: hover) and (pointer: fine) {
  .modal-close-btn:hover {
    background: var(--surface-2);
    color: var(--ink);
    border-color: var(--border);
  }
}


.modal-summary {
  padding: 0 1.25rem .875rem;
  border-bottom: 1px solid var(--border-soft);
}

.modal-title {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1.35;
  color: var(--ink);
  margin: 0 0 .625rem;
}

.modal-meta-row {
  display: flex;
  align-items: center;
  gap: .625rem;
}

.modal-meta-info {
  display: flex;
  flex-direction: column;
  gap: .125rem;
}

.modal-author {
  font-size: .8125rem;
  font-weight: 600;
  color: var(--ink);
  line-height: 1.2;
}

.modal-date {
  font-size: .75rem;
  color: var(--ink-faint);
  line-height: 1.2;
}

.modal-body {
  padding: 1.125rem 1.25rem;
  overflow-y: auto;
  flex: 1;
  min-height: 80px;
}

.modal-content-text {
  font-size: .9375rem;
  line-height: 1.65;
  color: var(--ink);
  white-space: pre-line;
  word-break: break-word;
}

.modal-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: .5rem;
  padding: .75rem 1.25rem;
  background: var(--surface-2);
  border-top: 1px solid var(--border);
}


/* ==========================================================================
   Search inputs: no browser clear button
   The filter bar has its own Clear, and Chrome's blue cross sat on top of the
   suggestions arrow in every name and code field.
   ========================================================================== */

input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
  appearance: none;
  display: none;
}

/* ==========================================================================
   Panels that open their page
   One real link inside the panel, stretched over it, so the whole panel is the
   target while the page still has a single focusable, named link per panel.
   ========================================================================== */

.is-linked { position: relative; }
.cover-link { color: inherit; text-decoration: none; }
.cover-link::after { content: ""; position: absolute; inset: 0; z-index: 1; }
.cover-link:focus-visible { outline: none; }
.is-linked:has(.cover-link:focus-visible) { outline: 2px solid var(--brand); outline-offset: -2px; }

.figure.is-linked { cursor: pointer; transition: background .15s ease; }
@media (hover: hover) and (pointer: fine) {
  .figure.is-linked:hover { background: var(--surface-2); }
}


.card.is-linked { cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease; }
@media (hover: hover) and (pointer: fine) {
  .card.is-linked:hover { border-color: var(--border-strong); box-shadow: 0 4px 14px rgb(0 0 0 / .06); }
}

.card-go {
  width: 14px; height: 14px; flex: 0 0 auto;
  color: var(--ink-faint);
  transition: transform .15s ease, color .15s ease;
}
@media (hover: hover) and (pointer: fine) {
  .is-linked:hover .card-go { transform: translateX(2px); color: var(--brand); }
}


/* ==========================================================================
   Salary components
   ========================================================================== */

.split-bar { height: 14px; }
.split-legend { margin-top: .75rem; }
.split-note { margin: .75rem 0 0; font-size: .75rem; color: var(--ink-soft); }
.split-note.is-bad { color: var(--bad); }

/* Notice period. Someone who has resigned opens the portal to see one number,
   so it is the first thing on the page and the largest thing on the card. */
.notice-card { border-color: var(--warn-line); }
.notice-card.is-approved { border-color: var(--info-line); }
.notice-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
  margin-bottom: .5rem;
}
.notice-count {
  display: flex; align-items: baseline; gap: .4375rem;
  margin-bottom: .375rem;
}
.notice-count strong {
  font-family: var(--font-display);
  font-size: 1.75rem;
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.notice-count span { font-size: .8125rem; color: var(--ink-soft); }
.notice-card .meta { font-size: .75rem; }

.comp-columns { align-items: start; }
.comp-list { list-style: none; margin: 0; padding: 0; }
.comp {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .75rem .875rem;
  border-bottom: 1px solid var(--border-soft);
}
.comp:last-child { border-bottom: 0; }
.comp-code {
  flex: 0 0 3.5rem;
  height: 2.25rem;
  display: grid;
  place-items: center;
  padding: 0 .25rem;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-weight: 600;
  border-radius: var(--r-sm);
  border: 1px solid var(--muted-line);
  background: var(--muted-bg);
  color: var(--ink-soft);
}
.comp-earning .comp-code { background: var(--ok-bg); color: var(--ok); border-color: var(--ok-line); }
.comp-deduction .comp-code { background: var(--bad-bg); color: var(--bad); border-color: var(--bad-line); }
.comp-main { flex: 1 1 auto; min-width: 0; }
.comp-name {
  display: flex; flex-wrap: wrap; align-items: center; gap: .375rem;
  font-weight: 600; color: var(--ink);
}
.comp-sub { font-size: .75rem; color: var(--ink-faint); margin-top: .125rem; }
.comp-amount {
  display: flex; flex-direction: column; align-items: flex-end;
  text-align: right; font-variant-numeric: tabular-nums;
}
.comp-amount strong {
  font-family: var(--font-display);
  font-size: 1rem; font-weight: 600; letter-spacing: -.01em; color: var(--ink);
}
.comp-amount span { font-size: .6875rem; color: var(--ink-faint); white-space: nowrap; }
.comp.is-retired .comp-code,
.comp.is-retired .comp-main,
.comp.is-retired .comp-amount { opacity: .55; }
.comp-empty { padding: 1.25rem .875rem; font-size: .8125rem; color: var(--ink-faint); text-align: center; }

/* An announcement row: what it says, then what it is doing. */
.ann-title { display: flex; align-items: center; gap: .375rem; min-width: 0; }
.ann-snippet { display: block; }
@media (hover: hover) and (pointer: fine) {
  .announcement-open-btn:hover .ann-title .strong { color: var(--brand); }
}
.ann-pin { flex: 0 0 auto; color: var(--brand); }
.ann-snippet {
  margin-top: .125rem;
  max-width: 42rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The sort control. Chips rather than a dropdown: which order the list is in
   is worth being able to read without opening anything. */
.sortbar { display: flex; flex-wrap: wrap; align-items: center; gap: .375rem; }
.sortbar-label {
  font-size: .6875rem;
  font-weight: 600;
  letter-spacing: .02em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-right: .125rem;
}
.sort-chip {
  display: inline-flex; align-items: center; gap: .25rem;
  padding: .25rem .5625rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--ink-soft);
  font-size: .75rem;
  text-decoration: none;
  white-space: nowrap;
}
@media (hover: hover) and (pointer: fine) {
  .sort-chip:hover { border-color: var(--border-strong); color: var(--ink); }
}
.sort-chip.is-on {
  border-color: var(--action);
  background: var(--action);
  color: var(--action-ink);
  font-weight: 500;
}

/* ==========================================================================
   Salary structures
   A structure is read against the others beside it, so every card uses the
   same four columns and the amounts line up down the page.
   ========================================================================== */

.structure-grid { align-items: start; }
.structure-card { display: flex; flex-direction: column; }
.structure-body { flex: 1 1 auto; display: flex; flex-direction: column; gap: .625rem; }
.structure-note { margin: 0; font-size: .75rem; }
.structure-lines { list-style: none; margin: 0; padding: 0; }
.structure-line {
  display: grid;
  grid-template-columns: 3.25rem minmax(0, 1fr) auto 4.5rem;
  align-items: center;
  gap: .625rem;
  padding: .4375rem 0;
  border-bottom: 1px solid var(--border-soft);
  font-size: .8125rem;
}
.structure-line:last-child { border-bottom: 0; }
.structure-code {
  padding: .125rem .25rem;
  border-radius: var(--r-sm);
  border: 1px solid var(--ok-line);
  background: var(--ok-bg);
  color: var(--ok);
  font-size: .6875rem;
  font-weight: 600;
  text-align: center;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.structure-line.is-deduction .structure-code {
  border-color: var(--bad-line); background: var(--bad-bg); color: var(--bad);
}
.structure-name {
  display: flex; align-items: center; gap: .375rem;
  min-width: 0;
  color: var(--ink);
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.structure-basis { font-size: .6875rem; white-space: nowrap; }
.structure-value {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--ink);
}
.structure-line.is-empty {
  display: block;
  padding: .875rem 0;
  text-align: center;
  font-size: .8125rem;
}
.structure-foot { display: flex; align-items: center; gap: .5rem; }

@media (max-width: 640px) {
  .structure-line { grid-template-columns: 3rem minmax(0, 1fr) auto; }
  .structure-basis { display: none; }
}

@media (max-width: 640px) {
  .comp { flex-wrap: wrap; }
  .comp-amount { order: 3; flex-basis: 100%; align-items: flex-start; text-align: left; padding-left: 4.25rem; }
  .comp-amount span { white-space: normal; }
}

/* ==========================================================================
   Employee form: leave for the year
   One quiet row per leave type. The tick is the decision, so it sits with the
   name it decides; an unticked type fades rather than disappearing.
   ========================================================================== */

.alloc {
  max-width: 58rem;
  margin-top: 1.75rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--border);
}
.alloc h3 { margin: 0; font-size: .875rem; font-weight: 600; color: var(--ink); }
.alloc-intro { margin: .1875rem 0 0; font-size: .75rem; color: var(--ink-faint); }

.alloc-list {
  list-style: none;
  margin: .875rem 0 0;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r);
  background: var(--surface);
}
.alloc-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem 1.25rem;
  padding: .625rem .875rem;
  border-bottom: 1px solid var(--border-soft);
}
.alloc-row:last-child { border-bottom: 0; }

.alloc-type {
  flex: 1 1 14rem;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: .625rem;
  cursor: pointer;
}
.alloc-swatch {
  align-self: stretch;
  width: 3px;
  min-height: 1.875rem;
  border-radius: 2px;
  background: var(--swatch, var(--border-strong));
}
.alloc-text { display: flex; flex-direction: column; min-width: 0; }
.alloc-name { font-size: .8125rem; font-weight: 600; color: var(--ink); }
.alloc-meta { font-size: .6875rem; color: var(--ink-faint); }

.alloc-days { position: relative; flex: 0 0 6.5rem; }
.alloc-days .form-input {
  max-width: none;
  padding-right: 2.625rem;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.alloc-unit {
  position: absolute;
  right: .625rem;
  top: 50%;
  transform: translateY(-50%);
  font-size: .6875rem;
  color: var(--ink-faint);
  pointer-events: none;
}

.alloc-usage {
  flex: 0 0 6.5rem;
  text-align: right;
  font-size: .75rem;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
  line-height: 1.35;
}
.alloc-usage strong { color: var(--ink); font-weight: 600; }
.alloc-usage span { display: block; font-size: .6875rem; }

.alloc-row .error { flex-basis: 100%; font-size: .6875rem; color: var(--bad); padding-left: 2.25rem; }

.alloc-row:has(.alloc-type input:not(:checked)) :is(.alloc-text, .alloc-swatch, .alloc-days, .alloc-usage) {
  opacity: .4;
}

.alloc-sr {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

@media (max-width: 560px) {
  .alloc-usage { flex-basis: auto; text-align: left; }
  .alloc-usage span { display: inline; margin-left: .25rem; }
}
.comp-edit { flex: 0 0 auto; position: relative; z-index: 2; }

/* Number inputs: no browser spinner arrows. They are too small to hit, step by
   one when most fields here take money or half days, and scroll-wheel changes
   through them silently alter a value. People type the number instead. */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
}
input[type="number"] {
  -moz-appearance: textfield;
  appearance: textfield;
}

/* Employee dashboard: the shift this person is expected to keep, stated next
   to the check-in button it is judged against. */
.shift-strip {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin-top: .875rem;
  padding: .5625rem .6875rem;
  border: 1px solid var(--border-soft);
  border-radius: var(--r-sm);
  background: var(--surface-2);
}
.shift-strip > .icon { flex: 0 0 auto; color: var(--ink-faint); }
.shift-main {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .25rem .5rem;
  font-size: .8125rem;
  color: var(--ink);
}
.shift-label {
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .085em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* ==========================================================================
   Signed-out pages: the Dissolvex mark
   A framed tile, so the mark has an edge on either theme. Both colourways are
   in the markup and the theme decides which shows, following the toggle as
   well as the system setting.
   ========================================================================== */

.auth-logo {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  margin: 0 auto .75rem;
  padding: 8px;
  box-sizing: border-box;
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 1px 2px rgb(0 0 0 / .05);
}
.auth-logo img { display: block; width: 100%; height: 100%; object-fit: contain; }
.auth-logo .on-dark { display: none; }

:root[data-theme="dark"] .auth-logo .on-light { display: none; }
:root[data-theme="dark"] .auth-logo .on-dark { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .auth-logo .on-light { display: none; }
  :root:not([data-theme="light"]) .auth-logo .on-dark { display: block; }
}

/* Centred table columns — short counts read better under their heading. */
.center,
table.data td.center,
table.data th.center { text-align: center; }

/* Notification dropdown: a scrolling list, an empty state, and one way on to
   the full page. */
.pop-notifications .pop-list { max-height: 22rem; overflow-y: auto; }
.pop-empty { padding: 1.25rem .75rem; text-align: center; font-size: .75rem; color: var(--ink-faint); }
.pop-foot { padding: .5rem .625rem; border-top: 1px solid var(--border); background: var(--surface-2); }

/* Sign in: the reset link sits under the field it is about. */
.forgot-link { align-self: flex-end; margin-top: .125rem; }

/* Goal progress as a ring rather than a bar. */
.ring {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 42px;
  height: 42px;
  flex: 0 0 auto;
}
.ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring circle { fill: none; stroke-width: 3.5; }
.ring-track { stroke: var(--surface-3); }
.ring-value { stroke: var(--brand); stroke-linecap: round; transition: stroke-dasharray .4s ease; }
.ring.is-done .ring-value { stroke: var(--ok); }
.ring-label {
  position: absolute;
  font-size: .625rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.ring-sm { width: 34px; height: 34px; }
.ring-sm .ring-label { font-size: .5625rem; }

/* Resignation: exit clearance — what the leaver still holds or is owed. */
.clearance-sections { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); }
.clearance-section { padding: .75rem .875rem; border-top: 1px solid var(--border-soft); }
.clearance-section h3 {
  margin: 0 0 .375rem;
  font-size: .625rem;
  font-weight: 600;
  letter-spacing: .085em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.clearance-list { list-style: none; margin: 0; padding: 0; }
.clearance-item {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .4375rem 0;
  border-bottom: 1px solid var(--border-soft);
}
.clearance-item:last-child { border-bottom: 0; }
.clearance-mark {
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  border: 1.5px solid var(--border-strong);
  border-radius: 50%;
  color: #fff;
}
.clearance-item.is-done .clearance-mark { background: var(--ok); border-color: var(--ok); }
.clearance-mark .icon { width: 11px; height: 11px; stroke-width: 3; }
/* An open item's circle is a button: HR ticks it off by hand. */
.clearance-toggle { display: contents; }
.clearance-mark.is-button {
  padding: 0;
  background: var(--surface);
  cursor: pointer;
  font: inherit;
  transition: border-color .12s, background .12s, box-shadow .12s;
}
.clearance-item.is-done .clearance-mark.is-button { background: var(--ok); }
@media (hover: hover) and (pointer: fine) {
  .clearance-mark.is-button:hover { border-color: var(--ok); box-shadow: 0 0 0 3px var(--ok-bg); }
}
.clearance-mark.is-button:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.clearance-text { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
.clearance-label { font-size: .8125rem; font-weight: 500; color: var(--ink); }
.clearance-item.is-done .clearance-label { color: var(--ink-soft); }
.clearance-empty { margin: 0; font-size: .75rem; color: var(--ink-faint); }
.clearance .card-head { align-items: flex-start; }

/* Platform console: the Dissolvex mark in the sidebar, in the colourway that
   reads on the current theme. */
.sidebar-brand .on-dark { display: none; }
:root[data-theme="dark"] .sidebar-brand .on-light { display: none; }
:root[data-theme="dark"] .sidebar-brand .on-dark { display: block; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .sidebar-brand .on-light { display: none; }
  :root:not([data-theme="light"]) .sidebar-brand .on-dark { display: block; }
}

/* Export menu: one button, every format. */
.export-menu { min-width: 11rem; }
.export-menu .pop-item { align-items: center; }

/* Messages: icon, text, optional action and the close button on one centred
   line. The close button gets a fixed square so its "x" sits in the middle
   rather than riding the top of a multi-line message. */
.msg { align-items: center; }
.msg > .icon { flex: 0 0 auto; margin-top: 0 !important; }
.msg-action { flex: 0 0 auto; white-space: nowrap; }
.msg-close {
  flex: 0 0 auto;
  align-self: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  font-size: 1.125rem;
  line-height: 1;
}
@media (max-width: 640px) {
  .msg { flex-wrap: wrap; }
  .msg > span { flex: 1 1 calc(100% - 3.5rem); }
  .msg-action { margin-left: 1.5rem; }
}

/* Employee code shown beside a name, so people who share a name can be told
   apart. Quiet enough not to compete with the name itself. */
.person-code {
  font-family: var(--mono);
  font-size: .72em;
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--ink-faint);
  white-space: nowrap;
  margin-left: .25rem;
}

/* Leave balances for one person: who, their totals, then every leave type. */
.balance-person { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.balance-person-text { flex: 1 1 14rem; min-width: 0; }
.balance-person-name { font-size: 1rem; font-weight: 600; color: var(--ink); }
.balance-person-figures { display: flex; gap: 1.75rem; }
.balance-figure {
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
}
.balance-figure.is-strong { color: var(--ink); }
.balance-total td { border-top: 1px solid var(--border); background: var(--surface-2); }

/* Attendance correction: what was asked for beside what the record says, and
   the reason in full. */
.correction-times { display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); gap: .875rem; }
.correction-times .is-changed { color: var(--brand); font-weight: 600; }
.correction-reason {
  margin: .375rem 0 0;
  white-space: pre-line;
  word-break: break-word;
  line-height: 1.6;
  color: var(--ink);
}

/* Month switcher on a person's overtime page. */
.month-nav { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
.month-nav-title { margin: 0; font-size: 1rem; font-weight: 600; color: var(--ink); }

/* A leave type wears its own colour wherever it is named. */
.leave-tag { display: inline-flex; align-items: center; gap: .4rem; min-width: 0; }
.leave-dot { width: 9px; height: 9px; border-radius: 3px; flex: 0 0 auto; }

/* Leave on the employee's calendar takes the type's colour too. */

/* ==========================================================================
   Response
   ==========================================================================
   The interface answering a press, before anything else happens.

   This section exists because the product had no answer at all: not one
   `:active` rule in the whole stylesheet, so every button, row and link was
   hover-only. Hover does not exist on a touchscreen, which meant that on a
   phone — where this product also ships as an app — nothing acknowledged a
   finger until the next page painted. A press that lands 200ms before any
   feedback reads as a broken button, and people tap it again.

   Two shapes of press, chosen by the size of the thing:

   - Something you can cover with a fingertip — a button, a chip, a close
     control — takes `scale()`. It scales its label and icon with it, which is
     what makes it read as one physical object being pushed rather than a box
     changing size.
   - Something wider than a finger — a table row, a nav link, a linked card —
     takes a background shift instead. A full-width row that shrinks looks like
     a rendering fault, not a press.

   Everything here is 140ms and ease-out: long enough to see, short enough that
   the finger is still down when it finishes. No hover gating is needed — a
   press is a real press on every input device.
   -------------------------------------------------------------------------- */

/* --- scale presses ------------------------------------------------------- */

.btn {
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.btn:active { transform: scale(.97); }
/* A disabled button is not refusing the press, it is not hearing it. */
.btn:disabled:active { transform: none; }

/* Small targets need more scale to read as the same amount of push — 3% of a
   30px icon button is under a pixel. */
.icon-btn,
.modal-close-btn,
.password-toggle-btn,
.msg-close,
.remove-line-btn,
.chip-clear,
.cal-nav-btn,
.cal-tab-btn,
.pager-page,
.pager-step {
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.icon-btn:active,
.modal-close-btn:active,
.password-toggle-btn:active,
.msg-close:active,
.remove-line-btn:active,
.chip-clear:active,
.cal-nav-btn:active,
.cal-tab-btn:active,
.pager-page:active,
.pager-step:active { transform: scale(.92); }

.rate-chip span {
  transition: border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.rate-chip:active span { transform: scale(.94); }

/* --- surface presses ----------------------------------------------------- */
/* Wider than a fingertip, so the press is a shade rather than a squash. */

.navgroup-head,
.navgroup-links a,
.sidebar-signout,
.pop-item,
.tabs a,
.check-bare {
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.navgroup-head:active,
.navgroup-links a:active,
.sidebar-signout:active,
.pop-item:active { background: var(--surface-3); color: var(--ink); }

.tabs a:active { color: var(--ink); }

table.data tbody tr:active,
.perm-grid tbody tr:active { background: var(--surface-3); }

.check-row {
  transition: border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out);
}
.check-row:active { background: var(--surface-3); }
.check-bare:active { background: var(--surface-3); }

a.figure,
.figure.is-linked,
.announcement-item,
.announcement-card,
.filterbox-btn,
.filterbox-head {
  transition: background var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out);
}
a.figure:active,
.figure.is-linked:active,
.announcement-item:active,
.filterbox-btn:active,
.filterbox-head:active { background: var(--surface-3); }

/* A linked card is a big surface, so it presses by settling rather than by
   shrinking — the shadow drops and it sits down onto the page. */
.card.is-linked {
  transition: border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out),
              transform var(--dur-press) var(--ease-out);
}
.card.is-linked:active,
.announcement-card:active {
  transform: scale(.995);
  box-shadow: var(--shadow-card);
}

/* ==========================================================================
   Scroll edge
   ==========================================================================
   A floating bar only needs to separate itself from content when there is
   content underneath it. At the top of a page the hairline under the utility
   bar is drawing a line between a heading and nothing.

   So the border is carried by a state class app.js sets from scroll position,
   and it arrives as a hairline plus a short gradient rather than a hard rule —
   the edge where a material meets what it is floating over, instead of a box
   boundary. Without JavaScript the class never comes off and the bar keeps the
   border it has always had.
   -------------------------------------------------------------------------- */

/* The state class is `is-at-top`, not `is-scrolled`, and that direction is
   deliberate: the bar keeps the border it has always had, and JavaScript takes
   it away while there is nothing underneath. Written the other way round, a
   page with JavaScript off or still loading would show a bar with no edge at
   all — the new behaviour has to be the thing that is added, never the thing
   the old behaviour depends on. */
.utilitybar {
  transition: border-color var(--dur-pop) var(--ease-out);
}
.utilitybar.is-at-top { border-bottom-color: transparent; }

.utilitybar::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 100%;
  height: 12px;
  pointer-events: none;
  opacity: 1;
  transition: opacity var(--dur-pop) var(--ease-out);
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--bg) 70%, transparent),
    transparent
  );
}
.utilitybar.is-at-top::after { opacity: 0; }

/* ==========================================================================
   Charts you can touch
   ==========================================================================
   The ring's segments answered a hover and nothing else, which on a phone is
   not a reduced affordance but an absent one. Every figure is in the legend as
   text either way, so nothing was ever unreadable — what was missing is which
   arc a given row refers to, and with eight series two neighbouring colours
   are not always enough to tell.

   So the pairing is driven from a press on either half, and the emphasis lands
   on both at once. The legend row is the target that matters: it is a whole
   line of text, where the arc is a ring nine pixels thick.
   -------------------------------------------------------------------------- */

.legend li {
  transition: background var(--dur-fast) var(--ease-out);
  border-radius: var(--r-sm);
  cursor: default;
}
.legend li:active,
.legend li.is-linked-active { background: var(--surface-2); }

.donut-seg {
  transition: stroke-width var(--dur-fast) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out);
}
.donut-seg:active,
.donut-seg.is-linked-active { stroke-width: calc(var(--thickness) + 3px); }

/* While one segment is called out the others step back, so the emphasis is a
   comparison rather than a slightly fatter arc among equals. */
.donut.is-isolating .donut-seg:not(.is-linked-active) { opacity: .35; }

/* ==========================================================================
   Vibrancy
   ==========================================================================
   Text on a solid surface has one backdrop and can be tuned against it. Text
   on a material has whatever happens to be sliding underneath — a table row,
   a coloured pill, an avatar — and the contrast it was given against a flat
   panel is not the contrast it gets against that.

   So the type on the chrome gets pulled up a step: one tone stronger, half a
   weight heavier, and a touch more tracking, which is what keeps small text
   from filling in when a busy row passes below it. Not a colour change — the
   same token ladder, one rung up.

   Colour still belongs on solid ground. Pills and status text keep their own
   backgrounds rather than being tinted onto the translucent layer, because a
   hue over a moving backdrop is the first thing to become unreadable.
   -------------------------------------------------------------------------- */

.utilitybar .utilitybar-crumb,
.utilitybar .utilitybar-crumb .crumb a {
  color: var(--ink-soft);
  font-weight: 500;
  letter-spacing: .012em;
}

@media (max-width: 1000px) {
  /* The drawer is only a material at this width; above it there is nothing
     behind it to show through and the plain tokens are correct. */
  .sidebar .sidebar-kind,
  .sidebar .navgroup-head {
    color: var(--ink-soft);
    letter-spacing: .012em;
  }
  .sidebar .navgroup-links a { font-weight: 500; }
}

/* ==========================================================================
   Safe areas
   ==========================================================================
   viewport-fit=cover in base.html lets the page under the notch and the home
   indicator; these pad the content back out of them. Every value is 0px on a
   device with no insets, and on every desktop browser, so none of this is
   visible anywhere it is not needed.
   -------------------------------------------------------------------------- */

/* The bottom inset goes on the column, not on .content. .content's padding is
   re-declared at three breakpoints and again in the print stylesheet, where it
   is zero — a padding-bottom set here would outrank all four and put a 1.25rem
   margin at the foot of every printed payslip. .main carries no padding of its
   own, so adding it here clears the home indicator and collides with nothing. */
.main { padding-bottom: env(safe-area-inset-bottom, 0px); }
@media print { .main { padding-bottom: 0; } }

/* Only the left inset here: .sidebar-foot already lifts Sign out clear of the
   home indicator, and adding the bottom inset to the container as well would
   pad the same gap twice. Left is for landscape, where the notch takes a bite
   out of the side the drawer opens from. */
.sidebar { padding-left: env(safe-area-inset-left, 0px); }

.modal-dialog { margin-bottom: env(safe-area-inset-bottom, 0px); }

/* Only the topmost bar takes the top inset, or the two stack and the notch gets
   padded twice. The utility bar is that bar for everyone except platform staff
   working inside a tenant, who get the impersonation banner above it. */
.impersonation-bar { padding-top: env(safe-area-inset-top, 0px); }
.impersonation-bar + .utilitybar {
  padding-top: 0;
  height: var(--utility-h);
  min-height: var(--utility-h);
}

/* ==========================================================================
   Accessibility preferences beyond reduced motion
   ==========================================================================
   Translucency is the first thing to go for anyone who has asked for less of
   it, or for more contrast. Both queries put the chrome back to a solid
   surface; the layout, the spacing and the hierarchy are unchanged, so nothing
   moves — the material just stops being a material.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-transparency: reduce) {
  .utilitybar,
  .pop,
  .pop-wrap.is-open .pop {
    background: var(--surface);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .sidebar {
    background: var(--surface-2);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .modal-backdrop,
  .modal-backdrop.is-open {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: rgba(10, 12, 16, .72);
  }
  /* Vibrancy exists to survive a moving backdrop. With the backdrop gone the
     type goes back to the tone the solid surface was tuned for. */
  .utilitybar .utilitybar-crumb,
  .utilitybar .utilitybar-crumb .crumb a {
    color: var(--ink-faint);
    font-weight: 400;
    letter-spacing: normal;
  }
}

@media (prefers-contrast: more) {
  :root {
    --border: var(--border-strong);
    --ink-faint: var(--ink-soft);
  }
  .utilitybar,
  .pop,
  .pop-wrap.is-open .pop {
    background: var(--surface);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .utilitybar { border-bottom-color: var(--border-strong); }
  .sidebar {
    background: var(--surface-2);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  /* A focus ring legible against a soft border has to win against a strong
     one too. */
  :focus-visible { outline-width: 3px; }
}

/* Print has no chrome to be translucent, and a scroll edge is a screen idea. */
@media print {
  .utilitybar::after { display: none; }
}

/* ==========================================================================
   Utilities
   ==========================================================================
   The layer that replaced four hundred inline style attributes.

   Inline styles were not a shortcut here, they were a second design system
   with no rules: thirteen different top margins across the templates —
   .2rem, .25, .3, .35, .4, .5, .6, .75, .875, 1rem and a stray 2px — chosen
   one at a time, none of them wrong on their own and no two of them agreeing.
   None could be changed centrally, because none of them were anywhere a
   stylesheet could reach.

   Two things happen here. The values collapse onto one scale, and the scale is
   in rem so it still moves with the reader's text size. Nothing shifts by more
   than 0.8px and almost everything by 0.4px or less, which is why this could
   be done to a live product in one pass: it is the same design, finally
   written down.

   This block is last in the file on purpose. An inline style beat every rule
   in the stylesheet; a class only beats the rules above it, so anything that
   used to be overridden by `style=` has to still be overridden from here.
   -------------------------------------------------------------------------- */

:root {
  /* Named for the pixels they are at the default root size, because that is
     what anyone reading a template is picturing. They are declared in rem, so
     a reader who scales their text scales the spacing with it. */
  --s-2: .125rem;
  --s-4: .25rem;
  --s-5: .3125rem;
  --s-6: .375rem;
  --s-7: .4375rem;
  --s-8: .5rem;
  --s-10: .625rem;
  --s-12: .75rem;
  --s-14: .875rem;
  --s-16: 1rem;
  --s-18: 1.125rem;
  --s-20: 1.25rem;
  --s-24: 1.5rem;
}

/* --- space above and below ---------------------------------------------- */

.mt-2  { margin-top: var(--s-2); }
.mt-4  { margin-top: var(--s-4); }
.mt-5  { margin-top: var(--s-5); }
.mt-6  { margin-top: var(--s-6); }
.mt-8  { margin-top: var(--s-8); }
.mt-10 { margin-top: var(--s-10); }
.mt-12 { margin-top: var(--s-12); }
.mt-14 { margin-top: var(--s-14); }
.mt-16 { margin-top: var(--s-16); }
.mt-18 { margin-top: var(--s-18); }
.mt-20 { margin-top: var(--s-20); }
.mt-24 { margin-top: var(--s-24); }

.mb-4  { margin-bottom: var(--s-4); }
.mb-6  { margin-bottom: var(--s-6); }
.mb-7  { margin-bottom: var(--s-7); }
.mb-8  { margin-bottom: var(--s-8); }
.mb-10 { margin-bottom: var(--s-10); }
.mb-12 { margin-bottom: var(--s-12); }
.mb-14 { margin-bottom: var(--s-14); }
.mb-16 { margin-bottom: var(--s-16); }
.mb-20 { margin-bottom: var(--s-20); }

.pt-8  { padding-top: var(--s-8); }
.pt-12 { padding-top: var(--s-12); }
.pt-14 { padding-top: var(--s-14); }
.pt-16 { padding-top: var(--s-16); }

.pt-6  { padding-top: var(--s-6); }

.pb-6  { padding-bottom: var(--s-6); }
.pb-12 { padding-bottom: var(--s-12); }
.pb-14 { padding-bottom: var(--s-14); }
.pb-16 { padding-bottom: var(--s-16); }

.p-0  { padding: 0; }
.p-14 { padding: var(--s-14); }
.p-24 { padding: var(--s-24); }

.ml-8 { margin-left: var(--s-8); }
/* An icon set off from the word beside it. Small enough that it reads as
   letter-spacing rather than layout, which is why it is here and not a gap. */
.mr-2 { margin-right: var(--s-2); }
.mr-4 { margin-right: var(--s-4); }

/* Hanging indent for a list that has to line up under its own first line. */
.pl-18 { padding-left: var(--s-18); }

.m-0 { margin: 0; }

/* --- space between ------------------------------------------------------- */

.gap-2  { gap: var(--s-2); }
.gap-4  { gap: var(--s-4); }
.gap-7  { gap: var(--s-7); }
.gap-5  { gap: var(--s-5); }
.gap-6  { gap: var(--s-6); }
.gap-8  { gap: var(--s-8); }
.gap-10 { gap: var(--s-10); }
.gap-12 { gap: var(--s-12); }
.gap-16 { gap: var(--s-16); }
.gap-18 { gap: var(--s-18); }
.gap-20 { gap: var(--s-20); }

/* --- how a box behaves in its parent ------------------------------------- */

.grow  { flex: 1; }
.min-0 { min-width: 0; }
.w-full { width: 100%; }
.span-all { grid-column: 1 / -1; }

.flex { display: flex; }
.inline { display: inline; }
/* Named with the state prefix the rest of the sheet uses, and kept away from
   the `hidden` attribute, which the browser already handles. */
.is-hidden { display: none; }

.col { flex-direction: column; }
.wrap { flex-wrap: wrap; }
.items-start { align-items: flex-start; }
.items-center { align-items: center; }
.items-end { align-items: flex-end; }
/* Aligns to the text baseline rather than the box, which is what a number and
   its unit sitting beside each other actually want. */
.items-baseline { align-items: baseline; }
.between { justify-content: space-between; }
.justify-start { justify-content: flex-start; }

/* --- text ---------------------------------------------------------------- */

.right { text-align: right; }
.center { text-align: center; }
.left { text-align: left; }
/* A token or a URL with no spaces in it, which would otherwise run out of its
   container rather than wrap. */
.break-all { word-break: break-all; }
.text-warn { color: var(--warn); }
/* Text that arrived with its own line breaks — a typed-in note, an address —
   and must keep them without being turned into a code block. */
.prewrap { white-space: pre-line; }
/* A comfortable line length for prose inside a form-width column. */
.measure { max-width: 40rem; }
.text-lg { font-size: 1.25rem; }
.text-xl { font-size: 1.5rem; }

/* A plain element that a script has made clickable. The cursor is the only
   thing saying so, which is why it is worth a name rather than a one-off. */
.pointer { cursor: pointer; }

/* --- rules --------------------------------------------------------------- */

.rule-top { border-top: 1px solid var(--border); }
.rule-top-soft { border-top: 1px solid var(--border-soft); }
.rule-bottom { border-bottom: 1px solid var(--border); }
.rule-bottom-soft { border-bottom: 1px solid var(--border-soft); }

/* The most-repeated composite in the templates: a run of content set off from
   what came before by space and a hairline. It was written out by hand nine
   times, with three different top margins. */
.section-rule {
  margin-top: var(--s-16);
  padding-top: var(--s-14);
  border-top: 1px solid var(--border);
}
.section-rule-soft {
  margin-top: var(--s-12);
  padding-top: var(--s-12);
  border-top: 1px solid var(--border-soft);
}

/* --- my attendance: month view ---------------------------------------------
   The holiday calendar's grid, with each whole day tinted by what it was —
   the same tones as the list's pills — so a month of absences reads as red
   before a word is read. No box inside the box: the day is the colour. */
.cal-cell.att-fill.is-ok    { background: var(--ok-bg); }
.cal-cell.att-fill.is-warn  { background: var(--warn-bg); }
.cal-cell.att-fill.is-bad   { background: var(--bad-bg); }
.cal-cell.att-fill.is-info  { background: var(--info-bg); }
.cal-cell.att-fill.is-brand { background: var(--brand-soft); }
.cal-cell.att-fill.is-muted { background: var(--muted-bg); }
.cal-cell.att-fill.is-ok    .att-status { color: var(--ok); }
.cal-cell.att-fill.is-warn  .att-status { color: var(--warn); }
.cal-cell.att-fill.is-bad   .att-status { color: var(--bad); }
.cal-cell.att-fill.is-info  .att-status { color: var(--info); }
.cal-cell.att-fill.is-brand .att-status { color: var(--brand); }
.cal-cell.att-fill.is-muted .att-status { color: var(--ink-soft); }

.att-status {
  font-size: .75rem;
  font-weight: 700;
  line-height: 1.25;
}
.att-times,
.att-hours {
  font-size: .6875rem;
  line-height: 1.35;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.att-late {
  font-size: .625rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--warn);
}


.att-swatch.is-ok   { background: var(--ok); }
.att-swatch.is-warn { background: var(--warn); }
.att-swatch.is-bad  { background: var(--bad); }
.att-swatch.is-info { background: var(--info); }
.att-swatch.is-muted { background: var(--border-strong); }

@media (max-width: 720px) {
  /* A phone-width day has room for the outcome, not the detail; the List
     view carries the times. */
  .att-times, .att-hours { display: none; }
  .att-status { font-size: .5625rem; }
}

/* --- directory cards ------------------------------------------------------
   A grid, not a wrapping row: with flex-wrap, a card whose email was wider
   than the space beside the photo dropped its whole text block underneath
   it, so some cards stacked and their neighbours did not. The text column is
   minmax(0, 1fr), so a long name or address wraps inside it instead — an
   email is the thing people come here to read, so it is never cut off. */
.dir-card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: .875rem;
  align-items: start;
  padding: 1rem 1.125rem;
}
.dir-body { min-width: 0; display: flex; flex-direction: column; gap: .125rem; }
.dir-name {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: .375rem;
  min-width: 0;
  margin-bottom: .125rem;
}
.dir-name .person-name { font-weight: 600; overflow-wrap: anywhere; }
.dir-role { font-size: .8125rem; color: var(--ink-soft); }
.dir-dept { font-size: .75rem; color: var(--ink-faint); }
.dir-email {
  /* Breaks after the @ first (email_wrap puts a <wbr> there); mid-word only
     when one half alone is wider than the card. */
  overflow-wrap: break-word;
  margin-top: .375rem;
  font-family: var(--mono);
  font-size: .75rem;
  color: var(--ink-soft);
}
@media (hover: hover) and (pointer: fine) {
  .dir-email:hover { color: var(--brand); }
}

/* --- dropdowns (see "Dropdowns" in app.js) -------------------------------- */
.combo { position: relative; }
.combo-native {
  /* Kept in the form and in the layout, so it still submits and `required`
     still validates — just not seen or reached. */
  position: absolute;
  inset: 0;
  width: 100%;
  opacity: 0;
  pointer-events: none;
}
.combo-input {
  padding-right: 1.875rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.75 6 6.5l5-4.75' fill='none' stroke='%23808894' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .625rem center;
  background-size: 11px 8px;
  text-overflow: ellipsis;
}
.combo-input.is-static { cursor: pointer; caret-color: transparent; }
.combo-input:disabled { cursor: not-allowed; opacity: .6; }
.combo.is-open .combo-input {
  border-color: var(--brand);
  box-shadow: 0 0 0 3px var(--brand-soft);
}
.combo.is-invalid .combo-input,
.field.has-error .combo-input { border-color: var(--bad); }

/* Placed by app.js against the window: below the box, or above it when that
   is where the room is — never off-screen, never clipped by a card. */
.combo-list {
  position: fixed;
  z-index: 200;
  max-height: 16rem;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-pop);
  padding: .25rem;
}
.combo-option {
  padding: .4375rem .5rem;
  border-radius: calc(var(--r-sm) - 1px);
  font-size: .8125rem;
  color: var(--ink);
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.combo-option.is-blank { color: var(--ink-faint); }
.combo-option.is-selected { font-weight: 600; }
.combo-option.is-active { background: var(--brand-soft); color: var(--brand); }
.combo-empty {
  padding: .5rem;
  font-size: .75rem;
  color: var(--ink-faint);
}

/* --- typed dates (see "Typed dates" in app.js) ---------------------------- */
.date-text { position: relative; }
.date-text .form-input { padding-right: 2.25rem; font-variant-numeric: tabular-nums; }
.date-text-btn {
  /* Auto margins, not translateY, so a press effect can never move it. */
  position: absolute;
  top: 0;
  bottom: 0;
  margin: auto 0;
  right: .3125rem;
  display: inline-grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
}
@media (hover: hover) and (pointer: fine) {
  .date-text-btn:hover { background: var(--surface-2); color: var(--ink); }
}
/* The original date input stays in the form to carry the ISO value. */
.date-iso { display: none !important; }


/* ==========================================================================
   Phones — one pass over what made the product unusable at 375px
   Each rule answers something measured on every sidebar page at phone width:
   tables that needed a sideways scroll to reach their own actions, four stat
   cards filling the first screen one per row, every avatar blown up to 40px,
   labels at 9–10px beside 28px figures, and the closed drawer's shadow
   bleeding onto the left edge of every page.
   ========================================================================== */

/* The drawer only casts a shadow while it is on screen; closed, a 64px blur
   from just off the left edge greyed the whole left side of the page. */
@media (max-width: 1000px) {
  .sidebar { box-shadow: none; }
  .shell.panel-open .sidebar,
  .shell.is-drawer-moving .sidebar { box-shadow: var(--shadow-drawer); }
}

@media (max-width: 720px) {
  /* Only the account button in the bar is a fingertip target; avatars in
     lists and tables go back to list size. */
  .avatar { width: 28px; height: 28px; font-size: .625rem; }
  .utilitybar .avatar { width: 2.5rem; height: 2.5rem; font-size: .75rem; }
  .avatar-lg { width: 44px; height: 44px; font-size: .875rem; }

  /* Inputs stay at 16px so the phone doesn't zoom on focus, but the hint in
     an empty box was the largest text on the page and got cut mid-word. */
  input::placeholder,
  textarea::placeholder { font-size: .875rem; }

  /* One type scale: a card's heading is never smaller than what's in it,
     and nothing drops under 11px. */
  .card-head h2 { font-size: .9375rem; }
  .stat-label,
  .shift-label { font-size: .6875rem; letter-spacing: .06em; }
  .pill { font-size: .6875rem; }
  .tiny { font-size: .75rem; }
  .meta { font-size: .8125rem; }
  .ring-label { font-size: .625rem; }
  .page-head h1, .content > h1 { font-size: 1.375rem; }
  /* Codes and initials were 9–10px: shrunk relative to text that was already
     small. */
  .avatar { font-size: .6875rem; }
  td.mono, .mono.meta, .person-code { font-size: .75rem; }
  .att-status { font-size: .625rem; }
  /* The breadcrumb squeezed a clipped "Employ…" between the menu and the
     search; the page's own heading says the same thing just below. */
  .utilitybar-crumb { display: none; }

  /* A long button label wraps rather than pushing off the screen. */
  .btn { white-space: normal; max-width: 100%; text-align: center; }
}

@media (max-width: 640px) {
  /* Stat cards two to a row, compact — four of them no longer fill the
     first screen before any data appears. */
  .grid-2:has(> .stat), .grid-3:has(> .stat),
  .grid-4:has(> .stat), .grid-5:has(> .stat) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .5rem;
  }
  .stat { padding: .6875rem .75rem; }
  .stat-value { font-size: 1.375rem; }
  .stat-note { font-size: .6875rem; }

  /* Tables become a card per row: each cell on its own line with its column
     name beside it, so nothing needs a sideways scroll. app.js labels the
     cells (data-label) from the table's own header. */
  /* The cards flow down the page. The desktop cap on a table's height made a
     small scroll box inside the scrolling page, cutting records off. */
  .table-wrap:has(> table.data.is-stackable),
  .card:has(> .table-wrap) > .table-wrap:has(> table.data.is-stackable) {
    overflow: visible;
    max-height: none;
  }
  table.data.is-stackable,
  table.data.is-stackable tbody { display: block; width: 100%; }
  table.data.is-stackable thead { display: none; }
  table.data.is-stackable tr {
    display: block;
    padding: .625rem .875rem;
    border-bottom: 1px solid var(--border);
  }
  table.data.is-stackable tr:last-child { border-bottom: 0; }
  table.data.is-stackable td,
  table.data.is-stackable td.person {
    /* !important only to outrank the desktop rule that pins every cell to
       table-cell with !important (see "Tables" above). */
    display: flex !important;
    align-items: center;
    justify-content: flex-end;
    flex-wrap: wrap;
    gap: .375rem .5rem;
    min-width: 0;
    padding: .1875rem 0;
    border: 0;
    text-align: right;
    white-space: normal;
    overflow-wrap: anywhere;
  }
  table.data.is-stackable td::before {
    content: attr(data-label);
    flex: 0 0 auto;
    margin-right: auto;  /* label left, the cell's contents together on the right */
    max-width: 45%;
    text-align: left;
    font-size: .6875rem;
    font-weight: 600;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--ink-faint);
  }
  /* The first cell is the row's title: no label, full width, left. */
  table.data.is-stackable td:first-child {
    justify-content: flex-start;
    flex-wrap: nowrap;
    text-align: left;
    font-weight: 600;
    padding-bottom: .375rem;
  }
  table.data.is-stackable td:first-child::before,
  table.data.is-stackable td[data-label=""]::before { content: none; }
  /* A cell with no heading is the row's actions. */
  table.data.is-stackable td[data-label=""] { justify-content: flex-end; padding-top: .375rem; }
  table.data.is-stackable td[data-label=""]:empty { display: none !important; }
  /* Empty states and section rows span the whole card. */
  table.data.is-stackable td[colspan] { display: block !important; text-align: left; }
  table.data.is-stackable td[colspan]::before { content: none; }
  /* Avatar beside the name; the name block keeps its own lines (name, then
     email or code under it). */
  table.data.is-stackable td.person > .avatar { display: grid; flex: 0 0 auto; }
  table.data.is-stackable td.person > :not(.avatar) { display: block; min-width: 0; }
}

/* Recruitment pipeline on the overview: one number per stage, in order. */
.pipeline-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(72px, 1fr));
  gap: 8px;
  margin-bottom: 14px;
}
.pipeline-step {
  background: var(--surface-2, var(--bg));
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 10px 8px;
  text-align: center;
}
.pipeline-count {
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

/* Live password checks (app.js). A meter across the top, then the rules laid
   out as a grid that fills the width instead of one narrow column. */
.pw-live {
  margin-top: 10px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface-2);
}
.pw-meter { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.pw-meter-bars { flex: 1; display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.pw-meter-bars span {
  height: 5px; border-radius: 999px; background: var(--border);
  transition: background-color .2s ease;
}
.pw-live[data-strength="1"] .pw-meter-bars span:nth-child(-n+1) { background: var(--bad); }
.pw-live[data-strength="2"] .pw-meter-bars span:nth-child(-n+2) { background: var(--warn); }
.pw-live[data-strength="3"] .pw-meter-bars span:nth-child(-n+3) { background: var(--info); }
.pw-live[data-strength="4"] .pw-meter-bars span:nth-child(-n+4) { background: var(--ok); }
.pw-meter-label {
  min-width: 72px; text-align: right;
  font-size: .75rem; font-weight: 600; color: var(--ink-faint);
}
.pw-live[data-strength="1"] .pw-meter-label { color: var(--bad); }
.pw-live[data-strength="2"] .pw-meter-label { color: var(--warn); }
.pw-live[data-strength="3"] .pw-meter-label { color: var(--info); }
.pw-live[data-strength="4"] .pw-meter-label { color: var(--ok); }

.pw-rules {
  list-style: none; margin: 0; padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 6px 16px;
}
.pw-rule {
  display: flex; align-items: center; gap: 8px;
  font-size: .8125rem; color: var(--ink-soft);
  transition: color .15s ease;
}
.pw-rule-mark {
  flex: none; width: 18px; height: 18px; border-radius: 999px;
  display: grid; place-items: center;
  border: 1.5px solid var(--border-strong);
  color: transparent;
  transition: background-color .15s ease, border-color .15s ease, color .15s ease;
}
.pw-rule-mark .icon { width: 12px; height: 12px; stroke-width: 3; }
.pw-rule.is-met { color: var(--ink); }
.pw-rule.is-met .pw-rule-mark { background: var(--ok); border-color: var(--ok); color: #fff; }
.pw-rule.is-failed { color: var(--bad); }
.pw-rule.is-failed .pw-rule-mark { border-color: var(--bad); }
.pw-error { margin-top: 6px; font-size: .75rem; font-weight: 500; color: var(--bad); }
.pw-error[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) {
  .pw-meter-bars span, .pw-rule, .pw-rule-mark { transition: none; }
}

/* --- the calendar (see "Dates" in app.js) --------------------------------- */
.cal {
  position: fixed;
  z-index: 1000;
  width: 17.5rem;
  padding: .75rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: var(--shadow-pop);
  animation: cal-in .14s ease-out;
}
@keyframes cal-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}
.cal-head {
  display: grid;
  grid-template-columns: 2rem 1fr auto 2rem;
  gap: .375rem;
  align-items: center;
  margin-bottom: .5rem;
}
.cal-month, .cal-year {
  /* Safari draws its own select unless appearance is cleared, so the arrow
     is ours — the same one the form selects use. */
  appearance: none;
  -webkit-appearance: none;
  height: 2rem;
  padding: 0 1.5rem 0 .5rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.75 6 6.5l5-4.75' fill='none' stroke='%23808894' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .4375rem center;
  background-size: 10px 7px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--ink);
  font: inherit;
  font-size: .8125rem;
  font-weight: 600;
  cursor: pointer;
}
.cal-nav {
  display: inline-grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--ink-soft);
  cursor: pointer;
}
.cal-nav:disabled { opacity: .3; cursor: default; }
.cal-weekdays, .cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 2px;
}
.cal-weekdays span {
  padding: .25rem 0;
  text-align: center;
  font-size: .6875rem;
  font-weight: 600;
  color: var(--ink-faint);
}
.cal-day {
  height: 2.125rem;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: .8125rem;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.cal-day.is-outside { color: var(--ink-faint); }
.cal-day.is-today { box-shadow: inset 0 0 0 1.5px var(--brand); font-weight: 600; }
.cal-day.is-selected { background: var(--brand); color: #fff; font-weight: 600; box-shadow: none; }
.cal-day:disabled { color: var(--ink-faint); opacity: .35; cursor: not-allowed; }
.cal-day:focus-visible, .cal-nav:focus-visible, .cal-link:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 1px;
}
.cal-foot {
  display: flex;
  justify-content: space-between;
  margin-top: .5rem;
  padding-top: .5rem;
  border-top: 1px solid var(--border);
}
.cal-link {
  border: 0;
  background: none;
  padding: .25rem .375rem;
  border-radius: 6px;
  color: var(--brand);
  font: inherit;
  font-size: .8125rem;
  font-weight: 600;
  cursor: pointer;
}
.cal-link:disabled { color: var(--ink-faint); cursor: default; }
@media (hover: hover) and (pointer: fine) {
  .cal-nav:not(:disabled):hover, .cal-link:not(:disabled):hover { background: var(--surface-2); }
  .cal-day:not(:disabled):not(.is-selected):hover { background: var(--surface-2); }
}
@media (prefers-reduced-motion: reduce) {
  .cal { animation: none; }
}

/* --- trial countdown (administrators only; see core/context_processors.py) - */
.trial-strip {
  display: flex;
  align-items: center;
  gap: .625rem;
  margin-bottom: .875rem;
  padding: .5rem .75rem;
  border: 1px solid var(--info-line);
  border-radius: var(--r-lg);
  background: var(--info-bg);
  color: var(--info);
  font-size: .8125rem;
}
.trial-strip.is-urgent {
  border-color: var(--warn-line);
  background: var(--warn-bg);
  color: var(--warn);
}
.trial-strip span { flex: 1; min-width: 0; }
.trial-strip-link { color: inherit; font-weight: 600; text-decoration: underline; white-space: nowrap; }

/* --- what a customer has been sold (platform → organization → modules) ---- */
.module-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: .5rem .875rem;
}
.module-toggle {
  display: flex;
  align-items: flex-start;
  gap: .5rem;
  padding: .375rem .125rem;
  cursor: pointer;
}
.module-toggle > span { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.module-toggle input { margin-top: .1875rem; flex: none; }

/* --- plans (org → billing → plans) --------------------------------------- */
.is-current-plan { border-color: var(--ok-line); }
.plan-price {
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin-bottom: .25rem;
}
.plan-price .meta { font-size: .75rem; font-weight: 400; }
