/* =============================================================================
   Princess Operations Hub — admin dashboard styles.
   Light shell, warm neutral canvas, brand-navy accents — the DealerPro look:
   easy on the eyes, readable at a glance. No external fonts or CDNs (strict CSP).
   ============================================================================= */

:root {
  --navy: #1b2a45;
  --navy-700: #23375a;
  --navy-900: #131e33;
  --ink: #1a1c20;
  --ink-soft: #454b57;
  --muted: #6d7583;
  --line: #e4e4e0;
  --line-soft: #f0f0ec;
  --bg: #f6f6f4;
  --card: #ffffff;
  --accent: #2f6fed;
  --ok: #157a53;
  --ok-bg: #e4f3ec;
  --warn: #8a5a00;
  --warn-bg: #faf0da;
  /* The paler half of the caution pair. Only the hatched "on leave" fill on
     the Schedule grid needs two warm tints; it was carrying both as literals,
     so the hatch could drift away from every other caution surface. */
  --warn-bg-soft: #fff6e2;
  --bad: #b02a21;
  --bad-bg: #fbe9e7;
  --info: #1d4fb8;
  --info-bg: #e8eefb;
  /* The schematic worksite map. Sea and land sit close to the canvas tones so
     the card stays quiet; the pins carry the colour. */
  --map-sea: #e2ecf4;
  --map-land: #f7f4ec;
  --map-coast: #c3cbd4;
  --radius: 12px;
  --radius-ctl: 9px;
  /* ONE spacing rhythm for the canvas. Every gap between and inside cards
     derives from these two, which is what makes the pages read as balanced:
     equal air on all four sides of a card, equal air between any two cards. */
  --gap: 16px;
  --pad: 20px;
  /* ONE height for every single-line control, for the same reason. Left to
     their own metrics a text input, a select, a date field and a button come
     out 40 / 38 / 42 / 43px, and an <a class=btn> — inline, so vertical
     padding does not grow its line box — comes out 38px next to a 43px
     <button class=btn>. Six heights in one filter bar, all of them "correct".
     Every control is pinned to --ctl-h below so a row is level by
     construction rather than by luck of the font metrics. */
  --ctl-h: 40px;
  --ctl-h-sm: 32px;
  /* The gap between a field's label and its control. One value, so a label
     sits the same distance above its input on every page. */
  --label-gap: 6px;
  /* THE FRAME: the canvas gutter, the content column and the header band.
     They were literals in four rules here (.topbar twice, .content and the
     phone query) and guessed by anything outside them — the Permits & TCN
     sticky card pinned itself to a topbar height that had not existed for a
     month. One number each; page modules read them, never restate them. */
  --gutter: 24px;
  --column: 1280px;
  --topbar-h: 64px;

  /* ---- TYPE SCALE --------------------------------------------------------
     EIGHT sizes, all integers. There were eighteen, five of them half-pixel
     steps (10.5 / 11.5 / 12.5 / 13.5 / 14.5 / 15.5) sitting beside their own
     integers — so 12px and 12.5px text ran side by side on the same card, a
     difference too small to read as hierarchy and too large to be nothing.
     Every declaration in this file and in every page module now resolves to
     one of these. Half-steps rounded up; 10 folded into 11; 17 and 18 folded
     into their neighbours.

     Adding a ninth size is a design decision, not a convenience — if a new
     element needs one, it almost certainly wants an existing step plus a
     weight or colour change instead.

     TWO known values sit outside this and are meant to: the 21px "P" in the
     brand square (part of the mark, sized to its box) and the 22px +/− glyphs
     on Leaflet's zoom control, which come from the vendored leaflet.css and
     are sized to that control's own button box. Measured across all fourteen
     pages, nothing else renders at anything but the eight steps below. */
  --fs-2xs: 11px;    /* badges, table headers, axis labels, kbd */
  --fs-xs: 12px;     /* field hints, sub-lines under a name, footnotes */
  --fs-sm: 13px;     /* field labels, small buttons, card notes */
  --fs-md: 14px;     /* table cells */
  --fs-base: 15px;   /* body */
  --fs-lg: 16px;     /* card titles, section headings */
  --fs-xl: 19px;     /* page title */
  --fs-2xl: 26px;    /* the number on a stat tile */
  --shadow: 0 1px 2px rgba(20, 22, 28, .05);
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
}

* { box-sizing: border-box; }
html, body { height: 100%; }
body {
  margin: 0;
  font-family: var(--sans);
  font-size: var(--fs-base);
  line-height: 1.5;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}
.hidden { display: none !important; }
a { color: var(--accent); }

/* The skip link: off-screen until focused, then a real target at the top left.
   Not display:none and not visibility:hidden — either would take it out of the
   tab order and make it useless to exactly the people it is for. */
.skip-link {
  position: absolute; left: 8px; top: -60px; z-index: 300;
  padding: 10px 16px;
  background: var(--navy); color: #fff;
  border-radius: var(--radius-ctl);
  font-size: var(--fs-md); font-weight: 600; text-decoration: none;
  transition: top var(--dur-quick, .18s) ease;
}
.skip-link:focus { top: 8px; outline: 2px solid #fff; outline-offset: -4px; }

/* ---------- brand ---------------------------------------------------------- */
.brand { display: flex; align-items: center; gap: 10px; }
.brand .mark {
  width: 30px; height: 30px; flex: none;
  display: grid; place-items: center;
  border-radius: 8px;
  background: var(--navy); color: #fff;
  font-weight: 800; font-size: var(--fs-lg);
}
.brand .wordmark { color: var(--ink); font-size: var(--fs-base); letter-spacing: .01em; }
.brand .wordmark b { font-weight: 700; }
.brand.big { justify-content: center; margin-bottom: 4px; }
/* The one size outside the type scale, deliberately: this is the "P" inside the
   38px brand square — a piece of the mark, sized to its box, not text. */
.brand.big .mark { width: 38px; height: 38px; font-size: 21px; }
.brand.big .wordmark { font-size: var(--fs-lg); }

/* ---------- login ---------------------------------------------------------- */
.login {
  min-height: 100vh;
  display: grid; place-items: center;
  padding: 24px;
  background: linear-gradient(160deg, var(--navy-900), var(--navy) 55%, #2a3f66);
}
.login-card {
  width: 100%; max-width: 380px;
  background: var(--card);
  border-radius: 14px;
  padding: 30px 28px 24px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, .3);
}
.login-sub { margin: 0 0 22px; text-align: center; color: var(--muted); font-size: var(--fs-sm); }
/* The first-sign-in screen explains itself before asking for anything; the login
   screen has nothing to explain, so this only appears there. */
.login-note {
  margin: -12px 0 20px; font-size: var(--fs-sm); line-height: 1.5;
  color: var(--ink-soft); text-align: center;
}
.login-card label { display: block; margin-bottom: 14px; font-size: var(--fs-sm); font-weight: 600; color: var(--ink-soft); }
.login-card input { margin-top: 6px; }
.login-error {
  background: var(--bad-bg); color: var(--bad);
  border: 1px solid #f0c8c5; border-radius: 8px;
  padding: 9px 11px; font-size: var(--fs-sm); margin-bottom: 12px;
}
.login-foot { margin: 18px 0 0; font-size: var(--fs-xs); color: var(--muted); text-align: center; }

/* ---------- form controls -------------------------------------------------- */
/* The type list is exhaustive on purpose. A type missing from it gets NO part
   of this — no border, no padding, no height — and the omission is invisible
   until someone uses it: `search` was missing, so the Activity filter bar
   rendered Safari's native 21px pill between two 40px controls, which is what
   the ragged filter row on that page actually was. Add the type here before
   using it in a page module. An <input> with no type at all is a text input
   by the HTML default, so input:not([type]) is on the list too: six of them —
   the punch dialogs' Why and Note fields, the shift note — were bare 21px
   native boxes in dialogs where every other field was 40px. */
input[type=text], input[type=email], input[type=password], input[type=number],
input[type=date], input[type=time], input[type=search], input[type=tel],
input[type=url], input[type=month], input[type=week], input[type=datetime-local],
input:not([type]),
select, textarea {
  width: 100%;
  font: inherit;
  padding: 0 11px;
  border: 1px solid var(--line);
  border-radius: var(--radius-ctl);
  background: #fff;
  color: var(--ink);
}
/* Height, not padding, is what makes a row level: padding leaves the final
   number to each control's own line-box metrics, which is how 40 / 38 / 42
   happened. Excludes textarea, which is sized by its rows. */
input[type=text], input[type=email], input[type=password], input[type=number],
input[type=date], input[type=time], input[type=search], input[type=tel],
input[type=url], input[type=month], input[type=week], input[type=datetime-local],
input:not([type]),
select {
  height: var(--ctl-h);
}
/* A list box is not a single-line control. select[multiple] was caught by the
   rule above and pinned to 40px: the notices audience picker, drawn for seven
   rows (size=7), showed two of thirty-seven names through a scrollbar. */
select[multiple] { height: auto; padding: 6px; }
select[multiple] option { padding: 4px 6px; border-radius: 5px; }
/* Safari dresses type=search as a rounded native pill that ignores the border
   and padding above. Opting out of the appearance is the only way to make it
   the same control as its neighbours; the clear button is left alone. */
input[type=search] { -webkit-appearance: none; appearance: none; }
/* A native <select> and the app's own dropdown button (.ms-btn) share filter
   bars — Date range beside Workers and Sites on the Punch report form — so they
   have to read as one control. The rules above already give both the same 40px
   box, border and radius; this gives the select the same chevron, drawn with
   .ms-caret's own path and stroke, in the same place and colour, with room kept
   so a long option stops short of it. [multiple] is excluded: the notices
   audience picker is a list box, not a dropdown. The stroke is a literal
   because url() cannot read a custom property: #6d7583 is --muted, and moves
   with it. img-src allows data: (src/lib/http.ts). */
select:not([multiple]) {
  -webkit-appearance: none; appearance: none;
  padding-right: 32px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%236d7583' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  background-size: 14px 14px;
}
/* A native checkbox renders 13px. The label around it is the real target — the
   check rows are 32px tall, so WCAG 2.5.8 is met by the row rather than the box
   — but 13px is a small thing to aim at on the tablet this is actually used on,
   and 16px costs nothing. accent-color makes the checked state brand navy
   instead of the operating system's blue, which was the one control in the
   product still painting itself in a colour from outside the palette. */
input[type=checkbox], input[type=radio] {
  width: 16px; height: 16px; flex: none;
  accent-color: var(--navy);
}
/* A slider is a control too. The browser gives it no height, so on the Sites
   editor it sat 3px off the number box beside it. */
input[type=range] { width: 100%; height: var(--ctl-h); margin: 0; padding: 0; accent-color: var(--navy); }
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
  border-color: var(--accent);
}
textarea { min-height: 100px; padding: 9px 11px; resize: vertical; }
/* A column, so the label sits directly above its control at one fixed
   distance. It was `display:block` with a margin on the control, which is the
   same thing until the field lands in a grid — then the block box stretches to
   the row and the control drifts wherever the alignment puts it. */
