/* =========================================================================
   HR-only additions to the Glisco console theme.

   app.css in this folder is a BYTE-IDENTICAL copy of
   glisco-ops/public/app.css. Nothing HR-specific goes in it, so `diff` on the
   two files is the drift check — and when HR moves into that console, the
   shared half needs no merge at all.

   Everything below is either a component the ops console has no use for yet
   (severity stripes, the approval queue) or a page-level layout that will
   travel with its page.
   ========================================================================= */

/* --------------------------------------------------------- ONE COLUMN ONLY --
   The shell wraps every page in .wrap: 1180px, centred, with its margins.
   Anything inside it that ALSO sets a width or a padding gives that page a
   second, narrower column and different margins from its neighbours — which
   is why some pages started 50px lower than others and ran 80px narrower.

   Six names for the same mistake, collected from the pages: .page-wrap
   (payslips, open positions, reviews, notices), .container (ID cards),
   .container-fluid (employees, master data, users, archived), .wrapper and
   .main-card (exit papers, increment letters), .workspace.

   app-legacy.css neutralised three of them — but eighteen rail pages
   deliberately do not load app-legacy, so on those it did nothing. This file
   every rail page loads.

   A page that genuinely wants a narrower measure uses .wrap--narrow on the
   shell's own wrap. */
/* THE LEGACY CONTAINERS ADOPT THE SHELL'S MEASURE — they do not lose it.
   Zeroing them was half a fix. It stopped them being a second, narrower
   column inside .wrap, and it left the eleven pages that have no .wrap at all
   running full-bleed to the window edge — so walking from Dashboard to
   Reviews moved the left margin and changed the measure, which is exactly
   what "the layout changes between pages" is.

   `.wrap` is `max-width:1180px; margin:0 auto; padding:1.5rem 1.1rem 4rem`.
   A legacy container that is NOT inside a .wrap now gets that same measure,
   so every page in the console has one width and one top edge. One inside a
   .wrap still collapses to nothing, because its parent is already providing
   it. */
.main-content .page-wrap,
.main-content .wrapper,
.main-content .workspace,
.main-content .main-card {
  max-width: 1180px; width: auto; margin: 0 auto;
  padding: 1.5rem 1.1rem 4rem;
  background: none; border: 0; border-radius: 0; box-shadow: none;
}

/* Nested inside the shell's own wrap, they are redundant. */
.main-content .wrap .page-wrap,
.main-content .wrap .wrapper,
.main-content .wrap .workspace,
.main-content .wrap .main-card {
  max-width: none; margin: 0; padding: 0;
}

/* Bootstrap's two containers lose their width and their vertical padding, but
   KEEP their 12px horizontal padding. That padding is not decoration: it is
   what absorbs the -12px margins on .row, and zeroing it — which
   app-legacy.css did for .container-fluid.py-4 — pushes the first and last
   column of every Bootstrap grid 12px outside the page. On Employees that put
   the Export CSV button half off the right edge. */
.main-content .container,
.main-content .container-fluid {
  max-width: 1180px; width: auto; margin: 0 auto;
  padding-top: 1.5rem; padding-bottom: 4rem;
}
.main-content .wrap .container,
.main-content .wrap .container-fluid {
  max-width: none; margin: 0;
  padding-top: 0; padding-bottom: 0;
}

/* The generated document, below the form that generates it.
   Hidden until there is one — the JS on both pages sets style.display when it
   has something to show. Increment Letters had no definition for this at all
   after the retheme deleted its stylesheet, so the page opened with a
   "Download PDF" button under an empty preview and nothing to download. */
.preview-area { display: none; margin-top: 1.4rem; }

/* ---------------------------------------------- the employee-lookup panel --
   Search for somebody, get a list, load one. Increment Letters and Payslips
   both carry this markup — .db-panel / .db-panel-header / .db-panel-title,
   an .emp-search-bar and an #emp-results-box — and it was defined on the
   payslip page only. The retheme deleted Increment's own stylesheet and
   nothing replaced it, so on that page the panel had no ground, no border and
   no heading: "Employee Database" and "70 employees loaded" rendered as two
   lines of unstyled text above a bare input.

   Payslips still has its own copy, which is more specific in that document
   and wins there. This is the one every other page gets. */
.db-panel {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 0.9rem 1rem; margin-bottom: 1rem;
}
.db-panel-header {
  display: flex; align-items: baseline; gap: 0.8rem;
  flex-wrap: wrap; margin-bottom: 0.7rem;
}
.db-panel-title { font-size: 0.95rem; font-weight: 700; color: var(--ink); }
.db-panel-header #db-status,
.db-panel .db-stats { margin-left: auto; font-size: 0.8rem; color: var(--ink-3); }
.emp-search-bar { display: flex; gap: 0.5rem; }
.emp-search-bar input { flex: 1 1 auto; min-width: 0; }

/* ------------------------------------------------------------ page header -- */
.page-head {
  display: flex; align-items: flex-start; gap: 1rem;
  flex-wrap: wrap; margin-bottom: 1.2rem;
}
.page-head h1 { font-size: 1.5rem; letter-spacing: -0.02em; }
.page-head p { color: var(--ink-3); font-size: 0.9rem; margin-top: 0.2rem; max-width: 62ch; }
.page-head__actions { margin-left: auto; display: flex; gap: 0.5rem; flex-wrap: wrap; }

/* ONE HEADING TREATMENT, whichever convention a page was built with.
   Three grew up here: .page-head with a bare <h1> on the pages written after
   the theme landed, .page-title inside a .topbar on the ones retrofitted into
   it, and a naked <h1> on the oldest. They rendered at three different sizes
   and weights, which is most of why every page looked like a different
   product.
   Rather than rewrite twenty-one files and risk what the payslip rebuild
   already taught, the selectors converge on the same declarations. New pages
   should use .page-head; these two keep the rest looking right meanwhile. */
/* Every page heading, wherever the page happens to keep it.
   An earlier attempt listed the containers — .wrap > h1, .page-wrap > h1 and so
   on — and missed Increment, whose <h1> sits two levels deeper inside a
   .main-card. Enumerating each page's nesting is a game you lose one page at a
   time, so the rule stops caring: a top-level heading inside the console looks
   like a top-level heading. .page-head h1 above already matches this and says
   the same thing. */
.main-content h1,
.page-title {
  font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em;
  color: var(--ink);
  /* `margin-top: 0` with !important, because several of these pages put a
     Bootstrap `mt-2` on their heading — and a utility class is !important, so
     without this the first heading sits 8px lower on those pages than on the
     rest. Two pages differing by 8px at the top of the screen is the sort of
     thing nobody can name and everybody notices. */
  margin: 0 0 1.1rem !important;
}
/* Whatever the page put straight after the heading is its standfirst. */
.main-content h1 + p,
.main-content h1 + div:not([class]),
.page-title + p,
.page-title + div:not([class]) {
  color: var(--ink-3); font-size: 0.9rem; margin: -0.85rem 0 1.2rem;
  max-width: 62ch;
}
/* Inside .page-head the flex parent owns the spacing, so the heading gives it
   back. */
.page-head h1 { margin: 0 !important; }
.page-head h1 + p { margin: 0.2rem 0 0; }

/* ---------------------------------------------------------------- panels -- */
/* The box almost every page puts its content in.
   This lived only in app-legacy.css, which the pages written after the theme
   deliberately do not load — so on those pages .panel had no background, no
   border and no margin, and two panels' contents simply overlapped. It is a
   core primitive, not a transitional shim, so it belongs here. app-legacy keeps
   the .card-box / .box aliases for the older markup. */
.panel {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 1.1rem 1.2rem; margin-bottom: 1rem;
}
.panel:last-child { margin-bottom: 0; }
.panel > h2:first-child, .panel > h3:first-child { margin-top: 0; }

/* ------------------------------------------------------------- tool cards -- */
/* The dashboard. A card here is a door to a tool, so it gets the hover state
   and the arrow; nothing else in the console does. */