label.field {
  display: flex; flex-direction: column; align-items: stretch;
  margin-bottom: 14px; font-size: var(--fs-sm); font-weight: 600; color: var(--ink-soft);
}
label.field > input, label.field > select, label.field > textarea { margin-top: var(--label-gap); font-weight: 400; }
/* Controls are flex items here, so they must not be squeezed below their set
   height by a tall sibling hint. */
label.field > input, label.field > select { flex: none; }

/* Native file pickers, dressed as the button they are. ::file-selector-button
   is the one part of <input type=file> CSS can reach; the filename text beside
   it stays native, which is fine — it is data, not chrome. */
input[type=file] {
  font-size: var(--fs-md); color: var(--ink-soft);
  /* Sits in the same rows as the other controls, so it keeps their height and
     centres its native filename text against them instead of riding 11px
     high in the middle of a filter row. */
  display: flex; align-items: center; min-height: var(--ctl-h);
}
input[type=file]::file-selector-button {
  font: inherit; font-weight: 600; font-size: var(--fs-sm);
  color: var(--ink);
  background: #fff;
  border: 1px solid var(--line);
  border-radius: 8px;
  min-height: var(--ctl-h-sm);
  padding: 0 12px;
  margin-right: 10px;
  cursor: pointer;
}
input[type=file]::file-selector-button:hover { background: var(--line-soft); }

/* ---------- buttons -------------------------------------------------------- */
/* inline-FLEX, not the browser default. `.btn` is worn by both <button> and
   <a>, and an <a> is inline: vertical padding does not grow an inline box, so
   the identical class rendered 43px on a button and 38px on a link, five
   pixels out of level wherever the two sat side by side (Settings' "Manage
   sites", the Schedule row actions). inline-flex gives the anchor a block-ish
   box, centres the label on both, and lets min-height carry the height so a
   button matches an input in the same row. */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  font: inherit; font-weight: 600;
  min-height: var(--ctl-h);
  padding: 6px 16px;
  border-radius: var(--radius-ctl);
  border: 1px solid var(--line);
  background: #fff; color: var(--ink);
  text-align: center;
  cursor: pointer;
}
.btn:hover { background: var(--line-soft); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn.primary { background: var(--navy); border-color: var(--navy); color: #fff; }
.btn.primary:hover { background: var(--navy-700); }
/* Quiet secondary on any light surface (cards, drawers, the sidebar footer). */
.btn.ghost { background: transparent; border-color: var(--line); color: var(--ink-soft); }
.btn.ghost:hover { background: var(--line-soft); border-color: var(--muted); color: var(--ink); }
/* Destructive: quiet until hovered, but unmistakably not a neutral action. */
.btn.danger { background: #fff; border-color: #ecccc9; color: var(--bad); }
.btn.danger:hover { background: var(--bad-bg); border-color: var(--bad); }
.btn.small { padding: 4px 11px; font-size: var(--fs-sm); min-height: var(--ctl-h-sm); }
.btn.block { display: flex; width: 100%; }
.btn:disabled { opacity: .55; cursor: not-allowed; }

/* ---------- shell ---------------------------------------------------------- */
.shell { display: flex; min-height: 100vh; }
/* Sized up (2026-08-07, owner request after a Timesheet Mobile side-by-side):
   the rail was set in 13.5px/18px icons and read as fine print next to the
   pages it navigates. 15px text and 20px icons put it at the same visual
   weight as the content, and the wider rail absorbs the growth so nothing
   wraps. */
.sidebar {
  width: 256px; flex: none;
  background: var(--card);
  border-right: 1px solid var(--line);
  color: var(--ink);
  display: flex; flex-direction: column;
  padding: 18px 14px 16px;
  position: sticky; top: 0; height: 100vh;
}
.company-switch { margin: 18px 0 6px; }
/* .ms-label since the switcher became UI.selectMenu; the rule reads the same
   as the section headers in the rail below it, which is the point. */
.company-switch .ms-label {
  display: block; font-size: var(--fs-2xs); text-transform: uppercase;
  letter-spacing: .09em; color: var(--muted); font-weight: 700; margin-bottom: 5px;
}
.company-switch .ms-btn { font-size: var(--fs-md); }
#nav { display: flex; flex-direction: column; gap: 2px; overflow-y: auto; flex: 1; }
/* Tiny uppercase group headers, the DealerPro way: they let ten destinations
   read as four decisions. */
#nav .nav-sec {
  font-size: var(--fs-2xs); font-weight: 700; text-transform: uppercase;
  letter-spacing: .09em; color: var(--muted);
  margin: 18px 12px 5px;
}
#nav a {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 12px;
  border-radius: 9px;
  color: var(--ink-soft); text-decoration: none;
  font-size: var(--fs-base); font-weight: 500;
}
#nav a:hover { background: var(--line-soft); color: var(--ink); }
#nav a.active { background: var(--navy); color: #fff; font-weight: 600; }
#nav .ic { width: 20px; height: 20px; flex: none; display: grid; place-items: center; opacity: .8; }
#nav a.active .ic { opacity: 1; }
#nav .ic svg { width: 20px; height: 20px; display: block; }
.sidebar-foot { border-top: 1px solid var(--line); padding-top: 12px; margin-top: 12px; }
.userbox { display: flex; align-items: center; gap: 10px; font-size: var(--fs-sm); margin-bottom: 10px; line-height: 1.3; }
.userbox .avatar {
  width: 32px; height: 32px; flex: none;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--line-soft); color: var(--navy);
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .02em;
}
.userbox b { display: block; color: var(--ink); font-size: var(--fs-sm); }
.userbox span { color: var(--muted); }

.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* THE BAR SITS ON THE SAME COLUMN AS THE CARDS. #content is a 1280px column
   centred inside .main, but the bar spans .main whole, so a flat 24px gutter
   here put the header on a different left and right edge from every card the
   moment the window passed 1280px + the sidebar: measured at 1920x1080, the
   page title started 148px to the LEFT of the first card and the company pill
   ended 168px PAST its right edge, which is what read as the dashboard's
   border being out of true. The max() keeps the plain 24px gutter until the
   column starts to centre, then grows with it — one expression, no wrapper
   element, and the white bar and its bottom rule still span the full width.
   Percentage padding resolves against the containing block (.main), which is
   the same width .content centres in, so the two can never disagree.

   Height is declared rather than inherited from whatever the tallest control
   in it happens to measure: with 13px of padding the bar was 64px only because
   an inline row of a 34px button and a 28px pill made a 37px line box. */
.topbar {
  display: flex; align-items: center; gap: 12px;
  background: var(--card);
  border-bottom: 1px solid var(--line);
  padding: 0 max(var(--gutter), calc((100% - var(--column)) / 2 + var(--gutter)));
  min-height: var(--topbar-h);
  position: sticky; top: 0; z-index: 20;
}
/* Payroll's period sheet opts out of the 1280px column (.content-wide below),
   so the bar follows it back out to the plain gutter rather than staying
   behind at a column the page no longer uses. */
.main:has(.content-wide) .topbar { padding: 0 var(--gutter); }
.topbar h1 { font-size: var(--fs-xl); margin: 0; font-weight: 650; letter-spacing: -.01em; }
/* The things with a fixed size (the back button, the section chip, the search
   button) are flex:none; the two things made of words — this title and the
   company pill — are the ones that give. Without that, below ~455px the bar was
   wider than the window: the page scrolled sideways, the browser zoomed the
   whole admin out to 88%, and the two icon buttons were squeezed to 18px. */
.topbar h1 { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The section's own icon beside the title — one glance says which page this
   is, before a word is read. Filled by app.js from the same NAV table that
   drew the sidebar, so the two can never disagree. */
/* --ctl-h-sm, like every other thing on this bar: the icon was 34px beside a
   32px back button, a 34px search button and a 28px pill — four heights in one
   row, none of them a token. */
.page-ic {
  width: var(--ctl-h-sm); height: var(--ctl-h-sm); flex: none;
  display: grid; place-items: center;
  border-radius: var(--radius-ctl);
  background: var(--navy); color: #fff;
}
.page-ic:empty { display: none; }
.page-ic svg { width: 19px; height: 19px; display: block; }
/* A flex row, not an inline one. Left inline, the search button and the company
   pill sat on a text baseline, so the box around them measured 37px — taller
   than either control — and it was that phantom line box, not the controls,
   that set the height of the whole bar. */
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 12px; min-width: 0; }
.burger, .backbtn {
  background: none; border: 0; border-radius: 8px;
  width: 32px; height: 32px; padding: 0; flex: none;
  display: grid; place-items: center;
  cursor: pointer; color: var(--ink-soft);
}
.burger:hover, .backbtn:hover { background: var(--line-soft); color: var(--ink); }
.burger:focus-visible, .backbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.burger svg, .backbtn svg { width: 18px; height: 18px; display: block; }
.burger { display: none; }
/* A pill is a chip at the small control height, because every pill in the
   admin sits in a row of controls: the company name on this bar, the week
   range beside the week buttons on Punch and Schedule, "3 unread" beside the
   queue's select, a job title beside the year select on a leave balance, the
   seven days of a work pattern. It was a padded inline span that measured
   27.5px beside 32px buttons in every one of those rows; only the topbar had
   pinned it. A count inside a heading is a .badge, not a pill. */
.pill {
  display: inline-flex; align-items: center;
  height: var(--ctl-h-sm); padding: 0 12px; border-radius: 20px;
  background: var(--line-soft); color: var(--ink-soft);
  font-size: var(--fs-sm); font-weight: 600; white-space: nowrap;
}
/* On a phone the company name would wrap the topbar to two lines; clamp it. */
.topbar .pill { max-width: 45vw; min-width: 2.5em; overflow: hidden; text-overflow: ellipsis; }
/* Equal air on every side: the canvas pads --pad+4 all round, cards are --gap
   apart, and the LAST child gives its bottom margin up — without that, every
   page ended (padding + margin) heavier at the bottom than the top, which is
   the imbalance the owner kept noticing. */
.content { padding: var(--gutter); flex: 1; width: 100%; max-width: var(--column); margin: 0 auto; }
/* A page that is one wide table opts out of the 1280px column. The payroll
   period sheet is fourteen columns that have to be read as a single row, and
   on a 27" display the column left it centred in a third of the screen with
   the rest empty. The page adds the class when it renders (payroll.js) and
   app.js's route() takes it off before every render, so nothing else inherits
   it by accident. */
.content.content-wide { max-width: none; }
.content:focus { outline: none; }
.content > :last-child { margin-bottom: 0; }
/* ...and through a wrapper. Payroll, Wages and Settings mount their whole page
   inside one div, the employee record inside two, and Permits & TCN ends on a
   grid — so the element the rule above reaches is the wrapper, and the last
   CARD inside it kept its 16px: those pages ended 40px heavy against a 24px
   top. A card or a grid hands the rule on to its own last child, whose margin
   is already zero (.card > :last-child below), so this never reaches into the
   content of a card. */
.content > :last-child > :last-child,
.content > :last-child > :last-child > :last-child { margin-bottom: 0; }
/* The way back from a record to its list — "← Back to pay periods". It is the
   first thing on five pages, and four of them wrapped it in a bare <p> the
   browser gave its own 1em margin: those pages started 15px lower than the
   lists they came from, and the Threads conversation dropped 18px when its
   data landed. One block, a 32px target, and the canvas rhythm to the card. */
.backlink {
  display: flex; width: max-content; align-items: center;
  min-height: var(--ctl-h-sm); margin: 0 0 var(--gap); padding: 0;
  font-size: var(--fs-md); font-weight: 500;
}

/* ---------- cards & layout -------------------------------------------------- */
.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--pad);
  margin-bottom: var(--gap);
}
/* DESCENDANT, not child. As `.card > h2` this only dressed the titles that
   happened to be direct children, and every card whose heading sits inside a
   header row — "Worker status", "Map view", "To-do list" on the Dashboard,
   "Open shifts" on Schedule — fell through to the browser's default h2 of
   1.5em, rendering at 22.5px beside 16px titles on the same screen. They read
   as a different level of heading because they were one. */
.card h2 { margin: 0 0 14px; font-size: var(--fs-lg); font-weight: 650; letter-spacing: -.005em; }
/* A card head: the title, then whatever sits beside it — a tab strip, a range,
   a count, a button — on ONE line, the same 14px above the body as a bare
   title. Three page modules each drew their own: the Dashboard's 10px under
   with the title pushing its controls right, Schedule's and Activity's 12px
   under and baseline-aligned, and Punch borrowed Activity's class name without
   its rule, so its three report heads had no layout at all. One rule, one
   gap. The minimum height is the small control's, so a head that holds a
   32px tab strip and a head that holds only words start their bodies on the
   same line across a band of cards. */
.cardhead, .dash-cardhead, .sch-cardhead, .act-head {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  min-height: var(--ctl-h-sm); margin-bottom: 14px;
}
.cardhead h2, .dash-cardhead h2, .sch-cardhead h2, .act-head h2 { margin: 0; flex: 1; }
.cardhead .muted, .dash-cardhead .muted, .sch-cardhead .muted, .act-head .muted { font-size: var(--fs-sm); }
/* The level below a card title — the group headings inside a long card, like
   the "Forms to generate and sign" / "Documents to gather" bands on Permits &
   TCN. Without a rule an h3 falls to the browser's 1.17em, which rendered at
   17.55px: LARGER than the 16px card title above it. */
.card h3 { margin: 18px 0 8px; font-size: var(--fs-base); font-weight: 650; }
.card h3:first-child { margin-top: 0; }
/* The inside of a card balances the same way the canvas does: whatever comes
   last — a table, a note, a form — sheds its trailing margin so the card's own
   padding is the border on all four sides. */
.card > :last-child { margin-bottom: 0; }
.card .empty { padding: 22px 0; }
.grid { display: grid; gap: var(--gap); margin-bottom: var(--gap); }
/* auto-ROWS 1fr, not just auto-fill columns. Nine tiles wrap to two rows, and
   a two-line sub-label on one tile in the second row made that whole row 18px
   taller than the first — two bands of tiles that should read as one block,
   visibly out of step. 1fr makes every row the height of the tallest tile on
   the page, so the grid is square whatever the labels say. */
/* THE TILE ROW SITS ON THE CANVAS, AND THE COUNT PICKS THE COLUMNS.

   Three rules were tried here and each failed a different page:
   - `repeat(auto-fill, minmax(200px, 1fr))` kept the tracks it made room for
     even with nothing in them: three tiles on Wages and Payroll sat in five
     214px tracks, two of them empty — 460px of bare canvas at the row's end.
   - `auto-fit` fixes that, but not nine tiles, which fill all five tracks on
     the first row and leave the second a tile short of the edge.
   - A wrapping flex row fills every row, but it stretches whatever lands on
     the last line: the Leave employee tab's six tiles went 5 + 1 with
     "Remaining" drawn 1232px wide, and a timesheet's one tile filled the page.

   So the column count is chosen from the NUMBER of tiles, and every tile is
   one track of the same grid. 3, 6 and 9 go three across — the Dashboard's
   nine then land on exactly the edges of the three-card bands above them.
   Five go five across, so the Punch and Reports headline rows stay one row.
   Everything else goes four across, and one or two tiles take a track each and
   stop: a lone figure in a 1232px box was the largest empty space on its page.
   :has() does the counting — `:last-child:nth-child(n)` is true of exactly one
   n — and the rules only set a variable, so the narrow-screen fallback below
   can override the columns without out-ranking five selectors.

   grid-auto-rows: 1fr comes back with the grid: every row is as tall as the
   tallest tile in the block, so two rows of tiles read as one block. */
.grid.stats { grid-template-columns: repeat(var(--stat-cols, 4), minmax(0, 1fr)); grid-auto-rows: 1fr; }
.grid.stats:has(> :last-child:nth-child(3)),
.grid.stats:has(> :last-child:nth-child(6)),
.grid.stats:has(> :last-child:nth-child(9)) { --stat-cols: 3; }
.grid.stats:has(> :last-child:nth-child(5)) { --stat-cols: 5; }
/* Where five tiles can no longer hold a label on one line: two across for
   every count, then one on a phone. Predictable, and never narrower than a
   label needs. */
@media (max-width: 1100px) { .grid.stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .grid.stats { grid-template-columns: minmax(0, 1fr); } }
/* TWO columns, as the name says. This was `repeat(auto-fit, minmax(300px, 1fr))`
   — as many 300px columns as fit — which is three in any full-width card: the
   People edit form went 3 + 1, the Wages new-rate form put its last field in
   a 740px hole, and Payroll's New period card dropped Period end onto its own
   row. A form asks for two columns because its fields pair up; the count is
   the design, not a function of the window. One column on a phone, where two
   150px tracks cannot hold a date. */
.grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (max-width: 600px) { .grid.two { grid-template-columns: minmax(0, 1fr); } }
/* Inside a grid the gap IS the spacing; a field keeping its own bottom margin
   on top of it was the "more space here than there" effect in the forms. */
.grid > .field { margin-bottom: 0; }

/* ---- .fieldgrid: fields that line up PART for part ------------------------
   Aligning the field BOXES across a row is only half the job. A field is a
   three-part stack — label, control, hint — and those parts stay level only
   while every label on the row is exactly one line long. The moment one wraps
   ("Weekend public-holiday top-up (hours)" on Leave, at any column narrower
   than its text), its control drops a line below its neighbours' and the row
   is out of level again — with the labels still perfectly aligned, which is
   what makes it look like a bug rather than a wrap.

   subgrid is the fix and the only one that does not involve guessing a label
   height: the field hands its three rows back to the parent grid, so every
   label on a row shares one band, every control the next, every hint the
   last, each band as tall as its tallest member. row-gap:0 is required —
   without it the parent's row gap is inserted between the three bands too,
   and a field opens up to three times its height.

   Opt-in by putting .fieldgrid on the grid CONTAINER. Fields may hold two
   parts or three; a two-part field simply leaves the hint band empty. */
@supports (grid-template-rows: subgrid) {
  .fieldgrid > .field {
    display: grid;
    grid-template-rows: subgrid;
    grid-row: span 3;
    row-gap: 0;
  }
}

.stat {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 16px 18px;
  /* Every tile carries the rail; the tone only colours it. Same geometry on
     every tile, so a row of mixed tones still lines up to the pixel. */
  border-left: 3px solid var(--line);
  min-height: 106px;
}
.stat .label {
  font-size: var(--fs-sm); color: var(--muted); font-weight: 600;
  margin-bottom: 8px; letter-spacing: 0;
}
.stat .value {
  font-size: var(--fs-2xl); font-weight: 700; line-height: 1.1; color: var(--ink);
  letter-spacing: -.01em; font-variant-numeric: tabular-nums;
}
.stat .sub { font-size: var(--fs-xs); color: var(--muted); margin-top: 6px; }
/* Both tone vocabularies, deliberately — see the note at .badge below. */
.stat.alert, .stat.bad { border-left-color: var(--bad); }
.stat.attention, .stat.warn { border-left-color: var(--warn); }
.stat.good, .stat.ok { border-left-color: var(--ok); }
.stat.alert .value, .stat.bad .value { color: var(--bad); }
.stat.attention .value, .stat.warn .value { color: var(--warn); }
.stat.good .value, .stat.ok .value { color: var(--ok); }

/* ---------- tabs ------------------------------------------------------------ */
/* One segmented control for every page that has views (Punch, Wages). The
   active segment gets a white card face via inset ring, not a border, so
   switching never shifts the row by a pixel. */
.tabs {
  display: inline-flex; gap: 2px; flex-wrap: wrap;
  padding: 3px;
  background: var(--line-soft);
  border-radius: 10px;
  margin-bottom: 16px;
}
.tabs a {
  padding: 6px 13px;
  border-radius: 8px;
  color: var(--ink-soft); text-decoration: none;
  font-size: var(--fs-md); font-weight: 600;
}
.tabs a:hover { color: var(--ink); }
.tabs a.on {
  background: #fff; color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--line), 0 1px 2px rgba(20, 22, 28, .06);
}
.tabs a:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---------- tables ----------------------------------------------------------- */
.tablewrap { overflow-x: auto; }
table.data { width: 100%; border-collapse: collapse; font-size: var(--fs-md); }
table.data th, table.data td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line-soft); }
table.data th {
  font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); font-weight: 650; border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