.tool {
  display: flex; flex-direction: column; gap: 0.45rem;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 1.05rem 1.15rem;
  text-decoration: none; color: inherit;
  border-top: 3px solid var(--accent, var(--teal));
  transition: box-shadow 0.15s, transform 0.15s;
}
.tool:hover { box-shadow: 0 6px 18px -12px rgba(19,26,28,0.35); transform: translateY(-1px); }
.tool__icon {
  width: 34px; height: 34px; border-radius: 8px;
  display: grid; place-items: center; font-size: 1.05rem;
  background: var(--teal-pale); color: var(--teal-dark);
}
.tool h2 { font-size: 1rem; display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.tool p { font-size: 0.85rem; color: var(--ink-3); flex: 1; }
.tool__go { font-size: 0.78rem; font-weight: 600; color: var(--teal-dark); }

/* ------------------------------------------------------------- severities -- */
/* Data Health. Semantic, and deliberately not the brand teal: "critical" must
   never be able to read as "emphasis". */
.sev-critical { --sev: var(--err);  --sev-bg: #FBE9E7; }
.sev-warning  { --sev: var(--warn); --sev-bg: #FBF1DF; }
.sev-info     { --sev: var(--ink-3); --sev-bg: #EDEFF0; }

.finding {
  background: var(--card); border: 1px solid var(--line);
  border-left: 3px solid var(--sev, var(--line));
  border-radius: var(--radius); margin-bottom: 0.8rem; overflow: hidden;
}
.finding__head {
  display: flex; align-items: flex-start; gap: 0.8rem;
  padding: 0.95rem 1.1rem; cursor: pointer; width: 100%;
  background: none; border: 0; font: inherit; text-align: left; color: inherit;
}
.finding__head:hover { background: #FBFCFC; }
.finding__head h2 { font-size: 1rem; }
.finding__why { color: var(--ink-2); font-size: 0.88rem; margin-top: 0.35rem; max-width: 74ch; }
.finding__fix { font-size: 0.84rem; margin-top: 0.45rem; color: var(--ink); }
.finding__fix i { color: var(--ok); }
.finding__count {
  margin-left: auto; background: var(--sev-bg, #EDEFF0); color: var(--sev, var(--ink-3));
  font-size: 0.78rem; font-weight: 700; border-radius: 100px;
  padding: 0.1rem 0.6rem; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.finding__rows { border-top: 1px solid var(--line); }
.finding__row {
  display: flex; align-items: center; gap: 0.7rem;
  padding: 0.5rem 1.1rem; border-bottom: 1px solid var(--line); font-size: 0.87rem;
}
.finding__row:last-child { border-bottom: 0; }
.finding__row .note { margin-left: auto; color: var(--ink-3); font-size: 0.82rem; text-align: right; }

/* ---------------------------------------------------------------- empty -- */
.empty { background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 3rem 1.5rem; text-align: center; }
.empty i { font-size: 2rem; color: var(--ok); }
.empty h2 { font-size: 1.05rem; margin-top: 0.7rem; }
.empty p { color: var(--ink-3); font-size: 0.87rem; margin-top: 0.2rem; }

/* --------------------------------------------------------- approval queue -- */
/* Accounts LDAP created that nobody has approved. Warm, because it is a task
   somebody has to do rather than a fault. */
.queue {
  background: #FBF1DF; border: 1px solid #EBD8AE;
  border-radius: var(--radius); padding: 1rem 1.1rem; margin-bottom: 1.2rem;
}
.queue__head { display: flex; gap: 0.6rem; align-items: flex-start; }
.queue__head i { color: var(--warn); font-size: 1.1rem; }
.queue__head strong { display: block; }
.queue__head span { font-size: 0.85rem; color: #7A5410; }
.queue table { margin-top: 0.8rem; background: #fff; border-radius: 8px; }
.queue th { background: #FDF8EF; }

/* --------------------------------------------------------------- sign in -- */
.login-sub { color: var(--ink-3); font-size: 0.88rem; margin-bottom: 1.3rem; }
.or {
  display: flex; align-items: center; gap: 0.75rem; margin: 1.1rem 0;
  color: var(--ink-3); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.12em;
}
.or::before, .or::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.sso-btn {
  display: flex; align-items: center; justify-content: center; gap: 0.6rem;
  width: 100%; padding: 0.7rem 1rem; border-radius: 8px; border: 1px solid var(--ink);
  background: var(--ink); color: #fff; font: inherit; font-weight: 600;
  text-decoration: none; cursor: pointer;
}
.sso-btn:hover { background: #000; }
/* STACKED fields only, which means the login card — the one place in the
   product where fields sit one under the next with nothing between them.

   It used to be unscoped, as `.field + .field`, and that put 11px of
   margin-top on the SECOND field of every horizontal row of fields in the
   console: .field-row on the two offer generators, the incentive plans
   annexure and open positions, and .calc-pair on the salary calculator. Two
   inputs side by side, the right one sitting eleven pixels lower than the
   left. It reads as sloppiness rather than as a bug, which is why it survived
   this long — you notice it and cannot say what is wrong. */
.login-card .field + .field { margin-top: 0.7rem; }

/* --------------------------------------------------------------- utility -- */
.stack { display: grid; gap: 1rem; }
/* `.right` means two things, because it was written for one and used for the
   other.

   It arrived as a FLEX utility — push me to the end of the row — and that is
   what `margin-left: auto` does. It is then used 68 times across fourteen
   pages, and most of those are <th> and <td> in a figures table, where
   margin-left does nothing whatsoever. So every number column in Payroll,
   Off Payroll, Expense Claims and Reconcile was left-aligned under a heading
   that had asked to be right-aligned, and the digits did not line up.

   Renaming it in fourteen files is the tidier answer and the worse one: the
   flex use is correct and there is no reason a page author should have to
   know which of the two they are in. So the class does the right thing in
   both places. app.css's `td.num` stays as it is; this is the one people
   actually reach for. */
.right { margin-left: auto; }
th.right, td.right { text-align: right; margin-left: 0; }
.nowrap { white-space: nowrap; }
.tnum { font-variant-numeric: tabular-nums; }
.stamp { color: var(--ink-3); font-size: 0.78rem; margin-top: 1.2rem; }

/* ====================================================================== *
   Sidebar scrolling.

   THIS BELONGS UPSTREAM in glisco-ops/public/app.css. It is here so that
   app.css stays byte-identical to ops' copy and `diff` remains the drift
   check — but ops has the same bug waiting for it, and it should go across
   when HR merges. It is a fix, not an HR preference.

   The bug: .sidebar is `position: fixed` with `min-height: 100vh`, a column
   flex container, and no overflow rule. Ops has fifteen nav items and just
   fits on a laptop; HR has sixteen plus four section headings, so the
   sidebar grows past the viewport, and because it is fixed there is nothing
   to scroll — the user panel, Sign out and Collapse are simply unreachable.

   Three things make it work, and all three are load-bearing:

     height, not min-height   a flex container has to be bounded before its
                              children can be told to scroll inside it
     min-height: 0 on .nav    without it a flex item refuses to shrink below
                              its content, so overflow-y never engages. This
                              is the one everybody misses
     flex: 0 0 auto           on brand, foot and rail, so only the nav gives

   100dvh rather than 100vh because on mobile browsers 100vh includes the
   chrome that is about to slide away, which puts the sign-out button just
   off-screen.
 * ====================================================================== */
.sidebar {
  height: 100dvh;
  overflow: hidden;
}
.sidebar-brand,
.sidebar-foot,
.rail-btn { flex: 0 0 auto; }

.sidebar .nav {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* Vertical only. A long menu label pushes the list wider than the rail and
     puts a horizontal scrollbar along the bottom of the sidebar — which reads
     as a broken layout, not as a hint that a word is cut off. */
  overflow-x: hidden;
  overscroll-behavior: contain;
}

/* And the labels themselves stop rather than push. */
.sidebar .nav-link { min-width: 0; }
.sidebar .nav-link span {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* margin-top:auto pushed the footer down when nothing scrolled. Now the nav
   takes the slack, and leaving it in fights the flex sizing. */
.sidebar-foot { margin-top: 0; }

/* A visible scrollbar on a dark sidebar looks like a seam, and an invisible
   one hides that there is more below. Thin and dim: present when you look. */
.sidebar .nav { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.22) transparent; }
.sidebar .nav::-webkit-scrollbar { width: 6px; }
.sidebar .nav::-webkit-scrollbar-track { background: transparent; }
.sidebar .nav::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,.18); border-radius: 3px;
}
.sidebar .nav:hover::-webkit-scrollbar-thumb { background: rgba(255,255,255,.3); }

/* ====================================================================== *
   Collapsible nav sections.

   ALSO BELONGS UPSTREAM in ops' app.css — an admin there will carry every
   section of both consoles in one rail, which is the case this is for.

   The first section never gets a header and never folds: it is the everyday
   work, and a console whose main list can be hidden is a console people
   lose. Sections default to OPEN, because collapsed-by-default hides from a
   new user that the rest of the tool exists.
 * ====================================================================== */
.nav-group { display: block; }

.sidebar-heading--btn {
  display: flex; align-items: center; gap: 0.4rem; width: 100%;
  background: none; border: 0; font: inherit; cursor: pointer;
  /* Matches .sidebar-heading exactly, so the button is invisible as a control
     until you hover it — the words have to sit where they always did. */
  padding: 1.1rem 1.25rem 0.35rem; font-size: 0.62rem; font-weight: 700;
  letter-spacing: 0.16em; text-transform: uppercase; color: rgba(255,255,255,0.32);
  text-align: left;
}
.sidebar-heading--btn:hover { color: rgba(255,255,255,0.6); }
.sidebar-heading--btn span { flex: 1; }
.sidebar-heading--btn i {
  font-size: 0.7rem; transition: transform 0.18s; flex: 0 0 auto;
  transform: rotate(-90deg);
}
.nav-group.open > .sidebar-heading--btn i { transform: rotate(0deg); }
.sidebar-heading--btn:focus-visible {
  outline: 2px solid var(--teal); outline-offset: -2px;
}

.nav-group__items { display: none; }
.nav-group.open > .nav-group__items { display: block; }

/* Collapsed to a rail there are no words to fold, so every section shows its
   icons and the headers disappear with them — otherwise a fold would hide
   icons behind a chevron the rail is too narrow to label. */
body.rail-collapsed .sidebar-heading--btn { display: none; }
body.rail-collapsed .nav-group__items { display: block; }

@media (max-width: 991.98px) {
  body.rail-collapsed .sidebar-heading--btn { display: flex; }
  body.rail-collapsed .nav-group:not(.open) > .nav-group__items { display: none; }
}

/* ====================================================================== *
   Screenshot watermark.

   ALSO FOR UPSTREAM in ops' app.css — same console, same exposure, and the
   ops side shows customer addresses.

   The viewer's name and today's date, tiled diagonally at 20% black, so a
   photograph of a screen or a forwarded screenshot carries who was looking
   and when. A leak with a name on it is a conversation; an anonymous one is
   an investigation.

   Four properties are load-bearing:

     pointer-events: none   it must not swallow a click. Without this the
                            whole console becomes unusable
     user-select: none      so dragging to select text does not select forty
                            copies of somebody's name
     position: fixed        it stays over the viewport as the page scrolls,
                            rather than only over the first screenful
     z-index                above content, BELOW modals and the sidebar. A
                            watermark that lands on top of a dialog obscures
                            the thing somebody is trying to read, and an
                            obscured dialog is how a feature gets switched off

   Hidden in print on purpose: the payslip, ID card, offer letter and exit
   paper pages produce documents that are GIVEN to people, and an ID card
   with an HR user's name across it is broken output.
 * ====================================================================== */
.watermark {
  position: fixed;
  inset: 0;
  z-index: 1020;                 /* sidebar is 1040, mobilebar 1030, modals above */
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
  background-repeat: repeat;
  background-position: 0 0;
}

@media print {
  .watermark { display: none !important; }
}

/* Where somebody has asked for a quieter console — a projector, a screen share
   with a client — the mark can be dimmed but not removed. Halving it keeps it
   legible in a photograph while taking it out of the way on a wall. */
body.watermark-quiet .watermark { opacity: 0.5; }

/* ====================================================================== *
   Employee search.

   The pattern the increment and transfer letters already use: type a name or
   a code, pick from a short list, and the record loads. Lifted out of those
   pages' own stylesheets so Advances uses the same component rather than a
   second one that behaves almost the same.

   Search by NAME as well as code, deliberately. Nobody in HR knows that
   Divakar Sharma is 30151; they know his name.
 * ====================================================================== */
.emp-search { position: relative; }
.emp-search input { padding-left: 2.1rem; }
.emp-search__icon {
  position: absolute; left: 0.7rem; top: 50%; transform: translateY(-50%);
  color: var(--ink-3); pointer-events: none;
}
/* Whatever holds the dropdown becomes its anchor.
   .emp-results is absolutely positioned, so without this it measures itself
   against the nearest positioned ancestor — and on the Increment page there was
   none, so the results laid out 2015px wide and landed off the right of the
   screen. The search worked perfectly; it just drew itself somewhere nobody
   could see, which reads exactly like a broken search.
   Written with :has() so any container that holds a result list anchors it,
   rather than each page having to remember to. */
:has(> .emp-results) { position: relative; }
.emp-results {
  position: absolute; z-index: 30; left: 0; right: 0; top: calc(100% + 4px);
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: 0 12px 28px -18px rgba(19,26,28,.4);
  max-height: 17rem; overflow-y: auto;
}
.emp-result,
.emp-result-row {
  display: flex; align-items: baseline; gap: 0.7rem;
  padding: 0.55rem 0.75rem; cursor: pointer; border-bottom: 1px solid var(--line);
  width: 100%; background: none; border-left: 0; border-right: 0; border-top: 0;
  font: inherit; text-align: left; color: inherit;
}
.emp-result:last-child, .emp-result-row:last-child { border-bottom: 0; }
.emp-result:hover, .emp-result:focus-visible,
.emp-result-row:hover, .emp-result-row:focus-visible { background: var(--teal-pale); }
.emp-result strong, .emp-result-row strong { color: var(--ink); }
.emp-result .code, .emp-result-row .code { font-family: var(--mono); font-size: 0.78rem; color: var(--ink-3); }
.emp-result .meta, .emp-result-row .meta { margin-left: auto; font-size: 0.8rem; color: var(--ink-3); text-align: right; }
.emp-results-empty { padding: 0.7rem 0.75rem; color: var(--ink-3); font-size: 0.88rem; }

/* The loaded record. Deliberately shows salary and joining date: an advance is
   a credit decision and those are the two things somebody assessing it asks
   for. */
.emp-loaded {
  display: grid; gap: 0.75rem 1.4rem;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  background: var(--bg); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 0.9rem 1rem; margin-top: 0.8rem;
}
.emp-loaded dt {
  font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-3); font-weight: 600;
}
.emp-loaded dd { margin: 0.15rem 0 0; font-weight: 600; color: var(--ink); }
.emp-loaded dd.warn { color: var(--warn); }

/* ====================================================================== *
   Fixes found by opening the pages.
 * ====================================================================== */

/* Buttons were breaking onto two lines — "Add / Employee", "Add / New",
   "Export / CSV". A button is a single target and a two-line one reads as a
   layout fault. It stops shrinking instead. */
.btn, .btn-primary, .btn-secondary, .btn-success, .btn-danger, .btn-warning,
.btn-light, .btn-dark, .btn-outline-primary, .btn-outline-secondary,
.btn-action, .btn-add, .btn-cancel, .emp-load-btn {
  white-space: nowrap;
  flex: 0 0 auto;
}

/* A toolbar sits on one line and wraps as whole controls, not mid-word. */
/* .toolbar's display:flex came from app-legacy.css, which the themed pages do
   not load — so on those pages a toolbar's controls stacked vertically. Same
   mistake as .panel: a layout primitive parked in the transitional shim. */
.toolbar {
  display: flex; align-items: flex-start; gap: 0.6rem; flex-wrap: wrap;
  margin-bottom: 0.9rem;
}
.toolbar, .btn-row, .page-head__actions { align-items: center; }
.toolbar > *, .btn-row > * { flex: 0 0 auto; }
/* …except the search field, which takes the slack. */
.toolbar input[type=text], .toolbar input[type=search] { flex: 1 1 14rem; min-width: 0; }
/* A select in a toolbar sizes to its longest option plus room for the arrow.
   Without this it inherits width:100% from the form rules, which in a flex row
   with flex:0 0 auto collapses it to something narrower than its own text —
   Master Data's "Sort A–Z" had the chevron sitting on top of the Z. */
.toolbar select, .page-head__actions select, .btn-row select {
  width: auto; min-width: 9rem; padding-right: 2rem;
}

/* Running prose stays readable. A max-width on the paragraph alone was being
   ignored where a flex parent had already stretched its child. */
.page-head > div:first-child { min-width: 0; max-width: 72ch; }

/* An empty result area with nothing in it reads as a page that failed to load
   rather than one waiting for input. */
#results:empty::after {
  content: "Search by name or employee code to open a record.";
  display: block; padding: 3rem 1rem; text-align: center;
  color: var(--ink-3); font-size: 0.9rem;
}

/* ====================================================================== *
   Utilities the retheme took away.

   Each page used to define its own handful of one-line utilities at the
   bottom of its own stylesheet, and deleting those stylesheets took them
   with it. `.hidden` is the one that mattered: the status matrix and the
   payslip page hide their modals with `class="hidden"`, so losing it left
   two dialogs rendered open and unstyled on top of the page.

   Found by opening the pages, which is the only way this kind of thing is
   ever found. It is exactly the failure a class census misses — `hidden` was
   on my list of framework names to skip.
 * ====================================================================== */
.hidden { display: none !important; }

/* The bespoke modal these two pages use — not Bootstrap's. Styled to match
   .modal-content so a page can carry either while it waits to be converted. */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 1050;
  background: rgba(19,26,28,.55);
  display: grid; place-items: center; padding: 1.5rem;
  opacity: 1;                        /* Bootstrap sets .modal-backdrop to 0.5 */
}
.modal-card {
  background: var(--card); border-radius: 14px; border: 1px solid var(--line);
  width: min(560px, 100%); max-height: 90vh; overflow-y: auto;
  box-shadow: 0 24px 60px -30px rgba(19,26,28,.5);
}
.modal-head {
  padding: 1rem 1.2rem; border-bottom: 1px solid var(--line);
  font-weight: 700; font-size: 1.05rem;
}
.modal-foot {
  padding: 0.9rem 1.2rem; border-top: 1px solid var(--line);
  display: flex; gap: 0.5rem; justify-content: flex-end; flex-wrap: wrap;
}

/* ====================================================================== *
   Bare <button> elements.

   app.css styles `.btn`. The pages' own stylesheets styled the ELEMENT —
   `button { ... }` — so deleting them left every unclassed button rendering
   as a raw grey browser control. That is 57 buttons across eleven pages,
   including "Approve & Update Employee" on the increment letter.

   `:not([class])` is what makes this safe: a button that already carries a
   class keeps whatever that class says, so nothing here can fight .btn or
   the Bootstrap variants above.
 * ====================================================================== */
button:not([class]),
input[type="submit"]:not([class]),
input[type="button"]:not([class]) {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.5rem 0.95rem; border-radius: 8px;
  border: 1px solid var(--line); background: #fff; color: var(--ink-2);
  font: inherit; font-size: 0.88rem; font-weight: 600;
  cursor: pointer; white-space: nowrap;
}
button:not([class]):hover,
input[type="submit"]:not([class]):hover { background: #F1F3F3; color: var(--ink); }

button:not([class]):disabled { opacity: 0.5; cursor: not-allowed; }

/* A bare button that submits or confirms still reads as the primary action,
   which the pages used to express through their own colours. Named by intent
   so the markup can say which is which. */
button:not([class])[data-primary],
button.primary {
  background: var(--teal); border-color: var(--teal); color: #fff;
}
button:not([class])[data-primary]:hover, button.primary:hover {
  background: var(--teal-dark); border-color: var(--teal-dark); color: #fff;
}

/* Bare form controls, for the same reason. */
input:not([class]):not([type="checkbox"]):not([type="radio"]),
select:not([class]),
textarea:not([class]) {
  width: 100%; padding: 0.55rem 0.65rem; font: inherit; font-size: 0.92rem;
  border: 1px solid var(--line); border-radius: 8px;
  background: #fff; color: var(--ink);
}

/* ====================================================================== *
   Bespoke button classes the pages invented.

   Every page named its buttons differently — .primary-btn, .green-btn,
   .rep-btn, .load-btn, .pdf-btn — and defined each in its own stylesheet.
   Deleting those left "Approve & Update Employee" rendering as a raw browser
   control on the increment letter.

   THE MAPPING USED TO BE HERE AND ALSO 250 LINES BELOW.

   Two blocks, overlapping selectors, different opinions: this one made
   .green-btn a filled green and the other made it an unfilled teal; this one
   put .pdf-btn in quiet and so did that one, in slightly different greys. The
   lower block won on every shared name because it comes later, which made
   everything here either dead or — worse — half-live, applying to the names
   the other block happened not to list.

   Two copies of a mapping is the same fault as two copies of a palette, and
   it is the one that produced "the look and feel is all over the page": you
   cannot fix a button by reading one of them.

   So there is now ONE block, under "BESPOKE BUTTONS, MAPPED" further down.
   Everything button-shaped goes there. What stays here is the handful of
   things in this neighbourhood that are not buttons.
 * ====================================================================== */

/* A close glyph on the bespoke popups. */
.popup-close, .calc-card-close {
  background: none; border: 0; cursor: pointer; line-height: 1;
  font-size: 1.35rem; color: var(--ink-3); padding: 0 0.3rem;
}
.popup-close:hover, .calc-card-close:hover { color: var(--ink); }

/* A small labelled token — used as a filter and as a tag. */
.chip {
  display: inline-flex; align-items: center; gap: 0.3rem;
  padding: 0.2rem 0.6rem; border-radius: 100px;
  background: var(--teal-pale); color: var(--teal-dark);
  font-size: 0.78rem; font-weight: 600; border: 1px solid transparent;
  cursor: pointer; text-decoration: none;
}
.chip:hover { border-color: var(--teal); }
.chip.active, .chip[aria-pressed="true"] { background: var(--teal); color: #fff; }

.slot-code-input {
  padding: 0.35rem 0.5rem; border: 1px solid var(--line); border-radius: 6px;
  font: inherit; font-size: 0.85rem; width: 100%;
}
.hiring-link { color: var(--teal-dark); font-weight: 600; }

/* The bespoke popup shell these pages use, alongside .modal-card above.
   The head/body/foot parts were styled but the overlay and the panel itself
   never were, so a dialog appended to <body> rendered as a block at the very
   bottom of the document — present, focusable, and completely invisible unless
   you scrolled past the footer to find it. */
.popup-overlay {
  position: fixed; inset: 0; z-index: 1080;
  background: rgba(19, 26, 28, 0.45);
  display: flex; align-items: center; justify-content: center;
  padding: 1.5rem; overflow-y: auto;
}
.popup {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); width: 100%; max-width: 30rem;
  box-shadow: 0 18px 48px rgba(19, 26, 28, 0.22);
  max-height: 100%; display: flex; flex-direction: column;
}
.popup .popup-body { overflow-y: auto; }
/* A wider dialog, for one that holds a table rather than a form.
   This lived in public/admin/claims/index.html, so `.popup--wide` worked on
   that page and silently did nothing everywhere else — the same class name
   meaning two different things depending which page you were on. A dialog
   that holds a table and is 30rem wide is not obviously broken; it just
   scrolls sideways for no reason anybody can see. */
.popup--wide .popup { max-width: 46rem; }
.popup-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.7rem; padding: 1rem 1.2rem; border-bottom: 1px solid var(--line);
}
.popup-head h3 { font-size: 1.05rem; margin: 0; }
.popup-body { padding: 1.1rem 1.2rem; }
.popup-foot {
  padding: 0.9rem 1.2rem; border-top: 1px solid var(--line);
  display: flex; gap: 0.5rem; justify-content: flex-end;
}

/* =========================================================================
   THE EMPLOYEE RECORD
   One person, opened from search or straight from a Data Health link. HR works
   this screen next to the person whose record it is, so it reads top to bottom:
   who they are, what we hold about them, what has been changed, what was
   reviewed.

   These classes had styling inside the page until the page's own CSS block was
   removed in the Bootstrap migration, and nothing replaced it — the label/value
   pairs collapsed into one unstyled column and the record lost its surface.
   They live here rather than in app-legacy.css because this screen replaces
   ops' Team page and travels with it.
   ========================================================================= */
.employee-card {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 1.3rem 1.4rem; margin-bottom: 1rem;
}
.employee-card > div:first-child { gap: 1rem; flex-wrap: wrap; }
.employee-card h2 {
  font-size: 1.35rem; font-weight: 700; letter-spacing: -0.02em;
  color: var(--ink); margin: 0; display: flex; align-items: center;
  gap: 0.5rem; flex-wrap: wrap;
}

/* A definition list, not a paragraph. auto-fit means the same markup gives
   four columns on a desktop and one on a phone with no breakpoint to maintain. */
.employee-grid {
  display: grid; gap: 1rem 1.6rem;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  padding-top: 1.2rem; margin-top: 0.2rem; border-top: 1px solid var(--line);
}
.employee-grid .label {
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-3); margin-bottom: 0.2rem;
}
.employee-grid .value {
  font-size: 0.97rem; font-weight: 500; color: var(--ink);
  font-variant-numeric: tabular-nums; word-break: break-word;
}
/* A field we hold nothing for says so. Before this, an empty value rendered as
   nothing at all and left its label hanging over the next field's answer —
   which is how you read a blank cuisine as a blank salary. */
.employee-grid .value:empty::after,
.value--empty {
  content: "Not recorded"; color: var(--ink-3);
  font-weight: 400; font-style: italic; font-size: 0.9rem;
}
.value--empty { font-style: italic; }

/* --------------------------------------------------------- the action row -- */
/* Five buttons in five colours was five things all shouting. Hierarchy instead:
   the routine action is filled, everything else is quiet until you reach for
   it, and only the two that end someone's record carry a warning colour. */
.emp-actions { display: flex; gap: 0.45rem; flex-wrap: wrap; }
/* ONE BUTTON SIZE.
 *
 * .emp-load-btn was 0.55rem/0.85rem padding at 0.85rem type while .btn was
 * 0.5rem/0.95rem at 0.88rem. Two pixels of height and half a point of type —
 * invisible alone, obvious the moment the two sit in one toolbar, which they
 * do on Payroll Runs, Off Payroll, Reconcile, Expense Claims, Register
 * Clean-up and API Access. A row of buttons at two heights reads as a layout
 * fault rather than a design.
 *
 * The families now share one geometry and differ only in colour:
 *   .btn           teal — the action being encouraged
 *   .btn--quiet    white, grey border — everything else
 *   .btn--danger   white, red border — a red slab invites the click it should
 *                  be discouraging
 *   .emp-load-btn  the same as .btn--quiet, under its older name
 *
 * New markup uses .btn and its modifiers. The rest are aliases, kept so that
 * thirty pages did not have to be rewritten at once. */
.emp-load-btn {
  font: inherit; font-size: 0.88rem; font-weight: 600; line-height: 1;
  padding: 0.5rem 0.95rem; border-radius: 8px; cursor: pointer;
  border: 1px solid var(--line); background: var(--card); color: var(--ink-2);
  display: inline-flex; align-items: center; gap: 0.4rem; white-space: nowrap;
  text-decoration: none;
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.emp-load-btn:hover { background: var(--bg); border-color: var(--ink-3); color: var(--ink); }
.emp-load-btn:focus-visible {
  outline: 2px solid var(--teal); outline-offset: 2px;
}
.emp-load-btn--primary {
  background: var(--teal); border-color: var(--teal); color: #fff;
}
.emp-load-btn--primary:hover {
  background: var(--teal-dark); border-color: var(--teal-dark); color: #fff;
}
/* Reaching for these is deliberate: quiet until hover, then unmistakable. */
.emp-load-btn--warn { color: var(--warn); border-color: #E8DCC3; }
.emp-load-btn--warn:hover { background: #FDF6E7; border-color: var(--warn); color: #8A5D12; }
.emp-load-btn--danger { color: var(--err); border-color: #EFD3D0; }
.emp-load-btn--danger:hover { background: #FCEFEE; border-color: var(--err); color: #8F281F; }

/* ------------------------------------------------------------- the status -- */
.status-pill {
  display: inline-flex; align-items: center; gap: 0.3rem;
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; padding: 0.24rem 0.55rem; border-radius: 999px;
  background: var(--teal-pale); color: var(--teal-dark);
  border: 1px solid #CDE9EF; vertical-align: middle;
}
.status-date { font-weight: 500; letter-spacing: 0; text-transform: none; opacity: 0.85; }

/* ----------------------------------------------------- history and reviews -- */
.history-box {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 1.1rem 1.2rem; margin-bottom: 1rem;
}
.history-title {
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-3); margin: 0 0 0.8rem;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
}
.history-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; }
.history-table th {
  text-align: left; font-size: 0.66rem; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-3);
  padding: 0 0.7rem 0.5rem; border-bottom: 1px solid var(--line);
}
.history-table td {
  padding: 0.6rem 0.7rem; border-bottom: 1px solid var(--line);
  color: var(--ink-2); vertical-align: top;
}
.history-table tr:last-child td { border-bottom: 0; }
.history-when { white-space: nowrap; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.history-by { white-space: nowrap; color: var(--ink-3); }
.history-changes { color: var(--ink); }
.history-changes strong { font-weight: 600; }
.history-empty, .notes-text, .muted { color: var(--ink-3); font-size: 0.9rem; }
.history-empty { padding: 0.3rem 0; }

/* ------------------------------------------------------------ search rows -- */
.emp-result-wrap { overflow-x: auto; }
.emp-result-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.emp-result-table th {
  text-align: left; font-size: 0.66rem; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-3);
  padding: 0 0.7rem 0.5rem; border-bottom: 1px solid var(--line);
}
.emp-result-table td {
  padding: 0.65rem 0.7rem; border-bottom: 1px solid var(--line); color: var(--ink-2);
}
.emp-name { font-weight: 600; color: var(--ink); }
.emp-id { font-variant-numeric: tabular-nums; color: var(--ink-3); }
.emp-loc { color: var(--ink-3); }

.empty-state {
  text-align: center; padding: 2.6rem 1rem; color: var(--ink-3);
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
}
.empty-state i { font-size: 1.9rem; display: block; margin-bottom: 0.6rem; opacity: 0.5; }


/* =========================================================================
   BESPOKE BUTTONS, MAPPED
   Every page that predates the theme brought its own button class and, in
   several cases, an inline background colour. The result was one console with
   four greens in it. These map onto the same two shapes everything else uses:
   filled for the action a page exists to perform, quiet for the rest.
   Inline `style="background:#..."` beats a class, so those are removed from the
   markup rather than fought here.
   ========================================================================= */
/* ONE GEOMETRY.
   The shape below is app.css's .btn, to the character: 0.5rem 0.95rem,
   0.88rem, 8px, 600. It used to be 0.6rem/1rem here, which meant a page
   carrying an .add-btn beside a .btn had two buttons of visibly different
   height in the same toolbar — the same fault .emp-load-btn had. There is one
   button geometry in this product and this is it.

   .btn itself is deliberately NOT in this list. It is already this shape in
   app.css, and naming it here would put a teal fill after .btn--quiet and
   .btn--ghost in the cascade — repainting every quiet button in the console
   solid. */
.primary-btn, .btn-save, .add-btn, .btn-add, .load-btn, .rep-btn,
.btn-primary, .btn-primary-modal, .btn-act, .btn-pine, .btn-success,
.emp-search-btn, .action-btn, .btn-mini,
.btn-cancel, .preview-btn, .pdf-btn, .btn-secondary, .btn-quiet,
.btn-light, .btn-outline-secondary, .btn-help,
.btn-warning, .btn-info, .btn-dark,
.btn-outline, .btn-outline-primary, .btn-line, .btn-ghost, .green-btn,
.btn-danger, .btn-danger-modal, .red-btn, .btn-remove,
.btn-outline-danger, .slot-remove-btn, .danger {
  display: inline-flex; align-items: center; justify-content: center;
  gap: 0.4rem;
  font: inherit; font-size: 0.88rem; font-weight: 600; line-height: 1.2;
  padding: 0.5rem 0.95rem; border-radius: 8px; cursor: pointer;
  border: 1px solid var(--teal); background: var(--teal); color: #fff;
  text-decoration: none; white-space: nowrap;
}

/* FILLED — the one thing a page exists to do. */
.primary-btn:hover, .btn-save:hover, .add-btn:hover, .btn-add:hover,
.load-btn:hover, .rep-btn:hover, .btn-primary:hover,
.btn-primary-modal:hover, .btn-act:hover, .btn-pine:hover,
.btn-success:hover, .emp-search-btn:hover,
.action-btn:hover, .btn-mini:hover {
  background: var(--teal-dark); border-color: var(--teal-dark);
}

/* GHOST — the accent, unfilled. A second action of the same kind. */
.btn-outline, .btn-outline-primary, .btn-line, .btn-ghost, .green-btn {
  background: var(--card); color: var(--teal-dark); border-color: var(--teal);
}
.btn-outline:hover, .btn-outline-primary:hover, .btn-line:hover,
.btn-ghost:hover, .green-btn:hover {
  background: var(--teal-pale);
}

/* QUIET — everything else. Cancel, reset, download, open, go back. */
.btn-cancel, .preview-btn, .pdf-btn, .btn-secondary, .btn-quiet,
.btn-light, .btn-outline-secondary, .btn-help,
.btn-warning, .btn-info, .btn-dark {
  border-color: var(--line); background: var(--card); color: var(--ink-2);
}
.btn-cancel:hover, .preview-btn:hover, .pdf-btn:hover,
.btn-secondary:hover, .btn-quiet:hover, .btn-light:hover,
.btn-outline-secondary:hover, .btn-help:hover,
.btn-warning:hover, .btn-info:hover, .btn-dark:hover {
  background: var(--bg); border-color: var(--ink-3); color: var(--ink);
}

/* DANGER — never filled. A red rectangle is easy to press by accident, and
   every one of these removes something. */
.btn-danger, .btn-danger-modal, .red-btn, .btn-remove,
.btn-outline-danger, .slot-remove-btn, .danger {
  border-color: #EFD3D0; color: var(--err); background: var(--card);
}
.btn-danger:hover, .btn-danger-modal:hover, .red-btn:hover,
.btn-remove:hover, .btn-outline-danger:hover, .slot-remove-btn:hover,
.danger:hover {
  background: #FCEFEE; border-color: var(--err);
}

/* SIZE. Bootstrap's .btn-sm is loaded before this file, so the geometry above
   was silently making every "small" button full-size — a 9-page problem that
   only shows as a toolbar sitting a few pixels taller than it should. These
   come last so a size modifier still modifies. */
.btn-sm, .btn--sm, .btn-mini {
  padding: 0.28rem 0.6rem; font-size: 0.78rem;
}
.btn-lg {
  padding: 0.65rem 1.2rem; font-size: 0.95rem;
}

/* Disabled says so once, for all of them. Three pages had their own opacity
   and one of them kept the hover lift while disabled. */
.btn:disabled, .btn[disabled],
.primary-btn:disabled, .btn-save:disabled, .add-btn:disabled,
.btn-add:disabled, .btn-primary:disabled, .btn-act:disabled,
.btn-outline:disabled, .btn-ghost:disabled, .btn-line:disabled,
.btn-cancel:disabled, .pdf-btn:disabled, .btn-secondary:disabled {
  opacity: 0.45; cursor: not-allowed; transform: none;
}

.btn:focus-visible,
.primary-btn:focus-visible, .btn-cancel:focus-visible,
.btn-save:focus-visible, .add-btn:focus-visible, .btn-add:focus-visible,
.load-btn:focus-visible, .preview-btn:focus-visible, .pdf-btn:focus-visible,
.btn-remove:focus-visible, .btn-primary:focus-visible,
.btn-outline:focus-visible, .btn-ghost:focus-visible,
.btn-line:focus-visible, .btn-act:focus-visible, .btn-pine:focus-visible,
.btn-secondary:focus-visible, .btn-quiet:focus-visible,
.btn-danger:focus-visible, .red-btn:focus-visible {
  outline: 2px solid var(--teal); outline-offset: 2px;
}

/* =========================================================================
   TIMESHEETS
   A register of who was seen and when. Deliberately NOT a report of hours
   worked: this terminal reports every punch as the same event type, so there
   is no in/out flag, and a bar drawn from first to last punch is a span, not a
   shift. The design has to make that distinction impossible to misread —
   which is why a single punch is a MARK rather than a zero-width bar, and why
   the span column says "—" rather than "0h 00m".

   One scale for the whole page (06:00–24:00), so two rows can be compared.
   One hue for the bars; unmatched rows carry a status colour, not a second
   series colour, because "we don't know who this is" is a state and not a
   category.
   ========================================================================= */
.ts-note {
  display: flex; gap: 0.7rem; align-items: flex-start;
  background: var(--teal-pale); border: 1px solid #CDE9EF;
  border-radius: var(--radius); padding: 0.85rem 1rem; margin-bottom: 1rem;
  color: var(--ink-2); font-size: 0.88rem;
}
.ts-note i { color: var(--teal-dark); font-size: 1rem; margin-top: 0.1rem; flex: 0 0 auto; }
.ts-note p { margin: 0 0 0.35rem; max-width: 88ch; }
.ts-note p:last-child { margin-bottom: 0; }

/* -------------------------------------------------- FOLDED PAGE NOTES --
   These were three and four paragraphs of reasoning at the top of every
   page. Every word is worth having ONCE — why a missing punch is not
   evidence of absence, why the weekly summary arrives even when it is empty
   — and none of it is worth reading on the four hundredth visit. Standing
   between somebody and the table they came for, it stops being an
   explanation and becomes furniture.

   So shell.js folds every .ts-note behind one small button on every page,
   and remembers per page whether it was left open. The text is unchanged and
   one click away; it is the default that changed.

   Written as a modifier the script adds rather than as the base style, so a
   page with JavaScript off still shows its note in full — losing the
   explanation entirely is worse than showing it always.            */
.ts-note--fold {
  display: block; padding: 0; background: none; border: 0;
  margin-bottom: 0.9rem;
}
.ts-note__toggle {
  display: inline-flex; align-items: center; gap: 0.4rem;
  background: var(--teal-pale); border: 1px solid #CDE9EF;
  border-radius: 100px; padding: 0.25rem 0.7rem 0.25rem 0.5rem;
  font: inherit; font-size: 0.78rem; font-weight: 600;
  color: var(--teal-dark); cursor: pointer;
}
.ts-note__toggle:hover { border-color: var(--teal); background: #fff; }
.ts-note__toggle i { font-size: 0.95rem; margin: 0; color: inherit; }
.ts-note__toggle .ts-note__chev { font-size: 0.6rem; opacity: 0.8; }

.ts-note__body {
  display: flex; gap: 0.7rem; align-items: flex-start;
  background: var(--teal-pale); border: 1px solid #CDE9EF;
  border-radius: var(--radius); padding: 0.85rem 1rem;
  margin-top: 0.5rem; color: var(--ink-2); font-size: 0.88rem;
}
.ts-note__body > i { color: var(--teal-dark); font-size: 1rem; margin-top: 0.1rem; flex: 0 0 auto; }

/* Printed, it is documentation rather than furniture — so it prints open.
   Pages that carry `no-print` on the note keep that; this only affects the
   fold. */
@media print {
  .ts-note__toggle { display: none !important; }
  .ts-note__body { display: flex !important; }
}

.ts-stats { display: flex; flex-wrap: wrap; gap: 1.6rem 2.4rem; }
.ts-stat { display: flex; flex-direction: column; gap: 0.1rem; }
.ts-stat strong {
  font-size: 1.7rem; font-weight: 700; letter-spacing: -0.02em;
  line-height: 1.1; color: var(--ink); font-variant-numeric: tabular-nums;
}
.ts-stat span {
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-3);
}

.panel--attention { border-left: 3px solid var(--warn); }
/* .ts-head is THE section header, on ten pages: Timesheets, Reconcile, API
   Access, Register Clean-up, Payroll Runs, Off Payroll, Expense Claims,
   Devices, Employees and Blockers. The name is wrong — "ts" is timesheets,
   where it was written — and renaming it means touching ten files, so it is
   noted here rather than done badly. A page with a heading and a line of
   explanation above a panel wants this. */
.ts-head h2 {
  font-size: 1.02rem; font-weight: 700; letter-spacing: -0.01em;
  color: var(--ink); margin: 0 0 0.3rem;
  display: flex; align-items: center; gap: 0.45rem;
}
/* A section-heading icon takes the heading's colour, not a status colour.
   This was amber, which on Timesheets read as "careful, these are spans not
   shifts" — a real point, made once, in prose. Register Clean-up borrows
   .ts-head for its three numbered steps, so it inherited three amber icons on
   a page where nothing is being warned about, and amber stopped meaning
   anything on either page. */
.ts-head h2 i { color: var(--ink-3); }
.ts-head p { margin: 0 0 1rem; max-width: 80ch; font-size: 0.88rem; }

.ts-toggle {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: 0.85rem; color: var(--ink-2); cursor: pointer;
  margin-left: auto; white-space: nowrap;
}

/* ------------------------------------------------------------------ a day -- */
.ts-day { margin-bottom: 1.8rem; }
.ts-day:last-child { margin-bottom: 0; }
.ts-day__head {
  display: flex; align-items: baseline; gap: 0.7rem;
  padding-bottom: 0.45rem; border-bottom: 1px solid var(--line);
  margin-bottom: 0.2rem;
}
.ts-day__head h3 {
  font-size: 0.95rem; font-weight: 700; color: var(--ink); margin: 0;
}

/* The scale, drawn once per day and shared by every bar under it. Recessive:
   it orients the eye and then gets out of the way. */
.ts-scale {
  position: relative; height: 1.15rem;
  margin: 0.35rem 0 0.2rem;
  margin-left: 13rem; margin-right: 11.5rem;
}
.ts-scale span {
  position: absolute; transform: translateX(-50%);
  font-size: 0.66rem; color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

.ts-row {
  display: grid;
  grid-template-columns: 13rem 1fr 6rem 5.5rem;
  align-items: center; gap: 0 0;
  padding: 0.3rem 0; border-bottom: 1px solid var(--line);
}
.ts-row:last-child { border-bottom: 0; }
.ts-who { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; padding-right: 0.8rem; }
.ts-who strong {
  font-size: 0.9rem; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ts-who .emp-id { font-size: 0.74rem; }
.ts-flag {
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--warn);
}

/* A COUNT that needs attention, at the size of the counts beside it.
   .ts-flag above is the style of a WORD — "MISSED PUNCH", uppercase and
   small. Off Payroll was using it for a number in a numeric column, so
   Nirmal Chander's 2 days with one punch rendered two thirds the size of the
   9 in the column to its left: the figure that mattered was the smallest
   thing in the row, and the colour read as decoration rather than as a
   signal. Same colour, same size as its neighbours, and it is a figure
   again. */
.ts-count-warn {
  color: var(--warn); font-weight: 700;
  border-bottom: 1px dotted currentColor; cursor: help;
}

/* Off Payroll's list. A name is the column a person reads DOWN, so it does
   not wrap: "Ramvir Singh Chaudhary" on three lines made every row in the
   table three lines tall, and the table already scrolls sideways inside
   .table-scroll if it needs to. */
.emp-result-table .emp-name { white-space: nowrap; }
/* The line under a name — credential, branch, agency. It wraps rather than
   widening the column, because a nine-column table has no spare width and
   this is the least important thing in the row. */
.emp-result-table .meta { max-width: 13rem; }

/* A second line in a table heading. Used where a label genuinely needs two
   words and the column is narrow; the better answer is usually one word and a
   title attribute, which is what Off Payroll ended up with. */
.th-sub {
  display: block; font-weight: 600; opacity: 0.72;
}

/* The track. A faint ground so an empty stretch still reads as time. */
.ts-track {
  position: relative; height: 1.35rem;
  background: linear-gradient(var(--bg), var(--bg)) no-repeat center/100% 0.5rem;
  border-radius: 4px;
}
.ts-bar {
  position: absolute; top: 50%; transform: translateY(-50%);
  height: 0.5rem; min-width: 3px; border-radius: 4px;
  background: var(--teal);
}
/* One punch is a moment, not a span. Drawn as a mark so it can never be read
   as a shift of zero length. */
.ts-mark {
  position: absolute; top: 50%; transform: translate(-50%, -50%);
  width: 0.55rem; height: 0.55rem; border-radius: 50%;
  background: var(--card); border: 2px solid var(--teal);
}
/* Every individual punch, over the bar — so a day with four punches looks
   different from a day with two spanning the same hours. */
.ts-tick {
  position: absolute; top: 50%; transform: translate(-50%, -50%);
  width: 2px; height: 0.85rem; border-radius: 1px;
  background: var(--ink); opacity: 0.55;
}

.ts-times, .ts-span {
  font-size: 0.82rem; color: var(--ink-2);
  font-variant-numeric: tabular-nums; text-align: right;
  padding-left: 0.7rem; white-space: nowrap;
}
.ts-span { font-weight: 600; color: var(--ink); }

.ts-notes {
  grid-column: 2 / -1; display: flex; flex-direction: column; gap: 0.1rem;
  padding: 0.1rem 0 0.3rem;
}
.ts-notes span { font-size: 0.78rem; color: var(--ink-3); }

/* A row we cannot attribute. The warning colour is on the identity, not the
   bar — the times are just as true, it is the person that is unknown. */
.ts-row--unmatched .ts-bar { background: var(--warn); }
.ts-row--unmatched .ts-mark { border-color: var(--warn); }
.ts-row--before { opacity: 0.55; }

@media (max-width: 900px) {
  .ts-row { grid-template-columns: 10rem 1fr; }
  .ts-times, .ts-span { grid-column: 2; text-align: left; padding-left: 0; }
  .ts-scale { margin-left: 10rem; margin-right: 0; }
  .ts-notes { grid-column: 1 / -1; }
}

/* =========================================================================
   HELP, COLLAPSED
   The Devices page carries the whole runbook — new machine, machine coming
   back, admin PIN, RFID, what to do when it goes quiet. That is a lot of prose
   to sit above the list of machines you actually came to look at, so it lives
   in <details>: open the one you need, and the page stays a device list the
   rest of the time.

   Native <details> deliberately, not a JS accordion. It survives Ctrl-F (the
   browser opens a closed section to reveal a match), it prints expanded, and
   it needs no script to work — which matters for a page somebody reads while
   standing in front of a machine that is not working.
   ========================================================================= */
.help {
  border-top: 1px solid var(--line);
}
.help:first-of-type { border-top: 0; }
.help > summary {
  cursor: pointer; padding: 0.75rem 0.2rem;
  font-size: 0.92rem; color: var(--ink-2);
  display: flex; align-items: center; gap: 0.5rem;
  list-style: none;
}
.help > summary::-webkit-details-marker { display: none; }
.help > summary::before {
  content: ""; flex: 0 0 auto;
  width: 0.42rem; height: 0.42rem;
  border-right: 2px solid var(--ink-3); border-bottom: 2px solid var(--ink-3);
  transform: rotate(-45deg); transition: transform 0.15s;
}
.help[open] > summary::before { transform: rotate(45deg); }
.help > summary:hover { color: var(--ink); }
.help > summary strong { font-weight: 600; color: var(--ink); }
.help > summary:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }

.help__body {
  padding: 0.1rem 0 1.1rem 1rem;
  border-left: 2px solid var(--line); margin-left: 0.15rem;
}
.help__body p {
  font-size: 0.88rem; color: var(--ink-2); margin: 0 0 0.7rem; max-width: 78ch;
}
.help__body p:last-child { margin-bottom: 0; }
.help__body strong { color: var(--ink); font-weight: 600; }
.help__body table { font-size: 0.86rem; }

/* A command to type. Selectable, scrollable, and visibly not prose. */
.help__code {
  font-family: var(--mono); font-size: 0.8rem; line-height: 1.6;
  background: var(--bg); border: 1px solid var(--line); border-radius: 6px;
  padding: 0.6rem 0.75rem; margin: 0 0 0.8rem;
  overflow-x: auto; white-space: pre; color: var(--ink);
}

/* The things that cost data if ignored. Amber, because they are warnings about
   consequences rather than errors that have happened. */
.help__warn {
  display: flex; gap: 0.5rem; align-items: flex-start;
  background: #FDF6E7; border: 1px solid #E8DCC3; border-radius: 6px;
  padding: 0.6rem 0.75rem; margin: 0.9rem 0 0 !important;
  font-size: 0.85rem !important; color: #6B4A0F !important;
}
.help__warn i { color: var(--warn); margin-top: 0.1rem; flex: 0 0 auto; }

/* ------------------------------------------------------------ device admins -- */
/* Who can open a machine's menu. The most important thing on a device card:
   an admin can add and remove users at that branch, and a machine with none
   has an open menu. */
.admin-list { margin: 0 0 1rem; }
.admin-list h3 {
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--ink-3); margin: 0 0 0.2rem;
}
.admin-list > p { margin: 0 0 0.6rem; font-size: 0.85rem; }
.admin-list--none {
  display: flex; gap: 0.6rem; align-items: flex-start;
  background: #FDF6E7; border: 1px solid #E8DCC3; border-radius: var(--radius);
  padding: 0.75rem 0.9rem; font-size: 0.88rem; color: #6B4A0F;
}
.admin-list--none i { color: var(--warn); margin-top: 0.1rem; flex: 0 0 auto; }
.admin-list--none strong { color: #4A320A; }

/* ------------------------------------------------- your own machine access -- */
/* The one panel on the dashboard that is about the person looking at it. Their
   PIN is public (it is their employee ID); the password is not, so it is set
   apart rather than sitting in a row of facts. */
.da-title {
  font-size: 0.95rem; font-weight: 700; color: var(--ink);
  margin: 0 0 0.3rem; display: flex; align-items: center; gap: 0.45rem;
}
.da-title i { color: var(--teal-dark); }
.da-card {
  border: 1px solid var(--line); border-radius: 8px;
  padding: 0.8rem 0.95rem; margin-bottom: 0.7rem; background: var(--bg);
}
.da-card:last-child { margin-bottom: 0; }
.da-card h4 {
  font-size: 0.86rem; font-weight: 600; color: var(--ink); margin: 0 0 0.6rem;
}
.da-rows { display: flex; flex-wrap: wrap; gap: 0.6rem 2.4rem; }
.da-rows > div { display: flex; flex-direction: column; gap: 0.05rem; }
.da-rows span {
  font-size: 0.66rem; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-3);
}
.da-rows strong {
  font-size: 1.15rem; font-weight: 700; color: var(--ink); line-height: 1.2;
}
.da-rows em { font-size: 0.76rem; color: var(--ink-3); font-style: normal; }
.da-secret {
  font-family: var(--mono); letter-spacing: 0.06em;
  background: #FDF6E7; border: 1px solid #E8DCC3; border-radius: 5px;
  padding: 0.05rem 0.4rem; align-self: flex-start;
}
.da-quicker { margin: 0.7rem 0 0 !important; font-size: 0.82rem !important; }

/* One person's timesheet. Read down a column — were the days consistent, which
   are short, which have a punch missing — so no rule between rows. */
.ts-person-summary {
  display: flex; flex-wrap: wrap; gap: 1.2rem 2.4rem;
  padding-bottom: 1rem; margin-bottom: 0.3rem;
  border-bottom: 1px solid var(--line);
}

/* --------------------------------------------------------- period and totals -- */
.ts-range { align-items: center; gap: 0.8rem 1.1rem; }
.ts-presets { display: flex; gap: 0.35rem; flex-wrap: wrap; }
.ts-dates { display: flex; align-items: center; gap: 0.45rem; }
.ts-dates input { max-width: 10.5rem; }
.ts-toggle { margin-left: auto; }

/* Hours against expectation.
   The excess sits beside the total rather than under it, because the two are
   read together — a big number on its own says nothing without the shift it is
   being measured against. Tone is carried by a rule at the top rather than by
   colouring the figure: an amber number reads as an error, and somebody
   working over their days has not made one. */
/* THE HIGHLIGHTED FIGURE, and it is now a shared component.
   This tile was invented on Timesheets and asked for everywhere else — the
   figure large and tabular, the label small and uppercase above nothing, the
   context in an `em` underneath, and the TONE carried by the 3px rule along
   the top rather than by the colour of the number.
   `.figure` is the name to use from here on. `.ts-total` is the same rule,
   kept because Timesheets is built on it; one definition, two names, so the
   two cannot drift into looking almost alike. */
.figures,
.ts-totals {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
}
.figure,
.ts-total {
  display: flex; flex-direction: column; gap: 0.1rem;
  padding: 0.75rem 0.9rem;
  background: var(--bg); border: 1px solid var(--line);
  border-radius: 8px; border-top: 3px solid var(--line);
}
.figure strong,
.ts-total strong {
  font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em;
  line-height: 1.15; color: var(--ink); font-variant-numeric: tabular-nums;
}
.figure span,
.ts-total span {
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--ink-3);
}
.figure em,
.ts-total em {
  font-style: normal; font-size: 0.78rem; color: var(--ink-3);
  margin-top: 0.25rem; line-height: 1.35;
}
/* The unit that follows a figure — "days", "of 21". Part of the number, so it
   sits on the same line at the weight of the label rather than becoming a
   second figure competing with the first. */
.figure strong small {
  font-size: 0.78rem; font-weight: 600; color: var(--ink-3);
  letter-spacing: 0; margin-left: 0.25rem;
}
.ts-total--over { border-top-color: var(--warn); }
.ts-total--under { border-top-color: var(--teal); }
/* Tone along the top, never on the digits. "3 days" is not an error.
   --now    it falls due today
   --soon   this week
   --late   the date has passed */
.figure--now  { border-top-color: var(--warn); }
.figure--soon { border-top-color: var(--teal); }
.figure--late { border-top-color: var(--err); }
/* A figure that is a link to the page that acts on it. The whole tile, not a
   word inside it — a 2rem number is the thing the hand goes for. */
a.figure { text-decoration: none; color: inherit; transition: border-color .12s, background .12s; }
a.figure:hover { background: var(--card); border-color: #C9D4D6; }
a.figure:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
/* Nothing to count, because nothing counts it yet. Dashes rather than a zero,
   and the tile says why in its own `em`. */
.figure--untracked strong { color: var(--ink-3); font-weight: 600; }


/* ------------------------------------------------------- who is on site now -- */
/* A pill per site, and the count in it is EMPLOYEES only.
   Off-payroll people punch on the same terminals and are deliberately left
   out of the number — they are not employed, and a headcount that includes
   the guards makes every per-head figure downstream wrong. They are shown
   beside it instead, small, because somebody standing at the door can see
   them and a figure that disagrees with the door stops being believed.

   Fixed reading order, never sorted by count: a row of pills that reorders
   itself through the day is a row where the pill you are looking for has
   moved. */
/* The three groups. The label is a word beside the row rather than a heading
   above it: three headings for nine pills is more structure than nine pills
   deserve, and the eye needs only to know which row it is reading. */
.sitegroup {
  display: flex; align-items: baseline; gap: 0.7rem;
  flex-wrap: wrap; margin-bottom: 0.5rem;
}
.sitegroup:last-child { margin-bottom: 0; }
.sitegroup__label {
  flex: 0 0 7.5rem;
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--ink-3);
  cursor: help;
}
@media (max-width: 700px) {
  /* Stacked, and then the label does want to be a heading. */
  .sitegroup { display: block; }
  .sitegroup__label { display: block; margin-bottom: 0.35rem; }
}
.sites { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.site {
  display: inline-flex; align-items: baseline; gap: 0.45rem;
  padding: 0.4rem 0.85rem;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 100px; cursor: pointer; text-align: left;
  font: inherit; color: var(--ink);
}
.site:hover { border-color: #C9D4D6; background: var(--bg); }
.site:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.site__code {
  font-size: 0.75rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-3);
}
.site__n {
  font-size: 1.1rem; font-weight: 700; letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums; color: var(--ink);
}
.site__of { font-size: 0.74rem; color: var(--ink-3); }
/* The state dot. Three states and they are not decoration: a 0 from a live
   terminal means nobody is here, and a 0 from a terminal that stopped
   answering at lunch means we cannot see the place. */
.site__dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--ok); flex: 0 0 auto; align-self: center;
}
.site--live  .site__dot { background: var(--ok); }
.site--stale .site__dot { background: var(--warn); }
.site--none  .site__dot { background: var(--line); }
.site--none  { color: var(--ink-3); }
.site--none .site__n { color: var(--ink-3); font-weight: 600; }
/* Somebody is actually there. The one pill in the row that should catch an
   eye scanning it. */
.site--has { border-color: #BFDECB; background: #F3FAF6; }
.site[aria-expanded="true"] { border-color: var(--teal); background: var(--teal-pale); }
/* The off-roll count, when there is one. Same colour as the label, never the
   colour of the employee count it sits next to. */
.site__off { font-size: 0.72rem; color: var(--ink-3); }

/* WHERE IS ONE PERSON.
   Above the pills, because it answers a question somebody arrives with rather
   than one the pills provoke. The hint sits beside the box and not under the
   results: it is a caveat about what the answer means, and a caveat read
   after the answer is a caveat that has already been misread. */
.whereis {
  display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
  margin-bottom: 0.9rem;
}
.whereis input { max-width: 20rem; }
.whereis .muted { font-size: 0.78rem; }
.whereis-out { margin-bottom: 0.9rem; }
.whereis-none { margin: 0 0 0.9rem; font-size: 0.85rem; }
.whereis-note {
  margin: 0.6rem 0 0; font-size: 0.78rem; line-height: 1.4;
  max-width: 70ch;
}
/* The in/out pill is the first thing in the row and it is the answer, so it
   keeps its width whatever the name beside it does. */
.whereis-out .site-person > .pill { flex: 0 0 2.6rem; text-align: center; }

/* Who is in the place, once a pill is pressed. */
.site-people { margin-top: 0.8rem; }
.site-people__head {
  display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap;
  margin-bottom: 0.5rem;
}
.site-people__head h3 { margin: 0; font-size: 1rem; }
.site-person {
  display: flex; align-items: baseline; gap: 0.6rem;
  padding: 0.35rem 0; border-bottom: 1px solid var(--line);
  font-size: 0.88rem;
}
.site-person:last-child { border-bottom: 0; }
.site-person__for { margin-left: auto; color: var(--ink-3); font-size: 0.8rem; }
.ts-totals__ot { margin: 0.9rem 0 0 !important; font-size: 0.83rem !important; }

/* ------------------------------------------------ who this timesheet is for -- */
/* The facts that give the hours their units. Chips rather than a table row:
   they are read together and at a glance, and a table makes them look like
   data to be scanned rather than context to be absorbed. */
.subj-facts {
  display: flex; flex-wrap: wrap; gap: 0.4rem;
  margin: 0 0 1rem;
}
/* Each pill is a statement, not a label and a value. "Type: Corporate" spent a
   word saying what the next word already said. The rate lives IN the pill,
   because "eligible" without a rate is half an answer, and the reason lives in
   the title so hovering or focusing explains it. */
.subj-pill {
  display: inline-flex; align-items: center;
  font-size: 0.82rem; font-weight: 600; color: var(--ink);
  background: var(--card); border: 1px solid var(--line);
  border-radius: 100px; padding: 0.32rem 0.8rem;
  cursor: default;
}
.subj-pill[tabindex]:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
/* Eligible / not, in green and red as asked. These are the theme's semantic
   colours and this is a legitimate use of them: an entitlement either exists
   or it does not, and which one it is changes what every figure below means. */
.subj-pill--yes {
  background: #EAF6EF; border-color: #BFDECB; color: #17703F;
}
.subj-pill--no {
  background: #FCEFEE; border-color: #EFD3D0; color: #8F281F;
}

/* The caveats, folded away.
   Six paragraphs above the data is a wall, and a wall gets skipped — which is
   worse than one line, because the reader then has none of it. */
.ts-caveats {
  background: var(--teal-pale); border: 1px solid #CDE9EF;
  border-radius: var(--radius); margin-bottom: 1rem;
}
.ts-caveats > summary {
  cursor: pointer; list-style: none;
  padding: 0.6rem 0.9rem; display: flex; align-items: center; gap: 0.5rem;
  font-size: 0.85rem; color: var(--teal-dark); font-weight: 600;
}
.ts-caveats > summary::-webkit-details-marker { display: none; }
.ts-caveats > summary::after {
  content: ""; margin-left: auto; flex: 0 0 auto;
  width: 0.38rem; height: 0.38rem;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg); transition: transform 0.15s;
}
.ts-caveats[open] > summary::after { transform: rotate(-135deg); }
.ts-caveats > summary:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.ts-caveats__body { padding: 0 0.9rem 0.8rem 2.05rem; }
.ts-caveats__body p {
  margin: 0 0 0.5rem; font-size: 0.85rem; color: var(--ink-2); max-width: 88ch;
}
.ts-caveats__body p:last-child { margin-bottom: 0; }

/* -------------------------------------------------------- presence overview -- */
.pres-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin-bottom: 0.9rem;
}
.pres-head .da-title { margin: 0; }

/* One terminal, collapsed. The count is the answer most of the time; the names
   matter when it is wrong. */
.pres-machine {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); margin-bottom: 0.6rem;
}
.pres-machine > summary {
  cursor: pointer; list-style: none;
  padding: 0.75rem 0.95rem;
  display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap;
}
.pres-machine > summary::-webkit-details-marker { display: none; }
.pres-machine > summary strong { font-size: 0.95rem; color: var(--ink); }
.pres-machine > summary:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.pres-count {
  font-size: 0.85rem; font-weight: 600; color: var(--teal-dark);
  background: var(--teal-pale); border-radius: 100px; padding: 0.15rem 0.6rem;
}
.pres-meta { margin-left: auto; font-size: 0.8rem; }
.pres-body { padding: 0 0.95rem 0.9rem; }

/* A terminal's health in three states, not two: "quiet" is the one to look at
   before it becomes "offline". A dot rather than a word, because the word is
   already in the meta line and two of them is noise. */
.pres-dot {
  width: 0.5rem; height: 0.5rem; border-radius: 50%; flex: 0 0 auto;
}
.pres-dot--ok { background: var(--ok); }
.pres-dot--warn { background: var(--warn); }
.pres-dot--err { background: var(--err); }

/* -------------------------------------------------------- the two questions -- */
.ts-choose {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
}
.ts-choose__card {
  border: 1px solid var(--line); border-radius: 8px;
  padding: 0.95rem 1.05rem; background: var(--bg);
}
.ts-choose__card h4 {
  font-size: 0.92rem; font-weight: 600; color: var(--ink); margin: 0 0 0.25rem;
}
.ts-choose__card > p { margin: 0 0 0.8rem; font-size: 0.85rem; }

/* -------------------------------------------------------- the timesheet list -- */
/* A list, not a chart. The question here is who was in, from when to when, and
   for how long — four columns answer it and fit on a phone. The timeline that
   used to be here answered "were the days consistent", which nobody was
   asking. */
.ts-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.ts-table th {
  text-align: left; font-size: 0.66rem; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--ink-3);
  padding: 0 0.7rem 0.5rem; border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
.ts-table td {
  padding: 0.55rem 0.7rem; border-bottom: 1px solid var(--line);
  color: var(--ink-2);
}
.ts-table tr:last-child td { border-bottom: 0; }
.ts-table .num { text-align: right; }
.ts-table a { color: var(--ink); font-weight: 600; text-decoration: none; }
.ts-table a:hover { color: var(--teal-dark); text-decoration: underline; }
.ts-table .emp-id { font-size: 0.78rem; }
/* Hours line up under each other, which is the whole reason for a column of
   figures. */
.tnum { font-variant-numeric: tabular-nums; }

/* A missing out-time. Named rather than blank: an empty cell reads as an
   oversight in the page rather than a fact about the day. */
.ts-missing {
  font-size: 0.76rem; font-weight: 600; color: var(--warn);
  background: #FDF6E7; border: 1px solid #E8DCC3;
  border-radius: 4px; padding: 0.05rem 0.35rem; white-space: nowrap;
}
/* STILL IN, which is not the same as a missed punch. A day in progress is an
   ordinary state of affairs — teal like the rest of the "this is fine"
   vocabulary, not the amber of something to chase. It becomes .ts-missing
   after fourteen hours, and that one IS amber. */
.ts-open {
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--teal-dark);
  white-space: nowrap;
}
/* Before the payroll cut-off. Dimmed, and said once in the notes. */
.ts-tr--before td { opacity: 0.6; }

/* =========================================================================
   LETTERS
   A warning is not a document, it is a step in a sequence — and each one is
   evidence that the one before it happened. So the page leads with where
   somebody stands, then what can be issued next, then the letter itself on a
   sheet that prints as the real thing.
   ========================================================================= */
.esc {
  display: flex; gap: 1.1rem; align-items: flex-start;
  border-radius: var(--radius); padding: 0.9rem 1rem;
  border: 1px solid var(--line); background: var(--bg);
}
.esc__count { text-align: center; flex: 0 0 auto; min-width: 4.5rem; }
.esc__count strong {
  display: block; font-size: 1.9rem; font-weight: 700; line-height: 1;
  letter-spacing: -0.02em; color: var(--ink); font-variant-numeric: tabular-nums;
}
.esc__count span {
  font-size: 0.64rem; font-weight: 700; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--ink-3);
}
.esc p { margin: 0 0 0.3rem; font-size: 0.9rem; color: var(--ink-2); max-width: 74ch; }
.esc p:last-child { margin-bottom: 0; }
.esc--ok { border-left: 3px solid var(--ok); }
.esc--warn { border-left: 3px solid var(--warn); background: #FDF6E7; }
.esc--err { border-left: 3px solid var(--err); background: #FCEFEE; }

/* The kinds of letter. A kind the escalation does not allow is shown disabled
   with the reason rather than hidden: a missing button is a mystery, a
   disabled one with a sentence is an explanation. */
.kind-grid {
  display: grid; gap: 0.6rem; margin-bottom: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}
.kind {
  display: flex; flex-direction: column; gap: 0.2rem; text-align: left;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 8px; padding: 0.7rem 0.85rem; cursor: pointer; font: inherit;
}
.kind:hover { border-color: var(--teal); background: var(--teal-pale); }
.kind:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.kind strong { font-size: 0.9rem; color: var(--ink); }
.kind span { font-size: 0.78rem; color: var(--ink-3); line-height: 1.4; }
.kind--off { opacity: 0.55; cursor: not-allowed; }
.kind--off:hover { border-color: var(--line); background: var(--card); }

.letter-form { max-width: 44rem; }
.letter-form h4 { font-size: 1rem; font-weight: 600; margin: 0 0 0.8rem; color: var(--ink); }
.letter-form .form-label { margin-top: 0.9rem; }
.inc {
  display: grid; grid-template-columns: 1.6rem 1fr 2rem;
  gap: 0.5rem; align-items: start; margin-bottom: 0.6rem;
}
.inc__n {
  font-size: 0.85rem; font-weight: 700; color: var(--ink-3);
  padding-top: 0.55rem; text-align: right;
}
.inc textarea { margin-top: 0.35rem; }
.obj {
  display: grid; grid-template-columns: 1fr 2rem; gap: 0.5rem;
  margin-bottom: 0.5rem;
}

/* ---------------------------------------------------------------- the sheet -- */
/* Deliberately a sheet of paper on screen too. Somebody about to put their
   name to a letter should see it as the person receiving it will, not as a
   form. */
.sheet {
  background: #fff; border: 1px solid var(--line);
  border-radius: 4px; padding: 3rem 3.2rem;
  max-width: 48rem; margin: 0 auto 1rem;
  font-size: 0.95rem; line-height: 1.65; color: #111;
  box-shadow: 0 8px 28px -20px rgba(19,26,28,0.4);
}
.sheet__head {
  text-align: right; font-size: 0.78rem; line-height: 1.5; color: #333;
  margin-bottom: 2.2rem;
}
.sheet__head strong { display: block; font-size: 0.95rem; color: #000; }
.sheet__title {
  text-align: center; font-size: 1rem; font-weight: 700;
  text-decoration: underline; margin: 0 0 1.6rem; letter-spacing: 0.01em;
}
.sheet__date { text-align: right; margin: 0 0 1.6rem; }
.sheet__to { margin: 0 0 1.4rem; }
.sheet p { margin: 0 0 1rem; }
.sheet__list { margin: 0 0 1rem; padding-left: 1.6rem; }
.sheet__list li { margin-bottom: 0.6rem; }
.sheet__table {
  width: 100%; border-collapse: collapse; margin: 0 0 1rem;
  font-size: 0.9rem;
}
.sheet__table th, .sheet__table td {
  border: 1px solid #ccc; padding: 0.45rem 0.6rem; text-align: left;
}
.sheet__table th { width: 12rem; background: #f6f6f6; font-weight: 600; }
.sheet__note { font-size: 0.82rem; color: #444; font-style: italic; }
.sheet__sign { margin-top: 2.4rem; }
.sheet__space { height: 3.2rem; }
.sheet__ack {
  margin-top: 2.6rem; padding-top: 1rem; border-top: 1px dashed #bbb;
  font-size: 0.88rem;
}
.sheet__ack p { margin: 0 0 0.8rem; }

/* Printing. The sheet is the page; everything else is scaffolding. */
@media print {
  .no-print, .rail, .main-content > .wrap > .page-head,
  .panel, #pickPanel, #escalation, #compose, #historyPanel { display: none !important; }
  .main-content { margin-left: 0 !important; }
  .wrap { max-width: none !important; padding: 0 !important; }
  .sheet {
    border: 0; box-shadow: none; padding: 0; max-width: none;
    border-radius: 0; font-size: 11pt;
  }
  @page { margin: 18mm 16mm; }
}

/* ══════════════════════════════════════════════════════════════════════════
   ONE PILL.

   A small word saying what state something is in: draft, final, revoked,
   queried, expiring, blocking. It appears on nine pages and had been
   reinvented on five of them — .cl-state on Expense Claims, .bl-tag on
   Blockers, .tok-state on API Access, .rq-cost on Reconcile, .rc-status on
   Register Clean-up — each with its own padding, its own radius and its own
   idea of what green means.

   Six meanings, and no more. A palette that grows a colour per page is a
   palette that stops telling the reader anything:

     .pill            neutral — a fact, no judgement
     .pill--ok        it happened, or it is fine
     .pill--warn      it needs somebody
     .pill--bad       it is wrong, or it was refused
     .pill--info      in progress, waiting
     .pill--quiet     over, superseded, archived

   Geometry is .status-pill's, which was already correct: a 999px lozenge at
   0.7rem. This adds the colours it was missing and one name for all of them.
   My first attempt at this used a 4px radius and would have been a sixth
   variant rather than a unification.
   ══════════════════════════════════════════════════════════════════════════ */

.pill {
  display: inline-flex; align-items: center; gap: 0.3rem;
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; white-space: nowrap;
  padding: 0.24rem 0.55rem; border-radius: 999px;
  border: 1px solid var(--line);
  background: #F2F4F4; color: var(--ink-2);
  vertical-align: middle;
}

.pill--ok    { background: #EAF6EF; color: #1A7A4A; border-color: #BFDECB; }
.pill--warn  { background: #FDF6E7; color: var(--warn); border-color: #E8DCC3; }
.pill--bad,
/* --err is app.css's name for the same meaning, and the dashboard, Data
   Health, Devices and the approval queue all write it. Adding .pill above
   without it silently repainted every one of those grey: the base rule sets
   a neutral ground and sits LATER in the cascade than app.css's .pill--err,
   so the severity colour lost. The dashboard's "Critical" rows going the
   same grey as everything else is how it was noticed. */
.pill--err   { background: #FCEFEE; color: var(--err); border-color: #EFD3D0; }
.pill--info  { background: var(--teal-pale); color: var(--teal-dark); border-color: #CFE9EF; }
.pill--quiet,
/* Same again: app.css calls the neutral one --muted. */
.pill--muted { background: #F2F4F4; color: var(--ink-3); border-color: var(--line); }

/* The old names, aliased rather than hunted down across nine pages. */
/* .status-pill was already the right shape — a 999px lozenge at 0.7rem —
   so .pill above was made to match IT rather than the other way round. What
   it lacked was a border and a neutral ground, which is what made the
   per-page reinventions look different even when the geometry agreed. */
.status-pill {
  border: 1px solid var(--line);
  background: #F2F4F4;
  vertical-align: middle;
}
.cl-state, .tok-state, .bl-tag, .rc-status {
  display: inline-flex; align-items: center; gap: 0.3rem;
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.06em;
  text-transform: uppercase; white-space: nowrap;
  padding: 0.24rem 0.55rem; border-radius: 999px;
  border: 1px solid var(--line);
  background: #F2F4F4; color: var(--ink-2);
  vertical-align: middle;
}

/* State names as they are written in the data, mapped onto the six meanings
   above — so a page can print its own vocabulary and still look like the rest
   of the product. */
/* Blockers writes TWO classes per item and they are not interchangeable:
   .bl--blocking on the whole card, and .bl-tag--blocking on the little word
   inside it. These rules named the card, so `color: var(--err)` cascaded into
   every paragraph of a blocking item — the title, the prose, "I need" and
   "unblocks" were all red, which is unreadable and stops the colour meaning
   anything. It is the tag that carries the state. */
.cl-filed,    .tok-active,   .bl-tag--waiting  { background: var(--teal-pale); color: var(--teal-dark); border-color: #CFE9EF; }
.cl-approved, .cl-paid,      .tok-expiring     { background: #EAF6EF; color: #1A7A4A; border-color: #BFDECB; }
.cl-queried,  .bl-tag--degraded                { background: #FDF6E7; color: var(--warn); border-color: #E8DCC3; }
.cl-declined, .tok-revoked,  .bl-tag--blocking { background: #FCEFEE; color: var(--err); border-color: #EFD3D0; }
.tok-expired                                    { background: #F2F4F4; color: var(--ink-3); border-color: var(--line); }


/* =========================================================================
   THE SHELL BAR
   Who is signed in, and what needs attention. Fixed at the top right on
   every console page.

   WHY IT IS NOT PART OF THE PAGE. Every page would otherwise decide for
   itself whether to show the bell, where to put it and what to call the
   person — which is how a console ends up with the user's name in three
   places and the alert count in none. shell.js draws it once.

   It floats over the content rather than pushing it down: the page heads
   were all written against a top edge, and inserting a 48px band above
   them would move every heading on every page by 48px. So it is
   position:fixed with a transparent ground, and the pages keep their
   measure. The two controls carry their own surfaces so they stay legible
   over whatever is behind them.
   ========================================================================= */
.shellbar {
  position: fixed; top: 0; right: 0; z-index: 1035;
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.7rem 1.1rem;
  pointer-events: none;            /* the gap between the two is not a target */
}
.shellbar > * { pointer-events: auto; }

/* A BUTTON, because pressing your own name is how you sign out. It was a
   div, and a div carrying the only route to Sign out is unreachable by
   keyboard. */
.shellbar__who {
  display: flex; align-items: center; gap: 0.5rem;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 100px; padding: 0.25rem 0.6rem 0.25rem 0.3rem;
  box-shadow: 0 1px 2px rgba(19,26,28,0.05);
  font-size: 0.82rem; line-height: 1.25;
  font: inherit; font-size: 0.82rem; cursor: pointer; text-align: left;
}
.shellbar__who:hover { border-color: var(--teal); }
/* The name-and-role stack. NOT `> span`: that also matched the avatar next
   to it, and at one class plus one type it beat `.shellbar__avatar`, so the
   avatar's `display:grid; place-items:center` was overridden to `block` and
   the initials sat 5px up and left of their circle's centre. */
.shellbar__id { display: block; }
.shellbar__chev { font-size: 0.62rem; color: var(--ink-3); }

/* The account menu. Narrower than the alert panel — it holds two rows, not a
   list of findings. */
.shellbar__menu { width: min(17rem, calc(100vw - 2rem)); }
.shellbar__item {
  display: flex; align-items: center; gap: 0.55rem; width: 100%;
  padding: 0.65rem 1rem; border: 0; background: none;
  border-bottom: 1px solid var(--line);
  font: inherit; font-size: 0.88rem; color: var(--ink-2);
  text-align: left; text-decoration: none; cursor: pointer;
}
.shellbar__item:last-child { border-bottom: 0; }
.shellbar__item:hover { background: var(--bg); color: var(--ink); }
.shellbar__item--danger:hover { color: var(--err); }
.shellbar__avatar {
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--teal); color: #fff;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.02em;
  /* Belt and braces on the vertical: with a line-height of 1 the glyph box
     is the cap height, so flex centring has nothing extra to fight. Two
     capitals have no descenders, which is why initials are the one case
     where this is exactly right rather than approximately. */
  line-height: 1;
  flex: 0 0 auto; overflow: hidden;
}
.shellbar__name {
  font-weight: 600; color: var(--ink); white-space: nowrap; display: block;
}
.shellbar__role {
  color: var(--ink-3); font-size: 0.7rem; text-transform: uppercase;
  letter-spacing: 0.06em; white-space: nowrap; display: block;
}

.shellbar__bell {
  position: relative;
  background: var(--card); border: 1px solid var(--line);
  border-radius: 50%; width: 36px; height: 36px;
  display: grid; place-items: center; cursor: pointer;
  color: var(--ink-2); font-size: 1rem;
  box-shadow: 0 1px 2px rgba(19,26,28,0.05);
}
.shellbar__bell:hover { border-color: var(--teal); color: var(--teal-dark); }

/* The count. Absent rather than zero: a badge showing 0 is a badge that
   trains people to ignore badges. */
.shellbar__count {
  position: absolute; top: -3px; right: -3px;
  min-width: 17px; height: 17px; padding: 0 4px;
  border-radius: 100px; background: var(--err); color: #fff;
  font-size: 0.62rem; font-weight: 700; line-height: 17px;
  text-align: center; border: 2px solid var(--card);
}
.shellbar__count--look { background: var(--warn); }

.shellbar__panel {
  position: absolute; top: calc(100% - 0.3rem); right: 1.1rem;
  width: min(26rem, calc(100vw - 2rem));
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: 0 18px 48px -18px rgba(19,26,28,0.4);
  max-height: min(30rem, 80vh); overflow-y: auto; overscroll-behavior: contain;
}
.shellbar__panel-head {
  padding: 0.8rem 1rem 0.6rem; border-bottom: 1px solid var(--line);
}
.shellbar__panel-head strong { font-size: 0.95rem; }
.shellbar__panel-head p {
  margin: 0.2rem 0 0; font-size: 0.8rem; color: var(--ink-3);
}
.shellbar__alert {
  display: block; padding: 0.7rem 1rem; border-bottom: 1px solid var(--line);
  border-left: 3px solid var(--line);
  text-decoration: none; color: inherit;
}
.shellbar__alert:last-child { border-bottom: 0; }
.shellbar__alert:hover { background: var(--bg); }
.shellbar__alert--act { border-left-color: var(--err); }
.shellbar__alert--look { border-left-color: var(--warn); }
.shellbar__alert-what {
  font-weight: 600; font-size: 0.87rem; color: var(--ink);
  display: flex; gap: 0.45rem; align-items: baseline;
}
.shellbar__alert-why {
  margin: 0.25rem 0 0; font-size: 0.8rem; color: var(--ink-3);
}
.shellbar__empty {
  padding: 1.2rem 1rem; font-size: 0.87rem; color: var(--ok);
  font-weight: 600;
}

/* ROOM FOR THE BAR, ON EVERY PAGE.
   This used to be `padding-right` on `.page-head:first-child` inside `.wrap`
   — which reserved space only on the pages that happen to have both, and
   silently did nothing on the eleven that have neither. Those pages put
   their own first heading straight under a fixed bar, which is what "I can't
   see the top right headers" was.

   So the reserve is on `.main-content` itself: a top padding the height of
   the bar. Nothing can slide under it, whatever a page is built out of, and
   it costs one rule instead of a per-page opt-in that pages forget.

   The bar is 36px of control plus 0.7rem of padding each side, so 3.4rem
   clears it with a little air. Only on wide screens — below 992px the bar
   sits alongside the mobile bar and the pages scroll normally. */
@media (min-width: 992px) {
  .main-content { padding-top: 3.4rem; }
  /* The page's own first block then starts flush, so its top margin goes —
     otherwise the reserve and the margin stack into a gap. */
  .main-content > .wrap { padding-top: 0.4rem; }
  /* `!important`, and not lightly.
     Several of these pages carry Bootstrap's `py-4`, which IS `!important`,
     so an ordinary declaration loses and their first heading landed 32px
     lower than every .wrap page's. Walking from Dashboard to Reviews moved
     the heading, which is half of what "the layout changes between pages"
     means. There is no way to win against a utility class's !important
     except in kind. */
  .main-content .page-wrap,
  .main-content .wrapper,
  .main-content .workspace,
  .main-content .main-card,
  .main-content .container,
  .main-content .container-fluid { padding-top: 0.4rem !important; }
}
@media (max-width: 991.98px) {
  .shellbar { padding: 0.45rem 0.6rem; }
  .shellbar__name, .shellbar__role, .shellbar__chev { display: none !important; }
  .shellbar__who { padding: 0.25rem; border-radius: 50%; }
}
@media print {
  .shellbar { display: none !important; }
}