/* Quiet zebra, the Timesheet Mobile way: on a 12-person board the stripe is
   what keeps the eye on one row across seven columns. Hover sits above it. */
table.data tbody tr:nth-child(even) { background: #fafaf6; }
table.data tbody tr:hover { background: var(--line-soft); }
/* The last row used to close with its own border a hair above the card's
   padding — the "space at the top but not at the bottom" look. The card's
   padding is the frame; the table ends where it ends. */
table.data tbody tr:last-child td { border-bottom: 0; }
/* A numeric column is right-aligned in BOTH halves of the table. UI.table has
   always stamped class="num" on the <th> as well as the <td>, but only the
   cell had a rule — so every money and hours column in the app ran its figures
   down the right edge under a heading pinned to the left. */
table.data th.num, table.data td.num { text-align: right; }
table.data td.num { font-variant-numeric: tabular-nums; }

.badge {
  display: inline-block; padding: 2px 9px; border-radius: 20px;
  font-size: var(--fs-xs); font-weight: 650; letter-spacing: .01em;
  white-space: nowrap;
}
.badge.ok, .badge.good { background: var(--ok-bg); color: var(--ok); }
.badge.warn, .badge.attention { background: var(--warn-bg); color: var(--warn); }
.badge.bad, .badge.alert { background: var(--bad-bg); color: var(--bad); }
.badge.info { background: var(--info-bg); color: var(--info); }
.badge.neutral { background: var(--line-soft); color: var(--ink-soft); }
/* The aliases above are not decoration. This file carries TWO tone vocabularies
   — .stat uses good/attention/alert, .badge uses ok/warn/bad — and handing one
   component the other's word is completely silent: the element keeps its
   geometry and simply loses its colour. It had already happened, to the
   'official' badge on Permits & TCN, which is the commonest badge on that page.
   Accepting both words in both vocabularies costs three selectors and makes the
   failure impossible. .stat carries the same aliases, up at its own rules. */

/* A short note at the FOOT of a card. */
.card-note {
  margin: 14px 0 0;
  font-size: var(--fs-sm); line-height: 1.5;
  color: var(--muted);
  max-width: 78ch;
}
.card-note b { color: var(--ink-soft); font-weight: 650; }

/* A checkbox and its words, as one row at the small control height. Six page
   modules had each drawn their own ("Fence rings" three times over) at three
   heights — 22, 32 and 40px — so the same tick sat level with its neighbours
   on one page and rode 9px high on the next. One rule; the page classes stay
   as aliases so their markup needs no change. .check-field is the same row
   standing in a form, with a field's 14px under it and room to wrap. */
.check, .dash-check, .punch-check, .st-check, .act-check, .payroll-check {
  display: inline-flex; gap: 8px; align-items: center;
  min-height: var(--ctl-h-sm);
  font-size: var(--fs-sm); font-weight: 400; color: var(--ink-soft);
  white-space: nowrap; cursor: pointer;
}
.check input, .dash-check input, .punch-check input, .st-check input, .act-check input, .payroll-check input {
  width: 16px; height: 16px; margin: 0;
}
.check-field { display: flex; margin-bottom: 14px; white-space: normal; }

/* A row of buttons under a form or a table: 8px apart, wrapping, 14px below
   what they act on. Reports had three joined by a literal space. */
.actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 14px; }

/* A bordered row inside a card — a filed document, a subject-rights request.
   People drew it three times, with two paddings and a radius of its own. */
.rowcard { border: 1px solid var(--line); border-radius: var(--radius-ctl); padding: 11px 12px; margin-bottom: 8px; }
.rowcard:last-child { margin-bottom: 0; }

/* A reference set in the mono face keeps the size of the heading it sits in:
   .mono's own 13px shrank the case reference on Reports inside a 16px title. */
h1 .mono, h2 .mono, h3 .mono { font-size: inherit; }

.empty { color: var(--muted); font-size: var(--fs-md); padding: 26px 22px; text-align: center; }
.muted { color: var(--muted); }
.mono { font-family: var(--mono); font-size: var(--fs-sm); }
.stub-note {
  background: var(--warn-bg); border: 1px solid #f0dcae; color: var(--warn);
  border-radius: var(--radius); padding: 14px 16px; font-size: var(--fs-md);
}

/* ---------- drawer ------------------------------------------------------------ */
/* Cosmetics only. Geometry (position:fixed, inset, z-index) stays INLINE in the
   page module on purpose: tests/admin-pages.test.ts greps rendered HTML for
   inline fixed positioning to catch overlays mounted inside #content, and that
   net only works while overlays declare their positioning where it can see it. */
.drawer-backdrop { background: rgba(19, 24, 35, .38); }
.drawer-panel {
  width: min(480px, 100%);
  background: var(--card);
  box-shadow: -16px 0 48px rgba(19, 24, 35, .22);
  overflow: auto;
  padding: 20px 22px;
}

/* ---------- toast & modal ------------------------------------------------------ */
.toast {
  position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(20px);
  background: var(--navy-900); color: #fff;
  padding: 10px 18px; border-radius: 9px; font-size: var(--fs-md);
  opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s;
  z-index: 100; max-width: 90vw;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast.bad { background: var(--bad); }
.toast.show.has-action { pointer-events: auto; display: flex; align-items: center; gap: 14px; }
.toast-action {
  background: transparent; border: 0; padding: 2px 4px; margin: -2px -4px -2px 0;
  color: #fff; font: inherit; font-weight: 700; text-decoration: underline; cursor: pointer;
}
.toast-action:focus-visible { outline: 2px solid #fff; outline-offset: 2px; border-radius: 4px; }

.modal-backdrop {
  position: fixed; inset: 0; background: rgba(19, 24, 35, .45);
  display: grid; place-items: center; padding: 20px; z-index: 90;
}
.modal {
  background: #fff; border-radius: 14px; padding: 22px;
  width: 100%; max-width: 480px; max-height: 88vh; overflow-y: auto;
  box-shadow: 0 24px 70px rgba(0, 0, 0, .3);
}
.modal h3 { margin: 0 0 14px; font-size: var(--fs-lg); font-weight: 650; }
.modal-actions { display: flex; gap: 9px; justify-content: flex-end; margin-top: 18px; }

/* ---------- responsive ---------------------------------------------------------- */
@media (max-width: 860px) {
  .burger { display: grid; }
  .sidebar {
    position: fixed; inset: 0 auto 0 0; z-index: 60;
    transform: translateX(-100%); transition: transform .2s;
    box-shadow: none;
  }
  .sidebar.open { transform: none; box-shadow: 0 0 60px rgba(0, 0, 0, .35); }
  /* The gutter tightens, and everything hung on it — the canvas, the bar, the
     back links — follows through the one token. The bar used to stay on 24px
     while the content dropped to 16, so on a phone the title and the search
     button were inset 8px further than every card under them. */
  :root { --gutter: 16px; }
  /* This carried a note saying 19px wraps "Punch & Timesheets" at 375px and
     17px does not. Measured 2026-08-10, that is no longer true and the size is
     not what decides it: at 375px the h1 is squeezed to 88px by the burger,
     the back button, the section chip and the search + company pill on the
     right, so the longest titles wrap at 14px just as they do at 19px. The
     topbar simply has more in it than it did when the note was written.
     --fs-lg is kept here because a smaller title costs nothing and buys back a
     line more often on mid-size phones — but fixing the wrap means taking
     something OUT of the bar on a phone, which is a product decision, not a
     type one. */
  .topbar h1 { font-size: var(--fs-lg); }
}

/* =============================================================================
   MOTION AND INTERACTION
   Ported from the employee app's motion vocabulary (mobile/src/motion.ts) so the
   two surfaces feel like one product. Three rules carried over:

     1. Motion answers a question — mostly "is this thing clickable?".
     2. Quiet by default: 90-180ms, decelerating.
     3. Reduce Motion is honoured. Everything below collapses to 0s at the end
        of this file, so the admin stays usable and simply stops moving.

   Only opacity, transform and colour are animated — the properties a browser can
   composite without re-laying-out a table of 40 employees on every frame.
   ============================================================================ */

:root {
  --ease: cubic-bezier(.16, 1, .3, 1);
  --dur-instant: 90ms;
  --dur-quick: 180ms;
  /* The one a bar or a segment grows over: long enough to read as growth
     rather than a flicker, short enough that nine of them are not a wait. */
  --dur-settle: 260ms;
  --shadow-lift: 0 6px 18px rgba(20, 22, 28, .10);
}

/* A KPI block that leads somewhere. It has to LOOK clickable before it is
   hovered, or the affordance only exists for people already using a mouse. */
.stat-link {
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  position: relative;
  /* A stat block may be a <button> (a filter with no URL of its own), and a
     button brings its own font, centred text and intrinsic width. Reset those
     so the two forms are pixel-identical. */
  font: inherit;
  text-align: left;
  width: 100%;
  /* And a button also centres its content VERTICALLY, which `display: block`
     does not switch off — that is button layout, not block layout. Every tile
     in a row is stretched to the tallest (grid-auto-rows: 1fr), so on People
     the one tile that is a button sat 12px lower than its three neighbours:
     label, number and "View →" all off the row's baselines. Making the button
     a flex column hands layout back to normal flow, and the two forms are
     pixel-identical again. Safe for .stat-donut, which is always a <div>. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  transition: transform var(--dur-quick) var(--ease),
              box-shadow var(--dur-quick) var(--ease),
              border-color var(--dur-quick) var(--ease);
}
.stat-link:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-lift);
  border-color: var(--navy);
}
.stat-link:active { transform: translateY(0) scale(.995); transition-duration: var(--dur-instant); }
.stat-link:focus-visible { outline: 2px solid var(--navy); outline-offset: 2px; }

/* The "View →" hint: present in the layout at all times so a hover never
   reflows the card, revealed on hover and always shown to keyboard users. */
.stat-go {
  font-size: var(--fs-xs); font-weight: 700; color: var(--navy);
  /* On the tile's FLOOR, not 8px under whatever the tile happens to end
     with. Every tile in a row is stretched to the tallest, so a sub-line on
     one tile (Notices: "Receipt written at publish" on one of four) put its
     hint 24px below its neighbours'. margin-top:auto works because a linked
     tile is a flex column (.stat-link). */
  margin-top: auto; padding-top: 8px; opacity: 0;
  transition: opacity var(--dur-quick) var(--ease);
}
.stat-link:hover .stat-go,
.stat-link:focus-visible .stat-go { opacity: 1; }

/* A link inside a table cell is a 17px-tall text node, so the thing you aim at
   is much smaller than the cell it sits in. Filling the cell's line box makes
   the target match what the eye reads as the target — without changing the
   type size, the row height, or the table's rhythm. */
table.data td a,
table.data th a {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
}

/* Rows and cards that open something. */
tr.row-link { cursor: pointer; transition: background-color var(--dur-instant) var(--ease); }
tr.row-link:hover { background: var(--line-soft); }

.card { transition: box-shadow var(--dur-quick) var(--ease); }

/* Buttons gain a press, so a tap is felt as well as seen. */
.btn { transition: transform var(--dur-instant) var(--ease), filter var(--dur-instant) var(--ease); }
.btn:active { transform: scale(.98); }

/* Page entrance: content fades up 6px. Cheap, and it gives the SPA a sense of
   arriving somewhere rather than blinking.

   CONSEQUENCE, and it is not obvious. Interpolating a transform to `none`
   does not interpolate to the keyword: css-transforms-1 replaces a `none`
   endpoint with the equivalent identity function list, so `to` here really
   means translateY(0px). `both` then keeps that filling forever once the
   animation finishes, and the computed transform of every direct child of
   #content settles at matrix(1,0,0,1,0,0) — an identity matrix, but NOT `none`.
   Any transform other than `none` makes an element the containing block for its
   position:fixed descendants. So a page module that renders an overlay into
   #content gets it sized against a child box rather than the viewport; the
   People drawer shipped that way and rendered as a 460x40 sliver below the
   fold (measured identically in Blink, WebKit and Gecko — this is the spec
   working as written, not a browser bug).

   DO NOT try to fix that by forcing `transform: none` on these children. The
   keyframe already ends at `none` and still yields a matrix, and Blink keeps
   containing-block status for an element with a transform animation even when
   an !important declaration makes the computed value genuinely `none` — so
   that "fix" tests clean in Safari and stays broken in Chrome. Overlays mount
   on #drawerRoot / #modalRoot at body level instead — index.html says so, and
   tests/admin-pages.test.ts fails the build if a page module puts
   position:fixed back inside #content. */
@keyframes admin-enter { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
#content > * { animation: admin-enter var(--dur-quick) var(--ease) both; }

/* A BACKGROUND REDRAW (app.js route({ quiet: true }), the Dashboard's and the
   Punch board's minute refresh) repaints a page the reader is already looking
   at, so nothing on it may arrive again: every card fading up and every bar
   regrowing from zero once a minute is exactly the flicker this exists to stop.

   A negative delay longer than any duration, NOT `animation: none`. Each
   animation still exists and simply starts finished, filling with its last
   keyframe — so the very first frame is the settled one, and the element keeps
   the same resolved transform matrix an entrance that played would have left.
   `none` would drop that matrix, and with it the stacking context and
   containing block every card has today (the note above): the map's Leaflet
   panes (z-index 400) would then be free to paint over the sticky topbar
   (z-index 20). Nor a near-zero duration: that animation still begins at
   `from`, so its first frame can be a whole page at opacity 0. */
#content.redraw > *,
#content.redraw .chart-bar,
#content.redraw .bd-seg { animation-delay: -10s; }

/* Overlays arrive from where they act: the drawer slides in from the edge it
   lives on, the modal settles down onto the page. Safe here because neither
   ever contains a position:fixed descendant of its own — and the drawer's
   geometry stays inline (see the drawer note above), so these animate
   transform/opacity only. */
@keyframes drawer-in { from { opacity: 0; transform: translateX(24px); } to { opacity: 1; transform: none; } }
.drawer-panel { animation: drawer-in var(--dur-quick) var(--ease) both; }
@keyframes overlay-fade { from { opacity: 0; } to { opacity: 1; } }
.drawer-backdrop, .modal-backdrop { animation: overlay-fade var(--dur-quick) var(--ease) both; }
@keyframes modal-in { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: none; } }
.modal { animation: modal-in var(--dur-quick) var(--ease) both; }

/* Skeletons instead of spinners: the shape of the answer, not a shrug. */
@keyframes admin-shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }
.skeleton {
  border-radius: 8px;
  background: linear-gradient(90deg, #ecece8 25%, #f6f6f3 37%, #ecece8 63%);
  background-size: 300% 100%;
  animation: admin-shimmer 1.3s ease-in-out infinite;
}
.skeleton.line { height: 12px; margin: 8px 0; }
/* The height of a linked tile (label, figure, the "View →" line): the skeleton
   was 86px against tiles of 116px, so a page jumped 30px when its numbers
   landed. */
.skeleton.stat { height: 116px; }

/* Some staff get motion sickness, and iOS/macOS expose the preference. Nothing
   here is load-bearing, so the honest response is to stop moving entirely. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .stat-go { opacity: 1; }
}

/* ============================================================================
   CHARTS
   ----------------------------------------------------------------------------
   Flexbox bars, no SVG and no library. Height comes from a --h custom property
   set inline per column, which is the only per-datum value; everything else is
   a class, so the whole chart is themeable from here.
   ========================================================================== */

.chart {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 132px;
  margin-top: 4px;
}
.chart-col {
  flex: 1 1 0;
  /* Yoga's web cousin: a flex child will not shrink below its content unless
     told, and the axis label is wider than a 14th of the card. */
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  height: 100%;
}
.chart-track {
  flex: 1;
  display: flex;
  align-items: flex-end;
  background: var(--line-soft);
  border-radius: 5px;
  /* The anchor for .chart-tip, and deliberately NOT a clipping box. This used to
     be `overflow: hidden` to round the bar's bottom corners into the track, and
     that one declaration is what made the tooltips invisible: a tip is far wider
     than one column, so it was cropped to a ~28px sliver of its own first word.
     The clip still has to happen — it just moved inwards, to .chart-clip, so the
     tip can escape it. */
  position: relative;
}
/* The bar's rounded-corner mask, and nothing else. It is exactly the track's box
   with the track's radius, so the bar renders pixel-for-pixel as it did when the
   track itself clipped.

   It has to be a separate element. Giving the bar its own `border-radius: 5px`
   and dropping the clip entirely LOOKS equivalent and is not: when the two radii
   on one side add up to more than that side's length, CSS Backgrounds 3 §5.5
   scales ALL of an element's radii by f = side ÷ sum. barChart floors a non-zero
   column at 2% (ui.js), which is 2.2px in this 112px track, so f = 0.22 and the
   bar's corners collapse to ~1.1px while the track keeps its full 5px. The bar's
   squarer corners then paint into the arc the track leaves transparent — a navy
   nub ~2.9px wide on the white card, on both bottom corners of every small bar.
   Measured in Blink; the old clip removed it, and so does this one. */
.chart-clip {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: flex-end;
  border-radius: 5px;
  overflow: hidden;
}
.chart-bar {
  width: 100%;
  height: var(--h, 0%);
  background: var(--navy);
  border-radius: 5px 5px 0 0;
  position: relative;
  /* Bars grow from the baseline on first paint. transform-origin is what makes
     it a growth rather than a slide, and scaleY is compositor-only. */
  transform-origin: bottom;
  animation: chart-grow var(--dur-settle) var(--ease) both;
  transition: filter var(--dur-instant) var(--ease);
}
@keyframes chart-grow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
.chart-col.attention .chart-bar { background: var(--warn); }
.chart-col.alert .chart-bar { background: var(--bad); }
.chart-x {
  font-size: var(--fs-2xs);
  font-weight: 650;
  color: var(--muted);
  text-align: center;
  margin-top: 6px;
  height: 14px;
  white-space: nowrap;
  overflow: hidden;
}

/* The tooltip. A title attribute would do, but it waits a second and cannot be
   styled; this appears immediately, which is what makes the chart feel live.

   It is positioned against .chart-track, not against .chart-bar, and that is
   load-bearing twice over. .chart-bar animates `transform` and fills with
   `both`, so its computed transform settles at a matrix and stays there — the
   same trap the note above @keyframes admin-enter documents for #content, and
   the same reason forcing `transform: none` is no way out of it here either.
   A bar is therefore a stacking context AND a containing block, and a tip
   parented to one pays twice: it (a) rides the scaleY growth and renders as a
   flat 0px-tall line for the whole entrance animation, and (b) can only ever
   paint inside its own bar's stacking context, so a wide tip on column 3 is
   overpainted by column 4's bar. As a sibling it escapes both — the bar
   animates alone, and every tip shares one stacking context where its z-index
   actually ranks against the other columns.

   `bottom: var(--h)` is the trick that keeps this CSS-only. --h is the bar's
   height as a percentage of the track, set inline per column by ui.js; a
   percentage `bottom` on a child of the track resolves against that same track
   height, so the tip's lower edge lands exactly on the bar's top edge whatever
   the value is. No measuring, no JS, no second source of truth. */
.chart-tip {
  position: absolute;
  bottom: var(--h, 0%);
  margin-bottom: 6px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--ink);
  color: #fff;
  font-size: var(--fs-2xs);
  font-weight: 600;
  line-height: 1.3;
  padding: 5px 8px;
  border-radius: 6px;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-instant) var(--ease);
  z-index: 3;
}
.chart-col:hover .chart-bar { filter: brightness(1.18); }
.chart-col:hover .chart-tip { opacity: 1; }
/* The first and last columns' tooltips would otherwise run off the card. Between
   the breakpoint and ~1200px two or three of their neighbours still spill a
   little past the card edge — measured worst 18px at an 880px viewport — but
   that lands in .content's 22px gutter, so it stays on screen and raises no
   horizontal scrollbar. A floating tip overlapping its card is what tooltips
   do; clamping more columns would only mean aiming them at the wrong bar. */
.chart-col:first-child .chart-tip { left: 0; transform: none; }
.chart-col:last-child .chart-tip { left: auto; right: 0; transform: none; }

/* On a phone the same chart is ~305px wide, so a column of the 30-day leave
   chart is 6.3px. A tooltip is readable at ~125px or it is not a tooltip, which
   makes it twenty times wider than the thing it points at: centre it on the
   column and a quarter of them hang off the card (measured at 375px: columns
   1-4 and 25-28, worst overshoot 31px). Nudging the ends is no answer either —
   there are only 21px to the left of column 2 to nudge into.
   So below the layout breakpoint the tip stops trying to point and becomes a
   caption centred over the whole chart, which always fits. Nothing is lost: the
   hover brightness on the bar is what identifies the column, and it stays. */
@media (max-width: 860px) {
  .chart { position: relative; }
  /* Static on purpose — it hands the tip's containing block up to .chart, so
     `left: 50%` centres over the chart rather than over a 6px column. */
  .chart-track { position: static; }
  .chart-tip,
  .chart-col:first-child .chart-tip,
  .chart-col:last-child .chart-tip {
    left: 50%;
    right: auto;
    bottom: 100%;
    margin-bottom: 8px;
    transform: translateX(-50%);
    max-width: 100%;
    white-space: normal;
    text-align: center;
  }
}

/* The whole chart is one link. It must not look like body text that happens to
   be blue, so the underline goes and the affordance is the CTA plus the lift. */
/* A column that is itself a link. .chart-col already carries the flex layout —
   this only strips the anchor's own decoration and gives it a focus ring, so a
   linked chart and a plain one are pixel-identical until you tab into it. */
a.chart-col { text-decoration: none; color: inherit; }
a.chart-col:focus-visible {
  outline: 2px solid var(--navy);
  outline-offset: 2px;
  border-radius: 6px;
}
a.chart-cta-link { text-decoration: none; }
a.chart-cta-link:hover { text-decoration: underline; }
a.chart-link {
  display: block;
  text-decoration: none;
  color: inherit;
  border-radius: 10px;
  padding: 4px;
  margin: -4px;
  transition: background-color var(--dur-quick) var(--ease);
}
a.chart-link:hover { background: var(--line-soft); }
a.chart-link:focus-visible { outline: 2px solid var(--navy); outline-offset: 2px; }
.chart-cta {
  display: block;
  margin-top: 10px;
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--navy);
}
/* 14px, the standard card-title gap, NOT the 2px this used to carry. The 2px
   was only ever correct while a description paragraph sat between the title and
   the chart and supplied the other 12px itself; with those paragraphs gone the
   title was left sitting 2px above the bars. The .muted rule below is kept for
   any chart card that still runs a line of its own under the heading. */
.chart-card h2 { margin-bottom: 14px; }
.chart-card h2 + .muted { margin-top: -10px; margin-bottom: 12px; }

/* ---- proportion bar + legend ---- */
.bd-bar {
  display: flex;
  height: 14px;
  border-radius: 7px;
  overflow: hidden;
  background: var(--line-soft);
}
.bd-seg {
  background: var(--navy);
  /* Same growth idea as the bars, along the other axis. */
  transform-origin: left;
  animation: bd-grow var(--dur-settle) var(--ease) both;
}
@keyframes bd-grow { from { transform: scaleX(0); } to { transform: scaleX(1); } }
.bd-seg.attention { background: var(--warn); }
.bd-seg.alert { background: var(--bad); }
.bd-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  margin-top: 12px;
}
.bd-item {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  /* The 32px floor the table links got, for the same reason: the legend is a
     row of links and a 15px text node is not a target. */
  min-height: 32px;
  font-size: var(--fs-sm);
  color: inherit;
  text-decoration: none;
  border-radius: 6px;
  padding: 0 6px;
  margin: 0 -6px;
  transition: background-color var(--dur-instant) var(--ease);
}
a.bd-item:hover { background: var(--line-soft); }
a.bd-item:focus-visible { outline: 2px solid var(--navy); outline-offset: 1px; }
.bd-dot {
  width: 10px; height: 10px; border-radius: 3px;
  background: var(--navy); flex: none;
}
.bd-dot.attention { background: var(--warn); }
.bd-dot.alert { background: var(--bad); }
.bd-label { color: var(--muted); }
.bd-count { font-variant-numeric: tabular-nums; }

/* ---------- multi-select (filter bars) ------------------------------------- */
/* The Timesheet Mobile "All Workers" / "All Sites" boxes (UI.multiSelect). The
   button is drawn as a control — same height, border and radius as a select —
   so a filter row stays level; the list floats over the card below it. */
.ms { position: relative; display: flex; flex-direction: column; min-width: 0; }
.ms-label {
  font-size: var(--fs-sm); font-weight: 600; color: var(--ink-soft);
  margin-bottom: var(--label-gap);
}
.ms-btn {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; height: var(--ctl-h); padding: 0 11px;
  border: 1px solid var(--line); border-radius: var(--radius-ctl);
  background: #fff; color: var(--ink);
  font: inherit; text-align: left; cursor: pointer;
}
.ms-btn:hover { border-color: var(--muted); }
.ms-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
.ms-btn[aria-expanded=true] { border-color: var(--accent); }
.ms-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A live selection reads as a filter, not as a default. */
.ms-text.ms-some { font-weight: 600; }
.ms-caret { flex: none; width: 14px; height: 14px; color: var(--muted); }
.ms-pop {
  position: absolute; z-index: 30; top: calc(100% + 4px); left: 0;
  min-width: 100%; width: max-content; max-width: min(380px, 90vw);
  display: flex; flex-direction: column;
  padding: 8px;
  background: var(--card); border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 12px 32px rgba(20, 22, 28, .12);
}
.ms-pop[hidden] { display: none; }
/* While open the panel is MOVED to #popRoot (wireMultiSelect) and positioned
   from its button by inline style, because every `#content > *` is its own
   stacking context and a panel left in its card paints under the cards below.
   Above the modal backdrop (90), under the toast (100). */
#popRoot .ms-pop { z-index: 95; }
/* The search box is a small control inside a popover; the shared 40px rule
   would make it taller than the rows it filters. */
.ms-pop input[type=search] { height: var(--ctl-h-sm); font-size: var(--fs-sm); }
.ms-tools { display: flex; gap: 4px; margin: 6px 0 2px; }
.ms-tool {
  font: inherit; font-size: var(--fs-xs); font-weight: 600; color: var(--accent);
  background: none; border: 0; padding: 4px 6px; border-radius: 6px; cursor: pointer;
}
.ms-tool:hover { background: var(--line-soft); }
.ms-tool:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ms-list { max-height: 264px; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; }
.ms-opt {
  display: flex; align-items: center; gap: 9px;
  min-height: 32px; padding: 5px 8px; border-radius: 7px;
  font-size: var(--fs-sm); font-weight: 400; color: var(--ink); cursor: pointer;
}
.ms-opt:hover { background: var(--line-soft); }
.ms-opt[hidden] { display: none; }
.ms-opt input { margin: 0; }
.ms-opt-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.ms-opt-text small { font-size: var(--fs-xs); color: var(--muted); }
.ms-empty { margin: 0; padding: 8px; font-size: var(--fs-sm); color: var(--muted); }

/* ---- the one-of-N menu (UI.selectMenu) -------------------------------------
   The app's own <select>, for the pickers where the browser's own menu could
   not be made to behave: on macOS Chrome positions a native select's popup so
   the SELECTED option sits over the button, so the same picker opened low while
   it read "All sites" and high once a site further down was chosen — the
   "sometimes lower, sometimes further up" the owner reported. It wears the
   multi-select's clothes exactly, so the two read as one control; the only
   differences are a row that is a <button> rather than a checkbox, and a tick
   on the current choice. */
.ms-opt-one {
  width: 100%; text-align: left; background: none; border: 0;
  font: inherit; font-size: var(--fs-sm);
  justify-content: space-between;
}
.ms-opt-one.on { font-weight: 600; }
/* Present in the layout whether or not it is showing, so choosing an option
   never shifts the row under the pointer. */
.ms-tick { width: 15px; height: 15px; flex: none; color: var(--navy); opacity: 0; }
.ms-opt-one.on .ms-tick { opacity: 1; }
/* Keyboard focus lives on the ACTIVE row, not on a focus ring per option: the
   panel keeps DOM focus (or its search box does) and moves this marker with the
   arrow keys, which is how a listbox is meant to behave. The RING only appears
   once somebody has actually pressed a key (.kbd, set in wireSelectMenu) —
   drawn on a panel opened with the mouse, a ring around the current row reads
   as a text field rather than as a cursor. */
.ms-opt-one.active { background: var(--line-soft); }
.ms-pop.kbd .ms-opt-one.active { box-shadow: inset 0 0 0 2px var(--accent); }
.ms-pop:focus, .ms-list:focus { outline: none; }

/* ============================================================================
   GLANCE COMPONENTS — the Timesheet Mobile borrowings (2026-08-07).
   Coloured-initials avatars, the live dot, the geofence globe, the donut and
   the worksite map. All drawn locally: no icon font, no tile server, no
   library — the CSP stays exactly as strict as it was.
   ========================================================================== */

/* ---- coloured-initials avatar (hue comes inline from UI.avatar) ---- */
.av {
  width: 30px; height: 30px; flex: none;
  display: inline-grid; place-items: center;
  border-radius: 50%;
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .02em;
  vertical-align: middle;
}
/* Name + avatar + sub-line as one table-cell unit. */
.emp-cell { display: flex; align-items: center; gap: 10px; min-width: 0; }
.emp-cell > div { min-width: 0; }

/* ---- the "on the clock right now" pulse ---- */
.live-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  background: var(--ok); margin-right: 6px;
  animation: live-pulse 2s var(--ease) infinite;
}
@keyframes live-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(21, 122, 83, .35); }
  50% { box-shadow: 0 0 0 4px rgba(21, 122, 83, 0); }
}

/* ---- the geofence globe: one glyph that answers "was this punch where it
   said it was?" — green at site, amber off site, grey when there is nothing
   to check. A BUTTON: clicking it opens the honest detail (site, method,
   rounded distance), so the colour is a summary, not the whole answer. ---- */
.geo {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; flex: none;
  border: 0; border-radius: 50%;
  background: var(--ok-bg); color: var(--ok);
  cursor: pointer; padding: 0;
  vertical-align: middle;
  transition: transform var(--dur-instant) var(--ease);
}
.geo svg { width: 17px; height: 17px; display: block; }
.geo.warn { background: var(--warn-bg); color: var(--warn); }
.geo.off { background: var(--line-soft); color: var(--muted); }
.geo:hover { transform: scale(1.12); }
.geo:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Globe + its method badge as one unit in a table cell. */
.geo-cell { display: inline-flex; align-items: center; gap: 8px; }

/* The distance meter inside the globe's modal: a band from the site outwards.
   Green is inside the fence, amber is near, red is the far cap; the needle is
   the punch's ROUNDED distance — the only granularity that exists anywhere. */
.geo-meter { position: relative; height: 10px; border-radius: 5px; overflow: hidden; margin: 10px 0 4px; display: flex; }
.geo-meter .z-in { background: var(--ok); }
.geo-meter .z-near { background: #e8c76a; }
.geo-meter .z-far { background: #d98b84; flex: 1; }
.geo-needle {
  position: absolute; top: -3px; width: 2.5px; height: 16px;
  background: var(--ink); border-radius: 2px;
  transform: translateX(-50%);
}
.geo-scale { display: flex; justify-content: space-between; font-size: var(--fs-2xs); color: var(--muted); }
.geo-rows { margin: 0 0 12px; padding: 0; list-style: none; }
.geo-rows li { display: flex; justify-content: space-between; gap: 14px; padding: 7px 0; border-bottom: 1px solid var(--line-soft); font-size: var(--fs-md); }
.geo-rows li:last-child { border-bottom: 0; }
.geo-rows .k { color: var(--muted); flex: none; }

/* ---- the location panel (UI.locationPanel) ----------------------------------
   The globe's destination: one frosted sheet carrying the worksite map, the
   written facts, and the rest of that day at that site.

   Glass is the FRAME, never the reading surface. The map, the facts and the
   list sit on solid --card inside it, so contrast never depends on whatever
   happens to be scrolled behind the panel. The admin is light-only by
   construction (no prefers-color-scheme anywhere in this file), so the tint is
   light; @supports gives browsers without backdrop-filter a plain white sheet
   rather than a translucent unreadable one.

   z-ladder: topbar 20 · sidebar 60 · modal 90 · toast 100 · gm-overlay 230 ·
   search 240 · HERE 250. It has to outrank the expanded map (.gm-overlay,
   230), not sit under it. */
.lp-backdrop {
  position: fixed; inset: 0; z-index: 250;
  background: rgba(19, 24, 35, .42);
  backdrop-filter: blur(16px) saturate(1.3);
  -webkit-backdrop-filter: blur(16px) saturate(1.3);
  display: grid; place-items: center; padding: 20px;
  animation: overlay-fade var(--dur-quick) var(--ease) both;
}
.lp-panel {
  width: min(1060px, 100%); max-height: min(90vh, 920px);
  display: grid; grid-template-rows: auto minmax(0, 1fr) auto;
  background: rgba(255, 255, 255, .74);
  backdrop-filter: blur(30px) saturate(1.8);
  -webkit-backdrop-filter: blur(30px) saturate(1.8);
  border: 1px solid rgba(255, 255, 255, .65);
  border-radius: 18px;
  box-shadow: 0 24px 70px rgba(0, 0, 0, .3);
  overflow: hidden;
  animation: modal-in var(--dur-quick) var(--ease) both;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .lp-panel { background: #fff; }
  .lp-backdrop { background: rgba(19, 24, 35, .55); }
}
body.lp-noscroll { overflow: hidden; }

.lp-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 18px 20px 14px;
  border-bottom: 1px solid rgba(255, 255, 255, .55);
}
.lp-head h2 { margin: 0; font-size: var(--fs-lg); font-weight: 650; }
.lp-sub { display: flex; flex-wrap: wrap; gap: 6px 12px; margin: 5px 0 0; font-size: var(--fs-sm); color: var(--ink-soft); }
.lp-chip {
  padding: 1px 9px; border-radius: 999px;
  background: rgba(255, 255, 255, .8); border: 1px solid var(--line);
  font-weight: 600; font-size: var(--fs-xs);
}
/* Clock in | Clock out: a timesheet row's two punches, one pressed. */
.lp-ends {
  display: inline-flex; flex: none; padding: 2px; gap: 2px;
  border: 1px solid var(--line); border-radius: 999px;
  background: rgba(255, 255, 255, .8);
}
.lp-end {
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 26px; padding: 0 11px;
  border: 0; border-radius: 999px; background: transparent;
  color: var(--ink-soft); font: inherit; font-size: var(--fs-xs); font-weight: 600;
  white-space: nowrap; cursor: pointer;
}
.lp-end:hover:not(:disabled):not(.on) { background: var(--line-soft); }
.lp-end.on { background: var(--navy); color: #fff; }
.lp-end:disabled { color: var(--muted); cursor: default; }
.lp-end:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.lp-end-when { font-variant-numeric: tabular-nums; font-weight: 500; }
.lp-head-side { display: flex; align-items: center; gap: 10px; flex: none; }
.lp-close {
  width: 32px; height: 32px; flex: none;
  border: 1px solid var(--line); border-radius: 50%;
  background: rgba(255, 255, 255, .85); color: var(--ink-soft);
  font-size: var(--fs-xl); line-height: 1; cursor: pointer;
}
.lp-close:hover { color: var(--ink); }
.lp-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.lp-body {
  display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(300px, 1fr);
  gap: 16px; padding: 16px 20px; overflow: auto;
}
.lp-mapcol { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
/* The map host is a solid card inside the glass, not a hole through it. */
.lp-map { background: var(--map-sea); box-shadow: var(--shadow); }
.lp-map.lp-map-none {
  height: auto; display: grid; place-items: center; padding: 26px 18px;
  background: var(--card); border: 1px solid var(--line);
}
.lp-map.lp-map-none .empty { margin: 0; text-align: center; }
.lp-caption { margin: 0; font-size: var(--fs-xs); color: var(--ink-soft); line-height: 1.5; }
.lp-side { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
.lp-facts, .lp-others {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 14px 16px;
  box-shadow: var(--shadow);
}
.lp-facts .geo-rows { margin: 0; }
.lp-others { display: flex; flex-direction: column; min-height: 0; }
.lp-others-head { margin: 0 0 8px; font-size: var(--fs-xs); font-weight: 650; color: var(--muted); text-transform: uppercase; letter-spacing: .03em; }
.lp-others-body { overflow-y: auto; max-height: 260px; margin: 0 -6px; padding: 0 6px; }
.lp-others-body .empty, .lp-loading { margin: 4px 0; font-size: var(--fs-sm); }
.lp-list { list-style: none; margin: 0; padding: 0; }
.lp-list li + li { border-top: 1px solid var(--line-soft); }
.lp-row {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 8px 8px; margin: 0 -8px; width: calc(100% + 16px);
  border: 0; border-radius: 8px; background: none; color: inherit;
  font: inherit; font-size: var(--fs-sm); text-align: left; cursor: pointer;
}
.lp-row:hover { background: var(--line-soft); }
.lp-row.on { background: var(--info-bg); }
.lp-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.lp-dot { width: 8px; height: 8px; flex: none; border-radius: 50%; background: var(--ok); }
.lp-dot.warn { background: var(--warn); }
.lp-dot.off { background: var(--muted); }
.lp-row-when { font-variant-numeric: tabular-nums; font-weight: 600; flex: none; }
.lp-row-dir { color: var(--muted); font-size: var(--fs-xs); flex: none; width: 22px; }
.lp-row-who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lp-row-far { color: var(--ink-soft); font-size: var(--fs-xs); flex: none; }
/* What the list leaves out, said under the list rather than implied by its
   heading: manager scope, current employees, this worksite only. */
.lp-foot-note { margin: 9px 0 0; font-size: var(--fs-xs); color: var(--muted); }

/* Screen-reader-only: the panel re-targets in place, so the change has to be
   announced or a non-sighted user gets no signal that anything happened. */
.lp-live {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

@media (max-width: 900px) {
  .lp-backdrop { padding: 0; }
  .lp-panel { width: 100%; max-height: 100vh; height: 100vh; border-radius: 0; border: 0; }
  .lp-body { grid-template-columns: minmax(0, 1fr); }
  /* Written at .lp-panel strength on purpose: .geomap.gm-tall (min(560px,64vh))
     is declared LATER in this file and would win on source order otherwise,
     leaving a 560px map above the facts on a phone-width screen. */
  .lp-panel .lp-map.gm-tall { height: 300px; }
  .lp-others-body { max-height: none; }
}

/* ---- donut (value arrives as --deg from UI.donut) ---- */
.donut {
  --donut-c: var(--navy);
  width: 64px; height: 64px; flex: none;
  border-radius: 50%;
  display: inline-grid; place-items: center;
  background: conic-gradient(var(--donut-c) var(--deg), var(--line-soft) 0);
}
.donut::before {
  content: ""; grid-area: 1 / 1;
  width: 46px; height: 46px; border-radius: 50%;
  background: var(--card);
}
.donut-center {
  grid-area: 1 / 1; z-index: 1;
  font-size: var(--fs-sm); font-weight: 700; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.donut.good { --donut-c: var(--ok); }
.donut.attention { --donut-c: var(--warn); }
.donut.alert { --donut-c: var(--bad); }
/* A stat tile that leads with the ring instead of a bare number. Its words are
   TOP-aligned like every other tile's — centred against a tile stretched to the
   row's height, its label sat 16px below the labels of the four tiles beside it
   on the Punch board. Only the ring is centred, against the tile, where it reads
   as placed rather than dropped; and the label and sub keep the shared 8px/6px
   steps instead of a 4px pair of their own. */
.stat.stat-donut { display: flex; align-items: flex-start; gap: 14px; }
.stat.stat-donut .donut { align-self: center; }

/* ---- worksite map ---- */
/* Capped: on a wide monitor an uncapped SVG scales its 12px labels to 20px
   billboard type. 900px keeps the map generous and the type near actual size. */
.sitemap svg { display: block; width: 100%; max-width: 900px; height: auto; margin: 0 auto; }
.map-sea { fill: var(--map-sea); }
.map-land { fill: var(--map-land); stroke: var(--map-coast); stroke-width: 1.2; stroke-linejoin: round; }
.map-ring { fill: rgba(47, 111, 237, .07); stroke: var(--accent); stroke-width: 1; stroke-dasharray: 4 3; }
.map-pin .pin-body { fill: var(--muted); stroke: #fff; stroke-width: 1.6; }
.map-pin.on .pin-body { fill: var(--navy); }
.map-pin .pin-eye { fill: #fff; }
.map-pin .pin-count-bg { fill: var(--ok); stroke: #fff; stroke-width: 1.6; }
.map-pin .pin-count {
  fill: #fff; font-size: var(--fs-2xs); font-weight: 700; text-anchor: middle;
  font-family: var(--sans);
}
.map-pin .pin-label {
  fill: var(--ink-soft); font-size: var(--fs-xs); font-weight: 600;
  font-family: var(--sans);
  paint-order: stroke; stroke: #fff; stroke-width: 3px; stroke-linejoin: round;
}
.sitemap-foot {
  display: flex; flex-wrap: wrap; gap: 4px 18px;
  margin-top: 10px; font-size: var(--fs-sm); color: var(--ink-soft);
}

/* ---- interactive worksite map (UI.geoMap — Leaflet, vendored) ---- */
/* Same invariant as the SVG map above: pins are worksites, bubbles are counts.
   The host element carries the size; Leaflet fills it. */
.geomap {
  position: relative;
  height: 320px;
  border-radius: 10px;
  overflow: hidden;
  background: var(--map-sea);
}
.geomap.gm-tall { height: min(560px, 64vh); }
.geomap.gm-pick { height: 280px; }
/* Expanded: the map takes the screen, Esc, the × or the backdrop hands it back.
   It cannot do that where it stands. Every direct child of #content keeps a
   resolved transform matrix (see the admin-enter note above), so a
   position:fixed map inside a card is sized against the CARD — which is what
   "expand" used to do: break out of the grid column, gain almost no height, and
   scroll away with the page. ui.js therefore MOVES the .geomap node into
   #geoFullRoot at body level and puts it back on close.

   The backdrop carries the z-index, which also makes it the stacking context
   that traps Leaflet's own numbers (panes 200-700, its controls 800, .gm-ctl
   810). 230 keeps the ladder above intact: the search palette (240) and the
   location panel (250) can both be opened over an expanded map and must land on
   top of it. */
.gm-overlay {
  position: fixed;
  inset: 0;
  z-index: 230;
  background: rgba(19, 24, 35, .55);
  animation: overlay-fade var(--dur-quick) var(--ease) both;
}
/* Declared AFTER .gm-tall and .gm-pick: all three are specificity 0,2,0, so
   source order is what decides the height. */
.geomap.gm-expanded {
  position: absolute;
  inset: max(14px, 2vh) max(14px, 2vw);
  height: auto;
  border-radius: 12px;
  box-shadow: 0 24px 80px rgba(0, 0, 0, .4);
}
/* The slot the map left behind, so the card does not collapse and reflow the
   page underneath while it is away. */
.gm-slot { height: 0; }
body.gm-noscroll { overflow: hidden; }
.geomap .leaflet-container {
  width: 100%; height: 100%;
  background: var(--map-sea);
  font-family: var(--sans);
}
.geomap .leaflet-popup-content { font-family: var(--sans); font-size: var(--fs-sm); line-height: 1.45; }
.geomap.gm-picking .leaflet-container { cursor: crosshair; }
.gm-pop-muted { color: var(--muted); font-size: var(--fs-xs); }

.gm-ctl { position: absolute; top: 10px; right: 10px; z-index: 810; display: flex; gap: 5px; }
.gm-btn {
  font: inherit; font-size: var(--fs-xs); font-weight: 600;
  padding: 5px 11px; border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, .94); color: var(--ink-soft);
  cursor: pointer; box-shadow: var(--shadow);
}
.gm-btn:hover { color: var(--ink); }
.gm-btn.on { background: var(--navy); border-color: var(--navy); color: #fff; }

/* The divIcon pin: same silhouette and colours as the SVG map's pins. */
.gm-pin-wrap { background: none; border: none; }
.gm-pin { position: relative; display: block; width: 26px; height: 32px; }
.gm-pin svg { width: 26px; height: 32px; display: block; fill: var(--navy); overflow: visible; }
.gm-pin svg path { stroke: #fff; stroke-width: 1.6; }
.gm-pin svg circle { fill: #fff; }
.gm-pin.off svg { fill: var(--muted); }
.gm-pin.pick svg { fill: var(--accent); }
.gm-count {
  position: absolute; top: -8px; right: -12px;
  min-width: 19px; height: 19px; padding: 0 4px;
  border-radius: 10px;
  background: var(--ok); border: 2px solid #fff; color: #fff;
  font-style: normal; font-size: var(--fs-2xs); font-weight: 700;
  line-height: 15px; text-align: center;
}
.gm-name {
  position: absolute; left: 30px; top: 7px;
  font-size: var(--fs-xs); font-weight: 600; color: var(--ink-soft);
  white-space: nowrap; pointer-events: none;
  text-shadow: 0 1px 0 rgba(255,255,255,.9), 0 -1px 0 rgba(255,255,255,.9),
    1px 0 0 rgba(255,255,255,.9), -1px 0 0 rgba(255,255,255,.9);
}
/* The wider dialog the Sites editor needs for its pick-a-point map. */
.modal.wide { max-width: 760px; }
/* The widest: a dialog that is a form of a whole RECORD, four fields across —
   the payroll row editor and its sixteen. Sized so those four columns hold
   their labels ("Agreed monthly amount (EUR)" is the longest) on one line;
   past that the editor's own media queries drop it to two columns and then to
   one. The dialog is still width:100% inside the backdrop, so a narrow window
   simply gets a narrower dialog. */
.modal.xwide { max-width: 1040px; }

/* ---- global search palette (app.js) ---- */
/* height, not min-height. The 32px minimum was already the right intent, but
   6px of padding around a 15px icon and 13px text overran it and the control
   came out 34px — a third height on a bar that should only ever show one. */
.searchbtn {
  display: inline-flex; align-items: center; gap: 7px;
  font: inherit; font-size: var(--fs-sm);
  padding: 0 12px; height: var(--ctl-h-sm); flex: none;
  border: 1px solid var(--line); border-radius: 999px;
  background: #fff; color: var(--muted); cursor: pointer;
}
.searchbtn:hover { color: var(--ink); border-color: var(--accent); }
.searchbtn svg { width: 15px; height: 15px; }
.searchbtn kbd {
  font: inherit; font-size: var(--fs-2xs); line-height: 1;
  padding: 3px 5px; border-radius: 5px;
  background: var(--line-soft); color: var(--muted);
}
@media (max-width: 700px) { .searchbtn span, .searchbtn kbd { display: none; } }

body.search-open { overflow: hidden; }
.search-backdrop {
  position: fixed; inset: 0; z-index: 240;
  background: rgba(19, 30, 51, .38);
  display: flex; justify-content: center;
  padding: 12vh 16px 16px;
  animation: overlay-fade var(--dur-quick) var(--ease) both;
}
.search-panel {
  width: 100%; max-width: 560px; align-self: flex-start;
  background: #fff; border-radius: 14px; overflow: hidden;
  box-shadow: 0 24px 70px rgba(0, 0, 0, .3);
  animation: modal-in var(--dur-quick) var(--ease) both;
}
.search-panel input[type=search] {
  width: 100%; border: 0; border-bottom: 1px solid var(--line);
  border-radius: 0; padding: 15px 18px; font-size: var(--fs-lg);
}
.search-panel input[type=search]:focus { outline: none; border-color: var(--accent); }
.search-results { max-height: min(52vh, 440px); overflow-y: auto; padding: 6px; }
.search-results .empty { margin: 18px; }
.sr-row {
  display: flex; align-items: center; gap: 11px; width: 100%;
  font: inherit; text-align: left;
  padding: 9px 11px; border: 0; border-radius: 9px;
  background: none; color: var(--ink); cursor: pointer;
}
.sr-row.on, .sr-row:hover { background: var(--line-soft); }
.sr-ic { display: grid; place-items: center; width: 28px; height: 28px; flex: none;
  border-radius: 8px; background: var(--info-bg); color: var(--navy); }
.sr-ic svg { width: 15px; height: 15px; }
.sr-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.sr-text b { font-weight: 600; font-size: var(--fs-md); }
.sr-text span { font-size: var(--fs-xs); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sr-kind { font-size: var(--fs-2xs); color: var(--muted); flex: none; }
.search-foot {
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
  padding: 9px 16px; border-top: 1px solid var(--line);
  font-size: var(--fs-xs); color: var(--muted); background: var(--line-soft);
}
.search-foot kbd {
  font: inherit; font-size: var(--fs-2xs); line-height: 1;
  padding: 3px 5px; border-radius: 4px; background: #fff;
  border: 1px solid var(--line); color: var(--ink-soft);
}

/* ---- notices: the site-broadcast shortcut in the audience picker ---- */
.n-crew {
  display: flex; gap: 7px; align-items: center; flex-wrap: wrap;
  margin: 8px 0 6px; font-weight: 400; font-size: var(--fs-sm); color: var(--muted);
}
.n-crew select { width: auto; flex: 0 1 auto; min-width: 108px; padding: 5px 8px; font-size: var(--fs-sm); }
/* The notice's body: its last paragraph gives up the margin the card's own
   padding already provides, so the card closes 20px under the text as it
   opens 20px above the title, not 35px. */
.n-body { margin-top: 14px; line-height: 1.55; }
.n-body > :last-child { margin-bottom: 0; }

/* ---- punch position on the location panel (ADR-009) ------------------------ */
/* The distance label on the line from the pin to the site. Leaflet's tooltip
   chrome (arrow, padding) is replaced with a pill that reads over any tile. */
.leaflet-tooltip.lp-line-label { background: #fff; border: 1px solid var(--line-soft); border-radius: 999px; padding: 1px 9px; font-size: var(--fs-xs); font-weight: 600; color: var(--ink); box-shadow: var(--shadow); white-space: nowrap; }
.leaflet-tooltip.lp-line-label::before { display: none; }
.lp-hold-toggle { margin-left: 8px; padding: 2px 9px; font-size: var(--fs-xs); min-height: 0; }
/* The inline hold form: a row of the facts list that opens into two fields. */
.geo-rows li.lp-hold-form { flex-direction: column; align-items: stretch; gap: 8px; }
.lp-hold-form label { display: flex; flex-direction: column; gap: 4px; font-size: var(--fs-sm); color: var(--muted); }
.lp-hold-form input { font: inherit; padding: 6px 8px; border: 1px solid var(--line); border-radius: var(--radius-ctl); color: var(--ink); }
.lp-hold-actions { display: flex; gap: 8px; }
