/* Falck Timekeeping.
 *
 * Fluid from 320px to ultrawide. Type and spacing scale with clamp() so there
 * are no jumps between breakpoints; layout only gains a column where a second
 * column earns its place rather than just filling space.
 *
 * Nothing is hidden at any size. Wide content scrolls inside its own container
 * so the page body never scrolls sideways.
 *
 * Colour and type come from tokens.css; nothing here hard-codes a brand value.
 */

:root {
  /* Type: small end holds at 320px, large end settles by ~1200px. */
  /* The fluid scale. Each step is the brand size from tokens.css with a smaller
     floor for phones — the ceilings ARE the brand values, which is why they are
     referenced rather than repeated.

     They used to be repeated as literals, which meant tokens.css looked like the
     source of truth while nothing consumed it: the whole --fs-* scale had zero
     uses and editing it changed nothing on screen. Now it governs. */
  --step-caption: clamp(0.75rem,   0.72rem + 0.15vw, var(--fs-caption));
  --step-small:   clamp(0.8125rem, 0.79rem + 0.15vw, var(--fs-body-sm));
  --step-body:    clamp(0.9375rem, 0.91rem + 0.18vw, var(--fs-body));
  --step-h3:      clamp(1.125rem,  1.05rem + 0.40vw, var(--fs-h3));
  --step-h2:      clamp(1.3125rem, 1.16rem + 0.70vw, var(--fs-h2));
  --step-h1:      clamp(1.625rem,  1.35rem + 1.30vw, var(--fs-h1));

  /* Space: gutters grow with the viewport rather than stepping. */
  --gutter:   clamp(0.875rem, 0.5rem + 1.6vw, 2rem);
  --stack:    clamp(0.875rem, 0.7rem + 0.7vw, 1.25rem);
  --card-pad: clamp(1rem,     0.8rem + 1vw,   1.75rem);

  /* Text runs the width of its card. It was capped at 68ch, which on a wide
     screen left half of every card empty beside its explanation (Peter,
     2026-10-19); the cards and the page wrap already bound the line. */
  --measure: none;

  /* The edge of a control you are meant to type in.
   *
   * NOT --border. That is Pantone 427 (#D1D4D3) and measures 1.49:1 against
   * white; WCAG 1.4.11 asks 3:1 of any boundary that identifies a control,
   * which is the whole reason a text field looks like a text field. Every input,
   * select and textarea in the app was drawn at 1.49:1 — a 2px line most people
   * can see and a real number of people cannot.
   *
   * --border-strong is not the answer either despite the name: --dark-red-30
   * (#D1B9BE) measures 1.84:1, so it is "strong" only relative to something
   * that already failed. This is --dark-red-60 (#A3747F) from the same
   * sanctioned ramp, which measures 3.92:1 on --surface (white) and 3.22:1 on
   * --surface-muted, the tint a locked field sits on. Both clear 3:1, so the
   * rule holds in every place a control is actually drawn.
   *
   * Defined here rather than in tokens.css because it is a decision about our
   * semantic layer — the brand guide has no opinion about form borders — and
   * because leaving --border alone keeps every table rule and card edge exactly
   * as designed. Only the boundaries that WCAG counts get darker.
   */
  --border-control: var(--dark-red-60);

  /* The two elevations this app actually has, named once.
   *
   * `var(--shadow-card)` was already being referenced by four rules — the ESO
   * handoff card, the reopen popover, the toast and the parallel-run readiness
   * panel — and was defined nowhere. An undefined custom property does not fall
   * back, it drops the whole declaration, so all four rendered flat while the
   * stylesheet read as though they were raised. Same class of fault as the
   * --space-5 note further down: it fails silently and looks intentional.
   *
   * --shadow-card is the exact value `.card` has always used, so a raised panel
   * matches the cards beside it rather than approximating them. --shadow-pop is
   * for the two things that float ABOVE the page — a popover and a toast — and
   * is the value `.sch-sticky-save` arrived at independently.
   */
  --shadow-card: 0 1px 3px rgb(0 0 0 / .07);
  --shadow-pop:  0 10px 30px rgb(0 0 0 / .2);

  /* Also referenced-but-undefined, in the one place a monospace stack was named
     rather than repeated: the ESO handoff card's raw values. The stack itself
     was already written out twice below, on `code` and on `a.mono`; both now
     point here so there is one answer. */
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* ---------- skip link ----------
   Every page in this app puts a rail of up to eight tools in front of its
   content, and there was no way past it: no landmark, no skip link, forty
   pages. Visible only on focus, and it has to be first in the DOM to be the
   first thing Tab reaches — hence its position in layout(). */
.skiplink {
  position: absolute; left: var(--space-2); top: var(--space-2);
  z-index: 100; transform: translateY(-200%);
  background: var(--surface); color: var(--accent-text);
  border-radius: var(--radius-sm);
  padding: 12px var(--space-4); min-height: 44px;
  font-weight: var(--fw-bold); text-decoration: none;
  box-shadow: 0 2px 10px rgb(0 0 0 / .25);
}
.skiplink:focus { transform: none; outline: 3px solid var(--focus-ring); outline-offset: 2px; }
@media (prefers-reduced-motion: no-preference) { .skiplink { transition: transform .1s; } }

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; transition-duration: .01ms !important;
  }
}
body {
  margin: 0;
  min-height: 100dvh;
  background: var(--surface-muted);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--step-body);
  line-height: var(--lh-body);
  overflow-wrap: break-word;   /* long ids/emails can't force a sideways scroll */
}
img { max-width: 100%; display: block; }

/* The browser's own `[hidden] { display: none }` is a bare element rule, so any
   class that sets `display` beats it on specificity. That silently defeated
   every `hidden` in the app: the filter box's Clear button carries the
   attribute and `.btn { display: inline-block }` outranked it, so Clear showed
   on all seven search boxes even with an empty field. Same for the
   `[data-empty]` placeholders. `!important` is the accepted fix here —
   `hidden` is a statement of fact, not a style preference. */
[hidden] { display: none !important; }

/* ---------- discreet page ----------
   Full-bleed brand red so it reads as deliberate rather than broken, while
   giving a stranger nothing about what the system does. */

body.plain { background: var(--falck-red); }
.blank {
  min-height: 100dvh;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--space-6); padding: var(--space-8); text-align: center;
}
.blank .mark { width: min(230px, 62vw); height: auto; }
.blank .line {
  color: var(--text-on-dark); opacity: .85; margin: 0;
  font-size: var(--step-body); font-weight: var(--fw-medium); max-width: 30ch;
}

/* ---------- shell ---------- */

/* The height of the sticky header, as one number.
 *
 * Three things stick to the top of the viewport — the header, the rail, and
 * table headers — and every one of them but the header has to start BELOW it.
 * They were all `top: 0`, so a sticky table header slid under the red bar and
 * vanished behind it while you scrolled, and the full-height rail hung the same
 * distance off the bottom of the screen.
 *
 * A fixed value rather than the clamp that sizes the padding, because a sticky
 * offset can't be computed from an element's rendered height in CSS — so the
 * header is given this height explicitly and everything else offsets by it.
 */
:root { --topbar-h: 3.5rem; }
@media (min-width: 60rem) { :root { --topbar-h: 3.75rem; } }

.topbar {
  background: var(--falck-red);
  /* viewport-fit=cover pulls the page under the iPhone notch in landscape;
     max() keeps the normal gutter everywhere else and yields to the inset
     only when the hardware demands more. */
  padding-block: 0;
  padding-inline: max(var(--gutter), env(safe-area-inset-left))
                  max(var(--gutter), env(safe-area-inset-right));
  height: var(--topbar-h);
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--stack); flex-wrap: nowrap;
  position: sticky; top: 0; z-index: 20;
}
.topbar .brand { flex: none; }
.topbar img { height: clamp(22px, 18px + 1vw, 30px); }
.topbar a { color: var(--text-on-dark); }
/* `.who` is the ONLY thing in this bar allowed to shrink, and it truncates with
   an ellipsis when it does. That is right for a name and catastrophic for a
   link: the sign-out anchor used to live inside this span, so on a 375px phone
   it laid out at x=415 — past the right edge of the viewport, inside a box
   clipped at 361 — and `.topbar` does not scroll, so there was no way to reach
   it. It is a sibling now (see .topbar-link) and this rule governs the name
   only. `flex: 1 1 auto` so it gives up its space before anything else does. */
.topbar .who {
  flex: 1 1 auto;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text-on-dark); font-size: var(--step-caption); font-weight: var(--fw-medium);
}
.topbar-meta {
  min-width: 0; display: flex; align-items: center; justify-content: flex-end;
  gap: var(--space-2); color: var(--text-on-dark);
  font-size: var(--step-caption); font-weight: var(--fw-bold);
}
/* Never shrinks, never truncates, and big enough to hit. `Sign out` measured
   47×16px — a 16px-tall target on a touch screen, while the `.topbar-stop`
   button two rules down already knew the answer was 44. Same 44px minimum
   here, taken as padding so the bar's height is unchanged. */
.topbar .topbar-link {
  flex: none;
  display: inline-flex; align-items: center;
  min-height: 44px; padding-inline: var(--space-2);
  font-size: var(--step-caption); font-weight: var(--fw-bold);
  color: var(--text-on-dark); white-space: nowrap;
}
.topbar .topbar-link:hover { text-decoration-thickness: 2px; }
/* Sign out is a POST, so it is a button dressed as the link beside it. */
.topbar .topbar-signout { display: contents; }
.topbar button.topbar-link {
  background: none; border: 0; margin: 0; font-family: inherit;
  text-decoration: underline; cursor: pointer;
}
.impersonation-label {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.topbar-meta form { flex: none; margin: 0; }
.topbar .topbar-stop {
  min-height: 44px; padding: 9px 14px;
  background: var(--text-on-dark); color: var(--accent);
}
.topbar .topbar-stop:hover { color: var(--accent); }

.wrap {
  width: 100%; max-width: 780px; margin-inline: auto;
  /* The bottom inset matters as much as the sides: the layout sets
     viewport-fit=cover, so on a notched phone the last row of a list or the
     final button in a form otherwise sits under the home indicator. */
  padding-block: var(--gutter)
                 calc(clamp(2rem, 5vh, 4rem) + env(safe-area-inset-bottom));
  padding-inline: max(var(--gutter), env(safe-area-inset-left))
                  max(var(--gutter), env(safe-area-inset-right));
}
.wrap.wide { max-width: 1400px; }
/* Two columns need room for two. 780px is the measure for one. */
.wrap.split { max-width: 1180px; }
.wrap.form-page { max-width: 60rem; }

.card {
  background: var(--surface);
  border-radius: var(--radius-md);
  padding: var(--card-pad);
  margin-bottom: var(--stack);
  box-shadow: 0 1px 3px rgb(0 0 0 / .07);
}
.card > :last-child { margin-bottom: 0; }

h1 { font-size: var(--step-h1); font-weight: var(--fw-heavy); line-height: var(--lh-tight);
     margin: 0 0 var(--space-2); text-wrap: balance; }
h2 { font-size: var(--step-h3); font-weight: var(--fw-bold); margin: 0 0 var(--space-3); }
/* --accent-text (#66182A, 12.15:1) rather than --accent (#FF1E28, 3.84:1).
   tokens.css says Falck Red is large-text-only and it is right: this heading
   renders at --step-caption, ~13px, which is small text needing 4.5:1. It stays
   distinct from body copy by being uppercase, bold and letter-spaced — three
   signals that do not depend on anybody's colour vision. */
h3 { font-weight: var(--fw-bold); color: var(--accent-text);
     margin: var(--space-4) 0 var(--space-2); text-transform: uppercase;
     letter-spacing: .06em; font-size: var(--step-caption); }
p { margin: 0 0 var(--space-3); max-width: var(--measure); }
.muted { color: var(--text-muted); }
.small { font-size: var(--step-small); }
a { color: var(--accent-text); text-decoration-thickness: 1px; text-underline-offset: 2px; }
/* Hover thickens the rule instead of turning the text Falck Red. Body-sized
   link text at 3.84:1 is below the 4.5:1 it needs, and there is no darker red
   in the palette to move TO — the link is already --falck-dark-red. So the
   affordance moves off colour entirely, which also means it survives being
   printed, dimmed or colour-blind. */
a:hover { text-decoration-thickness: 3px; }

/* ---------- highlighter / pills ---------- */

.tag {
  display: inline-block; border-radius: var(--radius-pill);
  padding: 3px 12px; font-size: var(--step-caption); font-weight: var(--fw-bold);
  background: var(--accent); color: var(--on-accent); white-space: nowrap;
}
.tag.quiet   { background: var(--dark-red-20); color: var(--accent-text); }
.tag.info    { background: var(--status-info); color: var(--text-on-dark); }
.tag.warn    { background: var(--status-attention-bg); color: var(--status-attention-text); }
.tag.ok      { background: var(--status-ok); color: var(--text-on-dark); }
/* The outcome that went against you: rejected, declined, withdrawn.
 *
 * NOT --accent. Falck Red is the brand mark and the call-to-action; spending it
 * on "this was refused" makes every rejected row shout louder than the button
 * you are meant to press, and white on --falck-red is 3.84:1, which a 13px bold
 * pill does not clear. --falck-dark-red carries white at 12.1:1 and is already
 * the app's ink, so the pill reads as grave rather than as an alarm.
 *
 * This variant is why views were writing
 * `<span class="tag" style="background:var(--falck-dark-red)">Rejected</span>`
 * by hand — the need was real and the kit had no answer. */
.tag.danger  { background: var(--falck-dark-red); color: var(--text-on-dark); }

/* ---------- summary rows ---------- */

.rows { border-top: 1px solid var(--border); }
.row {
  display: flex; justify-content: space-between; gap: var(--space-4);
  padding: var(--space-3) 0; border-bottom: 1px solid var(--border);
}
.row dt { color: var(--text-muted); font-size: var(--step-small); margin: 0; }
.row dd { margin: 0; font-weight: var(--fw-medium); text-align: right;
          font-variant-numeric: tabular-nums; }

/* ---------- explanation + pay period ---------- */

.explain {
  background: var(--dark-red-10); border-left: 4px solid var(--accent);
  border-radius: var(--radius-sm); padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
}
.explain, .period-note { width: 100%; max-width: none; }

.period-note {
  display: flex; flex-direction: column; gap: 2px;
  border-radius: var(--radius-sm); padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
  background: var(--surface-muted); border-left: 4px solid var(--border);
}
.period-note.now  { background: var(--surface-attention); border-left-color: var(--falck-response-yellow);
                    color: var(--status-attention-text); }
.period-note.past { opacity: .85; }
.period-note .period-dates { font-weight: var(--fw-medium); }
.period-note .period-deadline { display: block; }

.tk-home-dashboard { margin-bottom: var(--stack); }
.tk-home-dashboard .stats { margin-bottom: var(--space-3); }
.tk-home-actions { justify-content: flex-start; }

.card.period.past { opacity: .78; }
.period-head { display: flex; justify-content: space-between; align-items: flex-start;
               gap: var(--space-4); margin-bottom: var(--space-2); }
.period-count {
  font-size: var(--step-h2); font-weight: var(--fw-heavy); line-height: 1;
  color: var(--accent); font-variant-numeric: tabular-nums;
}
.card.period.past .period-count { color: var(--text-muted); }

/* 14-day band: one dot per affected shift, plus a today marker */
.band { margin: var(--space-6) 0 var(--space-4); }
.band-track {
  position: relative; height: 10px; border-radius: var(--radius-pill);
  background: var(--grey-200, var(--falck-cool-grey));
}
.card.period.now .band-track { background: var(--dark-red-20); }
.band-dot {
  position: absolute; top: 50%; width: 14px; height: 14px; border-radius: 50%;
  transform: translate(-50%, -50%);
  background: var(--accent); box-shadow: 0 0 0 3px var(--surface);
}
.card.period.past .band-dot { background: var(--falck-dark-grey); }
.band-today {
  position: absolute; bottom: 22px; transform: translateX(-50%);
  font-size: var(--step-caption); font-weight: var(--fw-bold);
  color: var(--status-attention-text); white-space: nowrap;
}
.band-today i {
  display: block; width: 2px; height: 20px; margin: 0 auto;
  background: var(--surface-inverse); position: absolute; top: 100%; left: 50%;
}
.band-ends {
  display: flex; justify-content: space-between; margin-top: var(--space-2);
  font-size: var(--step-caption); color: var(--text-muted);
}

/* ---------- shift list (batch of missed punches) ---------- */

.rowlist { display: flex; flex-direction: column; gap: var(--space-2); }
.rowitem {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border: 2px solid var(--border); border-radius: var(--radius-md);
  text-decoration: none; color: inherit; min-height: 68px;
}
.rowitem:not(.done):hover { border-color: var(--accent); background: var(--dark-red-10); }
.rowitem.done { opacity: .6; border-style: dashed; }
/* Unread rows in the updates feed: a left accent bar, so unread reads at a
   glance without competing with the hover tint. An inset shadow rather than a
   border, because a border on a <tr> does not render across engines. */
.rowitem.is-unread { border-left: 4px solid var(--accent); }
tr.is-unread td:first-child { box-shadow: inset 4px 0 0 var(--accent); }
.rowitem-main { display: flex; flex-direction: column; gap: 3px; align-items: flex-start; }
.rowitem-main strong { font-size: var(--step-body); }
.rowitem .chev { font-size: 1.6rem; color: var(--accent); line-height: 1; flex: none; }

/* ---------- the schedule card ----------
 *
 * Deliberately almost nothing. The card is built from the shared kit — card,
 * rowlist, rowitem, tag, empty-state — all of which are mobile-first and
 * already measured, so the only rules here are spacing and one guard. Adding a
 * grid would mean adding breakpoints, and a fortnight of one-line days has no
 * width at which two columns are right on a phone.
 *
 * `min-width: 0` on the row's own column is the guard, and it is the fix for
 * the failure this codebase has hit twice: a flex or grid child defaults to
 * min-width:auto and refuses to shrink below its content, so one long shift
 * name ("Spec Event/Training", a unit, a length tag) widens the row past the
 * card and the page scrolls sideways at 320px. Nothing here declares a width in
 * pixels for the same reason.
 */
.sched-card .sched-today { margin: 0 0 var(--space-2); }
.sched-week {
  margin: var(--space-4) 0 var(--space-2);
  font-size: var(--step-small); color: var(--text-muted);
}
.sched-card .rowitem-main { min-width: 0; }
.sched-card .rowitem { flex-wrap: wrap; }
.sched-footer { margin-top: var(--space-3); margin-bottom: 0; }
.sched-summary > p { margin: 0 0 var(--space-1); }
.sched-summary > p:last-child { margin-bottom: 0; }

/* ---------- missed-punch texts ----------
 *
 * The same deliberate almost-nothing as the schedule card above it, and for the
 * same reason: the page is built from card, rows, tag and empty-state, every one
 * of which is mobile-first and already measured. What is left is spacing, one
 * wrapping rule and the guard.
 *
 * NO PIXEL WIDTHS AND NO BREAKPOINT. The message box, the filter row and the
 * meta line are all fluid or flex rows that wrap; at 320px they become stacks
 * without a media query being involved, which is what stops this block from
 * being the thing that breaks a width later. `min-width: 0` on the wrapping
 * children is the guard against the failure this codebase has hit twice — a
 * flex child defaults to min-width:auto, refuses to shrink below its content,
 * and one long message pushes the card past the viewport.
 *
 * 34rem IS A READING MEASURE, NOT A LAYOUT. It is the ceiling on the message
 * box and on the sentences about the message, for the same reason `.wrap` caps
 * a column: a text message stretched across a 2560px monitor is harder to read
 * than the same text message on a phone, and this is the one thing on the page
 * somebody has to read every word of before pressing a button.
 */
.mptext-group { margin-bottom: var(--space-8); }
.mptext-group > h2 { margin-bottom: var(--space-1); }
.mptext-groupcount { color: var(--text-muted); font-weight: var(--fw-regular); }
.mptext-person { margin: var(--space-6) 0; }
.mptext-name { margin: 0 0 var(--space-1); }
.mptext-case { margin-top: var(--space-3); }
.mptext-case-head { margin: 0 0 var(--space-2); font-size: var(--step-body); }
.mptext-case .rows dd { overflow-wrap: anywhere; min-width: 0; }

/* ---------- the one block, in both places ----------
 *
 * The fixed halves of the message sit tight against the box, with no gap
 * between them and it, so the three read as one message rather than as a
 * caption, a field and a footnote. They are muted because they are not yours to
 * change, and that is the only thing their colour has to say.
 */
.mptext-block { margin: var(--space-4) 0 0; }
.mptext-block-head { margin: 0 0 var(--space-2); font-size: var(--step-body); }
.mptext-context { margin: 0 0 var(--space-3); }
.mptext-fixed { margin: 0; }
.mptext-text {
  display: block; width: 100%; min-width: 0; max-width: 34rem;
  margin: var(--space-1) 0; resize: vertical;
  font: inherit; font-size: var(--step-body);
}
.mptext-meta {
  margin: 0 0 var(--space-3); display: flex; flex-wrap: wrap;
  align-items: center; gap: var(--space-3); min-width: 0;
}
/* A button, because it does something on this page rather than going somewhere —
   and 44px tall, because it is the second thing a thumb reaches for here. */
.linkish {
  background: none; border: 0; padding: 0 var(--space-1); margin: 0;
  color: var(--accent-text); text-decoration: underline; cursor: pointer;
  font: inherit; font-size: var(--step-small);
  min-height: 44px; display: inline-flex; align-items: center;
}
.linkish:hover { color: var(--accent); }
.mptext-count { font-variant-numeric: tabular-nums; }
.mptext-log { margin: var(--space-2) 0 0; }
/* A sentence, not a control, so it is left as ordinary inline flow: made a flex
   row it broke "Employee responded 10:03 AM" and the words they sent onto two
   lines with the colon stranded at the start of the second. The one thing on it
   anybody presses is a real button underneath. */
.mptext-state {
  margin: var(--space-3) 0; min-width: 0; overflow-wrap: anywhere; max-width: 34rem;
}
.mptext-state.ok { color: var(--status-ok); }
.mptext-state.muted { color: var(--text-muted); }
.mptext-actions, .mptext-filters {
  display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center;
  margin-top: var(--space-3);
}
.mptext-actions > *, .mptext-filters > * { min-width: 0; }
.mptext-filters .field { margin-bottom: 0; }
/* The case reference is a link somebody opens with a thumb, not a citation, so
   it gets the target size a control gets. Inline-flex rather than a block so it
   still sits inside the definition list beside its own label. */
.mptext-caselink {
  display: inline-flex; align-items: center; min-height: 44px; min-width: 0;
  overflow-wrap: anywhere;
}
.mptext-thread, .mptext-list { margin: var(--space-3) 0 0; padding-left: 1.1rem;
  font-size: var(--step-small); }
.mptext-thread li, .mptext-list li {
  margin-bottom: var(--space-2); min-width: 0; overflow-wrap: anywhere;
}
.case-mptext { margin: var(--space-4) 0; }

/* ---------- the seven boards ----------
 *
 * A row of links, not tabs in the ARIA sense: each one is a URL you can send
 * somebody, which is the whole reason the queue keeps its state in the query
 * string. So they are anchors, and the active one is marked with
 * `aria-current="page"` rather than with colour alone.
 *
 * WRAPS, NEVER SCROLLS SIDEWAYS. This is the mistake the status chip row made
 * and had to be rebuilt out of: on a real queue the row scrolled, and the views
 * a timekeeper needed sat off the right-hand edge where nobody found them. Seven
 * labels at 320px is three lines, and three lines you can read beats one line
 * you have to swipe. 44px minimum on every one of them, because this is the
 * first thing touched on the page.
 */
.viewtabs {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  margin: var(--space-4) 0;
}
.viewtab {
  display: inline-flex; align-items: center; gap: var(--space-2);
  min-height: 44px; padding: var(--space-2) var(--space-4);
  border: 1px solid var(--border); border-radius: var(--radius-pill);
  color: var(--text); text-decoration: none; font-size: var(--step-small);
  min-width: 0; overflow-wrap: anywhere;
}
.viewtab:hover { border-color: var(--border-strong); }
.viewtab-n { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.viewtab.on {
  background: var(--surface-inverse); border-color: var(--surface-inverse);
  color: var(--text-on-dark); font-weight: var(--fw-medium);
}
.viewtab.on .viewtab-n { color: var(--text-on-dark); }

/* ---------- form ---------- */

.field { margin-bottom: var(--space-4); }
label.lbl, legend.lbl { display: block; font-weight: var(--fw-bold); font-size: var(--step-small);
            margin-bottom: var(--space-1); }
.hint { font-size: var(--step-caption); color: var(--text-muted); margin-top: var(--space-1); }

/* Matches by exclusion, not by listing types.
 *
 * This was `input[type=text], input[type=time], input[type=date]` — which does
 * NOT match `<input name="title">`, because an input with no type attribute
 * behaves as text but doesn't carry the attribute the selector tests. The title
 * field on the notice editor rendered at its default ~150px while every other
 * field was full width, and the same trap waits for any future untyped input.
 *
 * Excluding the types that must NOT be full width is the shorter list and the
 * one that can't fall out of date. */
/* :where() so the negations cost NOTHING in specificity.
 *
 * Written as a bare `:not(...)` chain this scored 0,5,1 — five attribute
 * selectors — which outranks `.field.locked input` at 0,2,1 and silently
 * repainted every locked punch field white and solid-bordered. The form still
 * said "the greyed-out times came through from the clock" while nothing was
 * greyed out.
 *
 * A base rule must always be the weakest thing in the cascade, or every
 * modifier built on top of it stops working. `:where()` keeps this at 0,0,1. */
input:where(:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=button]):not([type=file])),
select, textarea {
  /* Never below 16px. --step-body bottoms out at 15px, and iOS Safari zooms the
     whole page in on any focused control under 16 — which on the correction
     form (a phone task, often one-handed, often in a hurry) throws the layout
     sideways mid-entry and does not zoom back out. The floor costs nothing on
     desktop, where the clamp is already at 16. */
  width: 100%; font-family: inherit; font-size: max(16px, var(--step-body));
  padding: 13px var(--space-3);
  /* 3.92:1 against white, 3.22:1 against the muted tint a locked field sits on.
     Was --border at 1.49:1. See --border-control at the top of this file. */
  border: 2px solid var(--border-control); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
  min-height: 48px;                       /* comfortable touch target */
}
textarea { min-height: 96px; resize: vertical; line-height: var(--lh-body); }

/* iOS Safari gives date/time inputs an intrinsic size and centres their text
   oddly, ignoring the box styling above unless the native chrome is stripped.
   Everything else about them still works — the wheel picker is triggered by
   focus, not by the appearance. */
input[type=time], input[type=date] {
  -webkit-appearance: none;
  appearance: none;
}
input:focus-visible, select:focus-visible, textarea:focus-visible, button:focus-visible,
a:focus-visible {
  outline: 3px solid var(--focus-ring); outline-offset: 2px;
}

/* the field(s) they actually need to fill */
.field.needed input, .field.needed select,
.field.accent input { border-color: var(--accent); background: var(--surface); }
.field.needed label.lbl::after {
  content: 'Needs your input'; float: right; font-weight: var(--fw-bold);
  color: var(--accent); font-size: var(--step-caption); text-transform: uppercase;
  letter-spacing: .05em;
}

/* prefilled from the system — locked */
.field.locked input {
  background: var(--surface-muted); color: var(--text-muted);
  /* Still a control, so still 3:1 — a disabled-looking field is exempt from
     1.4.11, a locked-but-present one that the form asks you to read is not. */
  border-color: var(--border-control); border-style: dashed;
}
.field.locked label.lbl::after {
  content: 'From timekeeping'; float: right; font-weight: var(--fw-medium);
  color: var(--text-muted); font-size: var(--step-caption); text-transform: none;
  letter-spacing: 0;
}

.check { display: flex; gap: var(--space-3); align-items: flex-start;
         padding: var(--space-3); background: var(--surface-muted);
         border-radius: var(--radius-sm); }
.check input { width: 22px; height: 22px; min-height: 0; flex: none;
               /* Sits on the first line of the label rather than the top of the
                  box, so a two-line label doesn't leave it floating. */
               margin: 10px 0 0; }
/* The 22px box is the mark; the padded row keeps the touch row at 44px so the
   input and its label never render in a cramped band. */
.check { min-height: 44px; }
.check label { cursor: pointer; }
.check label {
  font-size: var(--step-small);
  /* Block, NOT flex. The label holds running text plus an inline <span> of
     explanation; making it a flex container turned those into two flex items
     and ate the space between them — "…lunch break— plenty of shifts". The
     44px touch target comes from padding instead, which leaves the text
     flowing as text. */
  display: block;
  padding-block: 11px;
  min-height: 44px;
}
.check label .muted { display: inline; }

.total {
  display: flex; justify-content: space-between; align-items: baseline;
  background: var(--falck-dark-red); color: var(--text-on-dark);
  padding: var(--space-3) var(--space-4); border-radius: var(--radius-md);
  margin-bottom: var(--space-4);
}
.total .n { font-size: var(--step-h2); font-weight: var(--fw-heavy);
            font-variant-numeric: tabular-nums; }

/* ---------- buttons ---------- */

.btn {
  display: inline-block; font-family: inherit; font-size: var(--step-body);
  font-weight: var(--fw-bold); border: 0; cursor: pointer; text-decoration: none;
  background: var(--accent); color: var(--on-accent);
  padding: 15px var(--space-6); border-radius: var(--radius-pill);
  min-height: 50px; text-align: center;
}
.btn:hover { filter: brightness(.94); color: var(--on-accent); }
.btn.block { display: block; width: 100%; }
.btn.sm { font-size: var(--step-small); padding: 9px 20px; min-height: 44px; }
.btn.ghost { background: transparent; color: var(--accent-text);
             box-shadow: inset 0 0 0 2px currentColor; }
.btn.ghost:hover, .btn.ghost:focus-visible {
  background: var(--dark-red-10); color: var(--accent-text);
  box-shadow: inset 0 0 0 3px currentColor; filter: none;
}
.btn.dark { background: var(--falck-dark-red); }
.btn[disabled] { opacity: .45; cursor: not-allowed; }
.btnrow { display: flex; flex-direction: column; gap: var(--space-3); }
/* Scoped to the case's own action row, NOT to every .btnrow. There are a dozen
   others and most sit inside cards, where `.card > :last-child` zeroes a margin
   only when they happen to be last — so a blanket rule here moved spacing on
   pages nobody was looking at. This one carries the stack the case bar above it
   gave up, so the header group ends where the content begins. */
.btnrow.case-actions { margin-bottom: var(--stack); }
.next-shift-status { align-self: center; }

/* ---------- alerts ---------- */

/* `:not(.banner)` because `alert` is both a component class and a banner tone.
   Unscoped, this rule also matched `<section class="banner alert">` and quietly
   restyled it — shrinking the banner lead to caption size and adding a stray
   margin, which is why the one "you must read this" banner looked wrong. */
.alert:not(.banner) { border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
         margin-bottom: var(--space-4); font-size: var(--step-small); }
.alert.err  { background: var(--surface-error); color: var(--status-error);
              box-shadow: inset 0 0 0 2px var(--status-error); }
.alert.flag { background: var(--surface-attention); color: var(--status-attention-text);
              box-shadow: inset 0 0 0 2px var(--status-attention-bg); }
.alert.ok   { background: var(--surface-ok); color: var(--status-ok);
              box-shadow: inset 0 0 0 2px var(--status-ok); }
.alert ul { margin: var(--space-2) 0 0; padding-left: 1.1rem; }
/* The unread-updates line on the queue: one row, the button beside the link. */
.unread-note { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.unread-note form { display: inline; margin: 0; }
/* The ESO check's progress, shown only while one is running. */
.sync-progress { flex-basis: 100%; width: 100%; height: 12px; margin-top: var(--space-2);
  -webkit-appearance: none; appearance: none; border: 0; border-radius: 999px;
  background: var(--surface-muted, #e6dfe3); overflow: hidden; color: var(--accent); }
/* The fill, in each engine's own pseudo-element: without these the native
   control drew a flat grey bar with nothing visibly filling it (2026-10-19). */
.sync-progress::-webkit-progress-bar { background: var(--surface-muted, #e6dfe3); border-radius: 999px; }
.sync-progress::-webkit-progress-value { background: var(--accent); border-radius: 999px; }
.sync-progress::-moz-progress-bar { background: var(--accent); border-radius: 999px; }
.sync-active { box-shadow: inset 0 0 0 2px var(--accent); }
/* The way to the ESO hand-off when its tile is pressed: sentence, then the button. */
.handoff-callout { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--space-3); }

/* ---------- admin table ---------- */

.tablewrap { overflow-x: auto; -webkit-overflow-scrolling: touch;
             border-radius: var(--radius-md); }
/* This lane scrolls, so it takes focus and shows that it has it — the same
   bargain `.chart-scroll` already makes. Without the ring the tab stop is
   invisible and reads as the page losing focus. */
.tablewrap:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* The containing block for anything absolutely positioned inside a scrolled
   table. A .visually-hidden label in a cell is position: absolute; with no
   positioned ancestor it was placed against the page, not the table, escaped
   the lane's overflow clipping, and pushed the whole page sideways: the closed
   cases archive measured 1,081px wide on a 320px phone (2026-09-23). */
.tablewrap, .chart-tablewrap, .chart-scroll { position: relative; }
table { border-collapse: collapse; width: 100%; font-size: var(--step-small); }
th, td { text-align: left; padding: var(--space-3); border-bottom: 1px solid var(--border);
         white-space: nowrap; }
th { font-weight: var(--fw-bold); color: var(--text-muted); font-size: var(--step-caption);
     text-transform: uppercase; letter-spacing: .05em;
     /* top: 0 relative to `.tablewrap`, which has `overflow-x: auto` and is
        therefore a scroll container — so this sticks inside the table, not to
        the viewport, and can never collide with the page header.
        It was `top: var(--topbar-h)`, which offset the header 60px DOWN inside
        its own table and left the first row rendering above it. The fix for
        viewport-sticky headers was applied to a header that was never sticky
        to the viewport. */
     position: sticky; top: 0; background: var(--surface); z-index: 1; }
tbody tr:hover { background: var(--surface-muted); }

/* Figures, so they can be compared without being read.
 *
 * `table()` decides which columns hold numbers and marks both the header and
 * the cells; this is the whole of what that mark does. Right-aligned so the
 * units line up under the units, and tabular so the digits are one width — with
 * proportional figures a "1" is narrower than a "4" and a right-aligned column
 * still comes out ragged, which is why both halves are needed and neither was
 * present.
 *
 * `td.num` was already in the stylesheet doing half the job for exactly one
 * caller. */
th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* Keep the primary action reachable when the table scrolls sideways —
   otherwise it's the first thing to disappear on a narrow screen.

   Opt-in, because it only holds when the last column is narrow. Applied
   globally it pinned whatever happened to be last: on the operator access
   table that's an 800px column of permission grants, which then painted over
   the two columns to its left and made Role and Areas vanish. */
/* The pinned column overlays whatever scrolls beneath it — that is what sticky
   does, and it is fine as long as it reads as a pinned lane rather than as the
   table clipping. Without a hard edge it looked like the row was cut off: an
   audit measured the pinned Comments column painting over a status tag at
   1440px with only ~37px of scroll to justify it.

   A border plus a wider shadow makes the boundary unambiguous. Eliminating the
   overlap entirely means only pinning when the overflow exceeds the pinned
   column's own width, which CSS cannot measure — that needs JS and is not done
   here. */
.pinlast th:last-child, .pinlast td:last-child {
  position: sticky; right: 0; background: var(--surface);
  border-left: 1px solid var(--border);
  box-shadow: -10px 0 12px -6px rgb(0 0 0 / .22);
}

/* An area with no figures explains itself across the numeric columns, and that
   explanation is a sentence. Table cells are `nowrap` by default — right for a
   date or a count, and it would otherwise push a six-column table two screens
   wide to accommodate one row of prose. */
.area-nodata td { white-space: normal; }

/* The spellings a merged certificate row stands for, and why. Prose under a
   name, so it wraps — left as `nowrap` it pushed the table 330px past its card
   at 1400px to keep three certificate names on one line. */
.certvariants { white-space: normal; max-width: 44ch; margin-top: 2px; }

/* Wide free-text columns wrap rather than forcing the table ever wider. */
td.wrapcell { white-space: normal; min-width: 22rem; }
td.wrapcell code { white-space: normal; }
tbody tr:hover td:last-child { background: var(--surface-muted); }
td.reason { white-space: normal; min-width: 12ch; max-width: 22ch; }
/* A list of certificate names ("Not in ESO"): wraps instead of widening the table. */
td.certlist { white-space: normal; min-width: 16ch; }
/* The "Export CSV" button the export script puts above a data table. */
.table-export { display: flex; justify-content: flex-end; margin: 0 0 var(--space-2); }
/* A capped table: the first 25 rows, whatever order they are sorted into. */
table.capped:not(.show-all) tbody tr:nth-child(n+26) { display: none; }
.table-more { display: flex; justify-content: center; padding: var(--space-3) 0 var(--space-2); }
/* A card that leads to a report: title and line on the left, the button on the right. */
.report-callout { display: flex; flex-wrap: wrap; gap: var(--space-4); align-items: center;
  justify-content: space-between; }
.report-callout h2 { margin: 0 0 4px; }
.report-callout p { margin: 0; }
/* Column groups: a header row naming each group, and a rule down the left
   of the first column of each. */
thead tr.col-groups th { text-align: left; font-size: var(--step-small); font-weight: var(--fw-heavy);
  text-transform: uppercase; letter-spacing: .06em; color: var(--text);
  border-bottom: 2px solid var(--border-strong); padding-bottom: 6px; }
th.group-start, td.group-start { border-left: 2px solid var(--border-strong); }
/* The by-person clock-in table: thirteen columns, so headers and names wrap
   and the cells sit close. */
.by-person th, .by-person td.wrapname { white-space: normal; }
.by-person th, .by-person td { padding-left: 6px; padding-right: 6px; }
.by-person td.wrapname { min-width: 8rem; }
/* A reference row under a table's header, such as the median line. */
thead tr.pinned-row td { font-weight: var(--fw-bold); background: var(--surface-muted);
  border-bottom: 2px dashed var(--border-strong); color: var(--text); }
/* "Texted 10:43 AM, responded 10:43 AM" — a short sentence, not a figure, so it
   wraps onto a second line rather than pushing the whole queue table sideways.
   Blank on most rows, and blank means blank: an em dash in every empty cell is
   furniture.
   THE FLOOR IS MEASURED, NOT GUESSED. It was 11ch, and a table competing for
   width gave the column 98px — four wrapped lines and a 109px queue row, on a
   board whose other rows are 43px. 18ch is the width of "responded 10:43 AM",
   which is the longer of the two halves, so the worst case is two lines and one
   half on each. The ceiling stops a pair from claiming a quarter of the board;
   the table already scrolls, so the extra width costs the page nothing. */
td.texting { white-space: normal; min-width: 18ch; max-width: 24ch; }

.stats { display: grid; gap: var(--space-3); margin-bottom: var(--stack);
         grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.stat { background: var(--surface); border-radius: var(--radius-md); padding: var(--space-4); }
/* Figures inside a card sit on the card's own white; a tint keeps them boxes. */
.card .stat { background: var(--surface-muted); }
.stat .n { font-size: var(--step-h1); font-weight: var(--fw-heavy); line-height: 1;
           color: var(--accent); }
/* Not a figure. Sized and coloured as the sentence it is, so it never competes
   with the real numbers in the row beside it — a 36px em-dash in Falck Red
   reads as a value, which is the whole problem. */
.stat .n.none { font-size: var(--step-body); font-weight: var(--fw-medium);
                color: var(--text-muted); line-height: var(--lh-tight); }
.stat .k { font-size: var(--step-caption); color: var(--text-muted); text-transform: uppercase;
           letter-spacing: .05em; margin-top: var(--space-1); }
.stat .stat-note { margin-top: var(--space-2); line-height: var(--lh-tight); }

/* ---------- diff ---------- */

.diff { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
        gap: var(--space-3); }
.diff > * { min-width: 0; }
.diff .col { background: var(--surface-muted); border-radius: var(--radius-md);
             padding: var(--space-3) var(--space-4); }
.diff .col.new { background: var(--dark-red-10); }
/* The changed value in a diff, at --step-small. Falck Red as the TEXT was
   3.84:1 on white and 3.16:1 on the tinted "new" column — both under the 4.5:1
   small text needs. The red moves to the underline, where it is a non-text
   boundary held to 3:1 and clears it in both places, and the text itself takes
   --accent-text at 12.15:1 / 9.99:1. Bold plus a red rule under it is a louder
   signal than red text was, and a legible one. */
.changed { color: var(--accent-text); font-weight: var(--fw-bold);
           text-decoration: underline; text-decoration-color: var(--accent);
           text-decoration-thickness: 2px; text-underline-offset: 2px; }

/* ============ app shell ============ */

.shell { display: block; }
.shell-main { min-width: 0; }

/* Tool rail. Horizontal strip on a phone, left column on a desktop — same
   markup, so nothing is hidden at either size. */
.rail {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  /* Below the topbar, which is also sticky. At top: 0 the strip slid behind
     the red bar on the first scroll (z 15 vs 20) and simply vanished — the
     same collision fixed on desktop earlier, missed on the phone strip. */
  position: sticky; top: var(--topbar-h); z-index: 15;
}
.rail ul {
  list-style: none; margin: 0;
  padding: var(--space-2) max(var(--gutter), env(safe-area-inset-right))
           var(--space-2) max(var(--gutter), env(safe-area-inset-left));
  display: flex; gap: var(--space-2); overflow-x: auto;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.rail ul::-webkit-scrollbar { display: none; }
.rail li { flex: none; }
.rail a, .rail .off {
  display: flex; align-items: center; gap: 6px;
  min-height: 44px;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-pill); text-decoration: none;
  font-size: var(--step-small); font-weight: var(--fw-bold);
  white-space: nowrap; color: var(--text);
  background: var(--surface-muted); border: 1px solid transparent;
}
.rail a.on { background: var(--accent); color: var(--on-accent); }
.rail a:hover { border-color: var(--accent); color: var(--accent-text); }
.rail a.on:hover { color: var(--text-on-dark); }
.rail .off { color: var(--text-muted); opacity: .5; cursor: default; }

/* ============ banner ============
   First thing on the page: who you are and what actually matters right now. */

/* Dark Blue, not Dark Red. The brand's own line is "reds reflect passion and
   human touch, while the blue and grey colours reflect expertise" — a status
   banner is expertise. It also stops two reds stacking under the red topbar,
   and Response Yellow sits on navy at 13.2:1 instead of fighting a warm red. */
.banner {
  background: var(--surface-inverse); color: var(--text-on-dark);
  padding: clamp(1.25rem, 3vw, 2.25rem) var(--gutter);
  display: flex; flex-wrap: wrap; gap: var(--stack);
  align-items: center; justify-content: space-between;
}
.banner.ok     { background: var(--status-ok); }
.banner.urgent { background: var(--surface-inverse); }
.banner h1 { margin: 0; color: var(--text-on-dark); }
.banner-eyebrow {
  margin: 0 0 var(--space-1); font-size: var(--step-caption);
  text-transform: uppercase; letter-spacing: .06em; opacity: .75;
}
.banner-lead { margin: var(--space-2) 0 0; opacity: .92; }
.banner-text { min-width: 0; }
.banner-actions { flex: none; }
.banner-actions .btn.ghost { color: var(--text-on-dark); border-color: rgba(255,255,255,.78); }
.banner-actions .btn.ghost:hover { background: var(--surface); color: var(--accent-text); border-color: var(--text-on-dark); filter: none; }
.banner-warn {
  background: var(--status-attention-bg); color: var(--status-attention-text);
  border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
  display: flex; flex-direction: column; gap: 2px; max-width: 34ch;
}
.banner-warn strong { font-size: var(--step-body); }
.banner-warn span { font-size: var(--step-small); }

/* ============ queue: head, saved views, filters ============ */

.queue-head { display: flex; flex-wrap: wrap; gap: var(--stack);
              justify-content: space-between; align-items: flex-start; }
.queue-head h1 { margin-bottom: var(--space-1); }

.savedviews { display: flex; flex-direction: column; gap: var(--space-2);
              align-items: flex-start; }
.savedlist { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.saveform { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.saveform input[type=text] { width: auto; min-width: 12rem; min-height: 40px;
                             padding: 8px var(--space-3); font-size: var(--step-small); }
.savedefault { font-size: var(--step-caption); color: var(--text-muted);
               display: flex; align-items: center; gap: 4px; white-space: nowrap; }
.savedefault input { width: 18px; height: 18px; min-height: 0; }

/* `justify-content: flex-start`, not `space-between`.
   Space-between distributes the free space BETWEEN the items on every wrapped
   line, so a bar that wrapped — which the queue's always did — pushed "Flagged
   only" and the order links to opposite edges of a line of their own with a
   void in the middle, and the eye had to travel the width of the card to find
   out what was in it. `.filterform` already carries `flex: 1 1 22rem` and takes
   the slack; the trailing controls stay next to what they follow. */
.filterbar { display: flex; flex-wrap: wrap; gap: var(--space-3);
             align-items: center; justify-content: flex-start; }
.filterform { display: flex; flex-wrap: wrap; gap: var(--space-2);
              align-items: center; flex: 1 1 22rem; }
.filterform input[type=search] { flex: 1 1 14rem; min-height: 44px;
                                 padding: 10px var(--space-3); }
.filterform select { width: auto; min-width: 9rem; min-height: 44px;
                     padding: 10px var(--space-3); }

.chips { display: flex; flex-wrap: wrap; gap: var(--space-2);
         margin-bottom: var(--stack); }
.trend-result { display: inline-grid; justify-items: start; gap: 4px; }
.trend-basis { white-space: nowrap; }
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  background: var(--dark-red-20); color: var(--falck-dark-red);
  /* Comfortable touch target at every width — the 5px 12px original made
     filter chips easy to miss (two independent testers fat-fingered the
     districts window chips), and 44px-ish targets shouldn't be mobile-only. */
  border-radius: var(--radius-pill); padding: 9px 16px; min-height: 36px;
  font-size: var(--step-caption); font-weight: var(--fw-bold); text-decoration: none;
}
.chip:hover { background: var(--dark-red-30); color: var(--accent-text); }
.chip.on { background: var(--accent); color: var(--on-accent); }
.chip.clear { background: transparent; box-shadow: inset 0 0 0 1px var(--border);
              color: var(--text-muted); }
.resultcount { margin: 0 0 var(--space-2); }
/* The line that says the list is holding rows back, and the press that shows
   them. A link and not a button because it is a URL: the long view shares,
   reloads and prints, which a scripted reveal does not. 44px so it is a tap
   target and not just text. */
/* THE CASE HISTORY, READ DOWN A SINGLE TIME COLUMN. A grid rather than a
   stack: the time is one narrow column and everything about the event is the
   other, so a reader scans times down the left instead of hunting for them in
   prose. The date is a row of its own, said once per day.

   Two columns from 30rem and one below it, because a clock time and a chip and
   a name do not fit beside each other on a 320px phone — and the rule is that
   content wraps rather than the page scrolling sideways. */
.hist { list-style: none; margin: 0; padding: 0; }
.hist-day {
  margin: var(--space-4) 0 var(--space-2);
  font-size: var(--step-caption); font-weight: var(--fw-bold);
  text-transform: uppercase; letter-spacing: .04em;
  color: var(--text-muted);
}
.hist-day:first-child { margin-top: 0; }
.hist-row {
  display: grid; gap: 2px var(--space-3);
  padding: var(--space-2) 0;
  border-top: 1px solid var(--border);
  overflow-wrap: anywhere;
}
.hist-time {
  font-variant-numeric: tabular-nums; white-space: nowrap;
  font-size: var(--step-small); color: var(--text-muted);
}
.hist-what {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-1) var(--space-2); min-width: 0;
  font-size: var(--step-small);
}
/* How many times the same event repeated. Muted, because the count is context
   for the chip beside it rather than a number anybody acts on. */
.hist-count { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.hist-detail { min-width: 0; }
.hist-detail .rows { margin: var(--space-1) 0 0; }
@media (min-width: 30rem) {
  .hist-row { grid-template-columns: 4.5rem minmax(0, 1fr); align-items: baseline; }
  .hist-detail { grid-column: 2; }
}
.rowmore {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-1) var(--space-2);
  margin: var(--space-3) 0 0;
  font-size: var(--step-small); color: var(--text-muted);
}
.rowmore a {
  display: inline-flex; align-items: center; min-height: 44px;
  color: var(--accent-text); font-weight: var(--fw-bold);
}

/* A stat that is also a link needs to say so — otherwise it's a number people
   never think to press. The affordance is restraint: a border that warms and a
   figure that turns red on hover, not a button pretending to be a card.
   `.on` marks the stat whose filter is currently applied.

   This used to be two separate blocks 260 lines apart that disagreed about the
   border width and the hover colour; the later one silently won half of it. */
a.stat { text-decoration: none; display: block;
         /* Transparent to start, so the hover border doesn't shift the card. */
         border: 2px solid transparent;
         transition: border-color .12s, transform .12s; }
a.stat:hover { border-color: var(--accent); transform: translateY(-1px); }
a.stat:hover .n { color: var(--accent-text); }
a.stat.on { border-color: var(--accent); background: var(--dark-red-10); }
/* `outline` needs a style — a bare colour computes to outline-style:none and
   draws nothing, which is how this focus ring went missing. */
a.stat:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  a.stat { transition: none; }
  a.stat:hover { transform: none; }
}

/* sortable headers */
.sortlink { display: inline-flex; align-items: center; gap: 4px;
            text-decoration: none; color: inherit; white-space: nowrap; }
/* Same reasoning as `a:hover` above: this is a --step-caption uppercase header,
   the smallest text on the page, so Falck Red's 3.84:1 is not available to it.
   --accent-text is 12.15:1 and still reads as a state change against the
   header's --text-muted (#415565) — different hue, clearly darker — with the
   arrow carrying the direction. */
.sortlink:hover { color: var(--accent-text); text-decoration: underline;
                  text-decoration-thickness: 2px; }
.sortlink.on { color: var(--accent-text); font-weight: var(--fw-heavy); }
.sortlink .arrow { font-size: .85em; }

/* ============ queue rows ============ */

tr.rowlink, .qcard.rowlink { cursor: pointer; }
tr.rowlink:hover { background: var(--dark-red-10); }
.rowmain { text-decoration: none; color: inherit; display: block; }
.rowmain:hover strong { color: var(--accent); }

/* comment badge */
.badge-comment {
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--surface-inverse); color: var(--text-on-dark);
  border-radius: var(--radius-pill); padding: 3px 10px 3px 8px;
  font-size: var(--step-caption); font-weight: var(--fw-bold);
  text-decoration: none; line-height: 1.4;
}
.badge-comment:hover { background: var(--accent); color: var(--on-accent); }
.badge-comment svg { flex: none; opacity: .9; }

/* card view of the same rows, for narrow screens */
.cardlist { display: none; }
.qcard {
  display: block; text-decoration: none; color: inherit;
  padding: var(--space-4); border-bottom: 1px solid var(--border);
}
.qcard:last-child { border-bottom: 0; }
.qcard:hover { background: var(--dark-red-10); }
.qcard-top { display: flex; flex-wrap: wrap; gap: var(--space-2);
             align-items: center; margin-bottom: var(--space-2); }
.qcard-name { display: flex; flex-direction: column; gap: 2px;
              margin-bottom: var(--space-3); }
.qcard-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
              gap: var(--space-2) var(--space-4); margin: 0; }
.qcard-grid dt { font-size: var(--step-caption); color: var(--text-muted);
                 text-transform: uppercase; letter-spacing: .04em; }
.qcard-grid dd { margin: 0; font-size: var(--step-small); font-weight: var(--fw-medium); }
/* A pair whose value is a sentence rather than a word. In its own full-width
   row so it does not set the column width for every card above it. */
.qcard-wide { grid-column: 1 / -1; }
.listcard { padding: 0; }

/* ---------- what sits beside a name on a queue row ----------
 *
 * Two things moved out of the Status column and into the Employee cell, where
 * both of them actually belong: the Demo label, which is a fact about the
 * PERSON on the row, and the presence line, which is a fact about the ROW. Both
 * are deliberately quieter here than they were as chips in a column of states —
 * the name is what you read, and these annotate it.
 *
 * The demo label keeps the same pill everything else uses, because a synthetic
 * record must never be told apart by how loud its label is; it is only set at
 * the caption size and pulled off the baseline so it reads as an aside to the
 * name rather than as a second line. */
.rowmain strong + .tag, .qcard-name .rowmain .tag {
  padding: 1px 8px; margin-left: var(--space-1); vertical-align: 1px;
}
/* And the presence line is capped where it sits in a table.
 *
 * "Demo Agency Timekeeping Manager editing" is `white-space: nowrap`, and a
 * table cell is shrink-to-fit — so the pill's own `max-width: 100%` resolves
 * against a width the pill is itself setting, and one long display name made the
 * Employee column 328px wide on a board where every other name fits in 200. The
 * pill already truncates with an ellipsis; this is the ceiling that lets it.
 * Character units, not pixels, so it tracks the caption size. */
td .presence-slot { max-width: 22ch; }

/* ============ punch row ============
   Chronological: in → lunch out → lunch in → out. */

.punchwrap {
  container-type: inline-size;
  /* Air below the grid. The fields inside drop their own margins so the grid
     gap can own the spacing BETWEEN them — but that left nothing after the
     grid, so the "no lunch" band sat flush on the inputs at every width and
     the focus ring of the highlighted field ran underneath it. Same rhythm as
     an ordinary .field. */
  margin-bottom: var(--space-4);
  overflow: visible;
}
.punchrow { display: grid; gap: var(--space-3); }
.punchrow > * { min-width: 0; }

/* Four punches pair: in/lunch-out, lunch-in/out. So the row goes 1 → 2 → 4 and
   never 3, which would strand punch out alone on a second line.
   `auto-fit` can't express "skip 3", and a viewport media query can't see this
   column's width — a container query is the only thing that knows both.

   The 14rem ceiling matters as much as the count. A time field holds "07:00 AM"
   and a picker button; `1fr` stretched it to 296px in the two-column range,
   which reads as a broken input rather than a generous one. Columns stop
   growing and align left instead. */
.punchrow { grid-template-columns: repeat(auto-fit, minmax(9rem, 14rem));
            justify-content: start; }
@container (min-width: 20rem) { .punchrow { grid-template-columns: repeat(2, minmax(0, 14rem)); } }
@container (min-width: 35rem) { .punchrow { grid-template-columns: repeat(4, minmax(0, 14rem)); } }
.field.punch { margin-bottom: 0; position: relative; overflow: visible; }
.field.punch:focus-within { z-index: 2; }
.field.punch .lbl { display: flex; flex-direction: column; gap: 1px; }
.sublbl { font-weight: var(--fw-regular); font-size: var(--step-caption);
          color: var(--text-muted); text-transform: none; letter-spacing: 0; }
.field.punch.needed .lbl::after, .field.punch.locked .lbl::after {
  float: none; display: block; margin-top: 2px; font-size: var(--step-caption);
}
.field.disabled { opacity: .45; }
.field.disabled input { background: var(--surface-muted); }

/* ============ layout at width ============ */

/* Below 56rem the queue table hides columns behind a scroll, so swap to cards —
   nothing disappears, everything stays tappable. */
@media (max-width: 59.99rem) {
  .listcard .tablewrap { display: none; }
  /* table() wraps its table in its own .card.flush; hiding only the tablewrap
     leaves that card as a zero-height box that still casts a shadow and a
     margin. Hide the whole card the swap replaces. */
  .listcard .card.flush { display: none; }
  .cardlist { display: block; }
}

/* Large phones in landscape, small tablets. */
@media (min-width: 34rem) {
  .grid2  { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
            gap: var(--space-4); }
  .grid2 > * { min-width: 0; }
  .btnrow { flex-direction: row; }
  .btn.block { display: inline-block; width: auto; }
  .btnrow .btn.block { flex: 1; }
}

/* Desktop. A second column only where it earns its place — supporting detail
   beside the thing you're acting on, rather than a long scroll with the
   context pushed off screen. */
/* Desktop: the tool rail becomes a left column. */
@media (min-width: 60rem) {
  .shell { display: grid; grid-template-columns: 13rem minmax(0, 1fr); align-items: start; }
  .rail {
    position: sticky; top: var(--topbar-h); align-self: start;
    /* Full height MINUS the header, or the rail runs off the bottom of the
       screen by exactly the header's height and its last item is unreachable. */
    height: calc(100dvh - var(--topbar-h));
    overflow-y: auto;
    border-bottom: 0; border-right: 1px solid var(--border);
    padding-top: var(--space-4);
  }
  .rail ul { flex-direction: column; overflow: visible; gap: 2px;
             padding: var(--space-3) var(--space-3); }
  .rail a, .rail .off {
    border-radius: var(--radius-md); padding: var(--space-3) var(--space-4);
    justify-content: space-between; background: transparent;
  }
  .rail a:hover { background: var(--surface-muted); }
  .rail a.on { background: var(--accent); }

  .split {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(280px, 22rem);
    gap: var(--gutter);
    align-items: start;
  }
  .split > .side { position: sticky; top: calc(var(--topbar-h) + var(--gutter)); }
  .split > .side .card:last-child { margin-bottom: 0; }

}

/* Ultrawide: stop the reading column stretching; let tables use the room. */
@media (min-width: 90rem) {
  .wrap { max-width: 900px; }
  .wrap.wide { max-width: none; }
  /* Two columns need room for two columns. The 900px measure is for prose. */
  .wrap.split { max-width: 1280px; }
  .qcard-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* Short viewports — phone in landscape. Trim vertical padding so the primary
   action stays reachable without scrolling. */
@media (max-height: 30rem) and (orientation: landscape) {
  /* Every sticky bar unpins together. A phone in landscape has ~300px of usable
     height; two pinned bars would eat a third of it — and once the topbar is
     static, a rail still offset by --topbar-h would hover mid-screen. */
  .topbar { position: static; }
  .rail   { position: static; }
  .wrap   { padding-block: var(--space-3); }
  .card   { padding-block: var(--space-3); }
  h1      { margin-bottom: var(--space-1); }
}

/* Tiny screens — the label suffix and the label itself collide below ~360px. */
@media (max-width: 22.49rem) {
  .field.needed label.lbl::after, .field.locked label.lbl::after {
    float: none; display: block; margin-top: 2px;
  }
}

/* ============ print ============
   The case file is the thing most likely to be printed — a legal request ends
   in a PDF. Strip the chrome and break sensibly across pages. */

@media print {
  body { background: var(--surface); }
  .topbar, .btn, .btnrow, form, .chev { display: none !important; }
  .wrap { max-width: none; padding: 0; }
  .card { box-shadow: none; border: 1px solid var(--border); break-inside: avoid; }
  .timeline li { break-inside: avoid; }
  a { text-decoration: none; color: inherit; }
}

/* The way out of a form you can't complete. Deliberately at the top, above the
   fields: someone who doesn't recognise the shift shouldn't have to scroll past
   a required punch-out to find out they were allowed to just ask. */
.askout {
  background: var(--surface-muted);
  border-left: 3px solid var(--status-info);
  border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  margin: 0 0 var(--stack);
  color: var(--text-muted);
}
.askout a { font-weight: var(--fw-medium); }

/* A name that is also a route. It stays a heading — same size, same weight —
   and earns an underline on hover rather than announcing itself as a link,
   which at h1 size would read as an error. */
.personlink { color: inherit; text-decoration: none; }
.personlink:hover, .personlink:focus-visible { text-decoration: underline;
  text-underline-offset: 4px; text-decoration-thickness: 2px;
  text-decoration-color: var(--accent); }

/* Core pages sit below the tools, divided from them: People is not a tool you
   open, it's the directory the tools describe. */
.rail-core { margin-top: var(--space-3); padding-top: var(--space-3);
             border-top: 1px solid var(--border); }
@media (max-width: 59.99rem) {
  .rail-core { margin-top: 0; padding-top: 0; border-top: 0;
               border-left: 1px solid var(--border); padding-left: var(--space-2);
               margin-left: var(--space-1); }
}

/* ============ operator ============ */

/* Sections within an area. Distinct from the rail, which moves between areas —
   this moves within one, so it reads as tabs rather than navigation. */
.subnav { display: flex; flex-wrap: wrap; gap: var(--space-2);
          margin-bottom: var(--stack); }
.subnav a {
  padding: 8px var(--space-4); border-radius: var(--radius-pill);
  background: var(--surface); color: var(--text-muted);
  text-decoration: none; font-weight: var(--fw-medium);
  font-size: var(--step-small); border: 1px solid var(--border);
}
.subnav a:hover { color: var(--accent-text); border-color: var(--accent-text); }
.subnav a.on { background: var(--surface-inverse); border-color: var(--surface-inverse); color: var(--text-on-dark); }

/* The role matrix reads as a grid, so the marks have to be unambiguous at a
   glance and still make sense to a screen reader via their title. */
.yes { color: var(--accent); font-size: 1.1em; }
.no  { color: var(--falck-cool-grey); }

/* One key/value pair out of an audit detail blob. */
.kv { display: inline-block; margin-right: var(--space-3); white-space: nowrap; }
.kv em { color: var(--text-muted); font-style: normal; font-weight: var(--fw-medium); }

.nowrap { white-space: nowrap; }

code {
  font-family: var(--font-mono);
  font-size: .85em; background: var(--surface-muted);
  padding: 1px 5px; border-radius: var(--radius-sm);
  white-space: nowrap;
}

/* A link that carries a tag or a code span — the child already has the visual
   weight, so the anchor shouldn't add underlines on top of it. */
a.plain { text-decoration: none; }
a.plain:hover .tag { filter: brightness(1.08); }
a.mono { font-family: var(--font-mono); font-size: .85em; }

/* Secondary "and everything else about this" link beside a primary one. */
.drill { font-size: var(--step-caption); color: var(--text-muted);
         margin-left: var(--space-2); white-space: nowrap; }
.drill:hover { color: var(--accent); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

/* a.stat is defined once, with the filter/stat-card rules above. */

/* ============ touch targets ============
 *
 * 44px is the floor for anything you tap, but applying it everywhere bloats a
 * dense desktop table — a 44px sort link in a table header is absurd with a
 * mouse.
 *
 * `any-pointer: coarse` rather than `pointer: coarse`: a touchscreen laptop
 * reports its *primary* pointer as fine, so `pointer` would leave those users
 * with 20px tap targets. `any-pointer` asks whether a finger is available at
 * all, which is the question that matters.
 *
 * The queue is the page that matters here. A supervisor triages it on a phone
 * between calls, and every one of these was between 18 and 24px.
 */
@media (any-pointer: coarse) {
  .sortlink {
    /* Fill the header cell rather than sitting in the middle of it — the cell
       padding isn't clickable, so the link has to reclaim it. */
    display: flex; min-height: 44px; align-items: center;
    margin: calc(var(--space-3) * -1); padding: var(--space-3);
  }
  .badge-comment { min-height: 44px; padding-inline: var(--space-3); }
  .chip { min-height: 44px; }
  .savedefault { min-height: 44px; }
  .saveform input[type=text] { min-height: 44px; }
}

/* The saved-view "Default" toggle: a label wrapping its own checkbox, so the
   label is already the target — it just needs the height. */
.savedefault { display: inline-flex; align-items: center; gap: var(--space-2); }

/* ============ data map ============
 *
 * A reference table people read rather than scan. Elsewhere a wide table gets a
 * horizontal scroller and that's fine — here it hid the "what it is for" column,
 * which is the only reason the page exists. So these wrap instead.
 */
.datamap .tablewrap { overflow-x: visible; }
/* No scroll container here, so these headers DO stick to the viewport and have
   to clear the page header. The only place the offset belongs. */
.datamap thead th { top: var(--topbar-h); }
.datamap table { table-layout: fixed; width: 100%; }
.datamap td, .datamap th { vertical-align: top; }
/* Identifiers and column names are long and unbreakable; let them break rather
   than force the whole table wider than its container. */
.datamap code { white-space: normal; overflow-wrap: anywhere; }
.datamap td:last-child { width: 45%; }

@media (max-width: 59.99rem) {
  /* Too narrow for four columns of prose — stack each row into a block. */
  .datamap thead { display: none; }
  .datamap tr { display: block; padding: var(--space-3) 0; border-bottom: 1px solid var(--border); }
  .datamap td { display: block; width: auto !important; padding: 2px 0; }
  .datamap td:last-child { color: var(--text-muted); margin-top: var(--space-2); }
}

/* A count beside a rail item. Small, high-contrast, and never the only signal —
   the screen-reader text beside it says what the number means. */
.railbadge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 6px; margin-left: auto;
  border-radius: var(--radius-pill);
  background: var(--accent); color: var(--on-accent);
  font-size: 12px; font-weight: var(--fw-bold); line-height: 1;
}
/* On the active item the rail is already red, so the badge inverts to stay
   legible rather than disappearing into its own background. */
.rail a.on .railbadge { background: var(--surface); color: var(--accent); }

/* ============ queue workflow controls and presence ============
 *
 * These controls are additive to the existing queue styling. The status
 * disclosure keeps the common path compact, but its checkboxes remain native
 * controls with an explicit legend for keyboard and screen-reader users.
 */
.savedviews-head { display: flex; flex-wrap: wrap; align-items: baseline;
                   gap: var(--space-2); }
.savedviews-head strong { font-size: var(--step-small); }
/* WAS `flex: 1 1 100%`, AND THAT ONE DECLARATION IS THE WHOLE RAGGED BAR.
 *
 * A 100% flex-basis inside `form.filterform` claims a full line for itself, so
 * the status disclosure could never share a row with the search field beside
 * it, and the Filter button it pushed down could never share one either. Three
 * controls, three lines, plus a fourth for what `.filterbar` scattered after
 * the form. Nothing was wrong with any of the six controls; they were told to
 * stack.
 *
 * Sized by its own content now — `flex: 0 1 auto` with a floor — so it sits
 * inline like every other control in the row. The open panel is the reason the
 * full-width basis was reached for in the first place: it is 560px tall on a
 * desktop and 970px on a phone, and in flow it shoves the entire queue down the
 * page. So it becomes a popover anchored to the summary, which is what a
 * disclosure holding a screen of checkboxes has to be, with its own scroll
 * rather than the page's.
 *
 * This markup is still live. It stays working until the views move to
 * `.toolbar` and its status chip row, which is where this control is going. */
.status-filter { flex: 0 1 auto; min-width: 11rem; position: relative; }
.status-filter > summary {
  display: inline-flex; align-items: center; min-height: 44px;
  /* 2px, matching every other field edge in the bar. At 1px this was the only
     control drawn on a different weight of line than the search box next to it. */
  padding: 8px var(--space-3); border: 2px solid var(--border-control);
  border-radius: var(--radius-sm); cursor: pointer; font-size: var(--step-small);
  font-weight: var(--fw-bold); color: var(--accent-text);
  list-style-position: inside; white-space: nowrap;
}
.status-filter > summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.status-filter-body {
  position: absolute; z-index: 4; top: calc(100% + var(--space-1)); left: 0;
  display: grid; grid-template-columns: repeat(2, minmax(11rem, 1fr));
  gap: var(--space-4); width: max(100%, min(48rem, 86vw));
  max-height: min(60vh, 32rem); overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: var(--space-3); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-pop);
}
.status-filter fieldset { min-width: 0; margin: 0; padding: 0; border: 0; }
.status-filter legend {
  margin-bottom: var(--space-2); font-size: var(--step-caption);
  font-weight: var(--fw-bold); color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .04em;
}
.status-options { display: grid; gap: 2px; }
.status-option {
  display: flex; align-items: center; gap: var(--space-2); min-height: 40px;
  color: var(--text); font-size: var(--step-small); cursor: pointer;
}
.status-option input { width: 18px; height: 18px; min-height: 0; accent-color: var(--accent); }
/* Mobile-first, and no `margin-left: auto`. An auto margin on a wrapped flex
   line eats every pixel of slack on that line, which is how the order links
   ended up alone at the right edge of a fourth row. They follow the toggle they
   belong beside; the bar decides where the row ends. */
.queue-order {
  display: inline-flex; flex-wrap: wrap; align-items: center; gap: 2px;
  min-height: 44px; font-size: var(--step-small);
}
.queue-order-label { color: var(--text-muted); font-weight: var(--fw-bold); margin-right: var(--space-1); }
.queue-order-link {
  display: inline-flex; align-items: center; min-height: 40px;
  padding: 6px var(--space-2); border-radius: var(--radius-sm);
  color: var(--accent-text); text-decoration: none;
}
.queue-order-link:hover, .queue-order-link.on { background: var(--dark-red-10); text-decoration: underline; }
.queue-order-link.on { font-weight: var(--fw-bold); }
.queue-order-link:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

.presence-slot {
  display: inline-flex; flex-wrap: wrap; gap: 4px; margin-top: 4px;
  max-width: 100%; vertical-align: middle;
}
/* The queue's version of the same fact: a dot and a name, in a table cell, on a
 * row you have not opened yet. Smaller than it was — this is the least
 * important thing in a row that also carries an employee, a date and a status —
 * and the dot pulses rather than the pill changing colour, so twelve rows of
 * these do not read as twelve warnings. Editing takes the same amber the
 * avatar cluster uses on the case bar, so the two say the same thing twice. */
.presence-pill {
  display: inline-flex; align-items: center; gap: var(--space-1);
  width: fit-content; max-width: 100%;
  padding: 2px 8px 2px 6px; border-radius: var(--radius-pill);
  color: var(--text-muted); background: var(--surface-muted);
  font-size: var(--step-caption); font-weight: var(--fw-medium);
  line-height: 1.35; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.presence-pill.is-editing {
  color: var(--status-attention-text); background: var(--surface-attention);
}
.presence-dot {
  position: relative; width: 8px; height: 8px; flex: none; border-radius: 50%;
  background: currentColor;
}
/* The ring, static by default — which is the reduced-motion state — and set
   moving by the `prefers-reduced-motion: no-preference` block further down,
   alongside the avatar cluster's, so both pulses are declared in one place. */
.presence-dot::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  box-shadow: 0 0 0 2px currentColor; opacity: .3;
}
.presence-pill.is-editing .presence-dot { color: var(--status-warn); }
.presence-panel {
  margin-bottom: var(--stack); padding: var(--space-4);
  background: var(--surface-muted); border: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.presence-panel-head { display: flex; flex-wrap: wrap; align-items: baseline;
                        justify-content: space-between; gap: var(--space-3); }
.presence-panel h2 { margin: 0; font-size: var(--step-h3); }
.presence-panel-head p { margin: 2px 0 0; }
.presence-summary { color: var(--accent-text); font-weight: var(--fw-bold); }
.presence-list { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-3); }
.presence-empty { margin: var(--space-3) 0 0; }

.staff-form-errors {
  margin-bottom: var(--space-3); padding: var(--space-3);
  border-left: 4px solid var(--status-error); border-radius: var(--radius-sm);
  /* Was --status-error-bg / --status-error-text. Neither exists, and an
     undefined custom property drops its declaration rather than falling back —
     so the box that lists why a staff correction would not save rendered as
     white text's default on the card's own white, i.e. blank. The real pair is
     the one `.alert.err` already uses. */
  background: var(--surface-error); color: var(--status-error);
}
.staff-form-errors ul { margin-bottom: 0; }
.staff-save-row { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); margin-top: var(--space-4); }
.outreach-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.outreach-history { display: grid; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
.outreach-history li { display: grid; grid-template-columns: auto 1fr; gap: 2px var(--space-2);
  padding: var(--space-3); border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-muted); }
.outreach-history li > span { color: var(--text-muted); font-size: var(--step-small); }
.outreach-history li > p { grid-column: 1 / -1; margin: var(--space-1) 0 0; white-space: pre-wrap; }

@media (max-width: 40rem) {
  .status-filter-body { grid-template-columns: 1fr; }
  .queue-order { width: 100%; margin-left: 0; }
  .staff-save-row { align-items: stretch; flex-direction: column; }
  .staff-save-row .btn, .outreach-actions .btn { width: 100%; }
}

/* A countdown. The number is the message, so it gets the size — the label is
   support. Colour is a status signal here, not decoration, so it ships with the
   words beside it rather than relying on the colour alone. */
.countdown {
  display: flex; align-items: center; gap: var(--space-4);
  padding: var(--space-4); margin: var(--stack) 0;
  border-radius: var(--radius-md); background: var(--surface-muted);
  border-left: 4px solid var(--text-muted);
}
.countdown .cd-num { font-size: clamp(2rem, 6vw, 3rem); font-weight: var(--fw-heavy);
                     line-height: 1; color: var(--text); }
.countdown .cd-txt { display: flex; flex-direction: column; gap: 2px; }
.countdown .cd-txt .small { color: var(--text-muted); }

.countdown.urgent { border-left-color: var(--accent); background: var(--dark-red-10); }
.countdown.urgent .cd-num { color: var(--accent); }
.countdown.soon   { border-left-color: var(--status-warn); }
.countdown.past   { opacity: .6; }

/* A documentation topic. Outstanding ones sit in the same list as written ones,
   dimmed but present — hiding them would make the docs look complete. */
.doctopic { padding: var(--space-4) 0; border-top: 1px solid var(--border); }
.doctopic:first-of-type { border-top: 0; }
.doctopic-head { display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; }
.doctopic-head h3 { margin: 0; }
.doctopic p { margin: var(--space-2) 0; }
.doctopic code { font-size: .85em; }

/* The filter row above a reference list. One line, always in the same place, so
   it's found without looking. */
.filterbox {
  display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap;
  margin-bottom: var(--stack);
}
.filterbox .filterinput {
  flex: 1 1 18rem; min-height: 44px; padding: 10px var(--space-3); margin: 0;
}
.filterbox .filtercount { white-space: nowrap; }

/* ============ notice editor ============ */

/* A fieldset with no border — grouping for screen readers without a box that
   competes with the card it already sits in. */
.plainset { border: 0; padding: 0; margin: 0; }
.plainset legend { padding: 0; margin-bottom: var(--space-3); }

/* ============ operator: add a person (Access page) ============ */

.add-staff { margin-bottom: var(--stack); }
.add-staff > summary { display: inline-flex; align-items: center; list-style: none;
                       margin-bottom: var(--space-3); }
.add-staff > summary::-webkit-details-marker { display: none; }
.add-staff-fields { display: grid; gap: 0 var(--space-4);
                    grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); }
.add-staff-areas { display: grid; gap: var(--space-2);
                   grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); }
.switch-staff > summary { list-style: none; }
.switch-staff > summary::-webkit-details-marker { display: none; }
.switch-staff[open] form { display: grid; gap: var(--space-2); margin-top: var(--space-2);
                           min-width: min(100%, 16rem); }

/* Choosing the kind comes first, because the answer decides which of the other
   questions are worth asking. Cards rather than a dropdown: three options with
   an explanation each, where a <select> shows one at a time and hides the
   reason to pick it. */
.kindgrid { display: grid; gap: var(--space-3);
            grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); }
.kindcard {
  display: block; cursor: pointer;
  padding: var(--space-4);
  border: 2px solid var(--border); border-radius: var(--radius-md);
  background: var(--surface);
  transition: border-color .12s, background .12s;
}
.kindcard:hover { border-color: var(--dark-red-40); }
.kindcard.on { border-color: var(--accent); background: var(--dark-red-10); }
.kindcard input { position: absolute; opacity: 0; pointer-events: none; }
/* Focus has to be visible even though the radio itself isn't. */
.kindcard:focus-within { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.kindcard-title { display: block; font-weight: var(--fw-bold); margin-bottom: 2px; }
.kindcard-blurb { display: block; font-size: var(--step-caption); color: var(--text-muted); }

/* Poll options: a row per option with its own remove, rather than four fixed
   boxes that were mostly empty and gave no way to add a fifth. */
.polloption { display: flex; gap: var(--space-2); align-items: center;
              margin-bottom: var(--space-2); }
.polloption input { margin: 0; flex: 1; }
.polloption .optremove { flex: none; }
.polloption .optremove[disabled] { opacity: .35; cursor: not-allowed; }

/* Areas as chips — a short list of short names, where stacked checkboxes waste
   a screen of height. */
.areagrid { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.areachip { display: inline-flex; align-items: center; gap: var(--space-2);
            min-height: 44px; padding: 0 var(--space-4);
            border: 2px solid var(--border); border-radius: var(--radius-pill);
            cursor: pointer; font-weight: var(--fw-medium); }
.areachip:has(input:checked) { border-color: var(--accent);
                               background: var(--dark-red-10); color: var(--accent-text); }
.areagrid.disabled { opacity: .4; pointer-events: none; }
.areachip input { width: 18px; height: 18px; min-height: 0; margin: 0; }

/* A control that sits inside a table cell — select plus its save button on one
   line, so a row stays a row. */
.inlineform { display: flex; gap: var(--space-2); align-items: center; }
.inlineform select { min-width: 0; flex: 1; min-height: 40px; padding: 6px var(--space-2); }
.inlineform .btn { flex: none; }
@media (any-pointer: coarse) {
  .inlineform select, .inlineform .btn { min-height: 44px; }
}

/* ---------- Scheduling local prototype ---------- */

.sch-wrap .subnav { margin-bottom: var(--stack); }
.sch-private-note, .sch-scope-note {
  display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4);
  margin-bottom: var(--stack); border-radius: var(--radius-md); background: var(--surface-muted);
  color: var(--status-attention-text); font-size: var(--step-small);
}
.sch-scope-note { align-items: flex-start; border-left: 4px solid var(--falck-response-yellow); }
.sch-scope-note span { color: var(--text-muted); }
.sch-kicker { display: block; margin-bottom: var(--space-2); color: var(--text-muted);
  font-size: var(--step-caption); font-weight: var(--fw-bold); text-transform: uppercase; letter-spacing: .07em; }
.sch-hero-grid, .sch-admin-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--stack); }
.sch-hero-grid > .card, .sch-admin-grid > .card { margin-bottom: 0; }
.sch-admin-grid { margin-top: var(--stack); }
.sch-admin-grid .card { display: flex; flex-direction: column; min-height: 230px; }
.sch-admin-grid .card .btn { margin-top: auto; align-self: flex-start; }
.sch-rank-card { background: var(--surface-inverse); color: var(--text-on-dark); }
.sch-rank-card .muted, .sch-rank-card .sch-kicker, .sch-rank-card .row dt { color: rgb(255 255 255 / .72); }
.sch-rank-card .rows, .sch-rank-card .row { border-color: rgb(255 255 255 / .18); }
.sch-rank { display: flex; align-items: flex-start; gap: 2px; font-size: clamp(3.5rem, 8vw, 6rem);
  line-height: .9; font-weight: var(--fw-heavy); font-variant-numeric: tabular-nums; }
.sch-rank span { font-size: .38em; color: var(--falck-response-yellow); }
.sch-card-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-4); margin-bottom: var(--space-3); }
.sch-card-head h2 { margin-bottom: 0; }
.sch-inline-state, .sch-offer-lead { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3); margin: var(--space-4) 0; }
.sch-choice-list { display: grid; gap: var(--space-2); }
.sch-choice { display: grid; grid-template-columns: minmax(0, 1fr) 6rem; gap: var(--space-4); align-items: center; margin: 0; }
.sch-choice[data-ranked="true"] { box-shadow: inset 4px 0 0 var(--falck-vibrant-blue), 0 1px 3px rgb(0 0 0 / .07); }
.sch-choice[data-duplicate="true"] { background: var(--surface-error); box-shadow: inset 4px 0 0 var(--status-error), 0 1px 3px rgb(0 0 0 / .07); }
.sch-choice h2 { margin-bottom: var(--space-1); }
.sch-choice p { margin-bottom: 0; }
.sch-choice label { color: var(--text-muted); font-size: var(--step-caption); font-weight: var(--fw-bold); text-transform: uppercase; }
.sch-choice input { margin-top: var(--space-1); text-align: center; font-size: var(--step-h3); font-weight: var(--fw-bold); }
.sch-choice[data-duplicate="true"] input { border-color: var(--status-error); }
.sch-rank-feedback { min-height: 1.5em; margin: calc(-1 * var(--space-2)) 0 var(--space-3); font-size: var(--step-small); }
.sch-rank-feedback.is-error { color: var(--status-error); font-weight: var(--fw-bold); }
.sch-sticky-save { position: sticky; bottom: var(--space-3); z-index: 5; display: flex;
  align-items: center; justify-content: space-between; gap: var(--space-4); margin-top: var(--stack);
  padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); background: var(--surface-inverse); color: var(--text-on-dark);
  box-shadow: 0 10px 30px rgb(0 0 0 / .2); }
.sch-sticky-save .btn { min-height: 44px; padding-block: 9px; }
.sch-steps { margin: 0; padding-left: 1.25rem; }
.sch-steps li { padding: var(--space-2) 0; }
.sch-flow { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-3); }
.sch-flow div { display: flex; align-items: center; gap: var(--space-2); min-width: 0; color: var(--text-muted); font-size: var(--step-small); }
.sch-flow strong { flex: none; display: inline-flex; width: 2rem; height: 2rem; align-items: center; justify-content: center;
  border-radius: 50%; background: var(--accent); color: var(--on-accent); }
.sch-offer-lead { margin: 0 0 var(--space-1); }

@media (max-width: 59.99rem) {
  .sch-flow { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 45.99rem) {
  .sch-hero-grid, .sch-admin-grid { grid-template-columns: 1fr; }
  .sch-choice { grid-template-columns: minmax(0, 1fr) 5rem; }
  .sch-card-head { flex-direction: column; }
  .sch-flow { grid-template-columns: 1fr; }
  .sch-sticky-save span { display: none; }
  .sch-sticky-save .btn { width: 100%; }
}

/* ══════════════════════════════════════════════════════════════════════════
   Components that were emitted but never styled.

   Each selector below is produced by a view in core/ or modules/ and had no
   rule anywhere in this file, so it rendered as unstyled default markup. They
   are grouped here rather than scattered because they were found together, in
   one audit, and because the shared kit shipping unstyled components is what
   pushed modules into hand-rolling their own markup in the first place.
   ══════════════════════════════════════════════════════════════════════════ */

/* ---- reading measure -------------------------------------------------- */
/* `wrap narrow` is emitted by every auth screen and by problem()/notice().
   `.narrow` had no rule, so all of them silently fell back to the 780px base.
   A single column of prose or one short form wants less than that. */
.wrap.narrow { max-width: 34rem; }

/* ---- banner tones ----------------------------------------------------- */
/* `.banner.urgent` existed but repeated the base background, so it was a
   no-op; `.banner.alert` — the tone Notices uses for a mandatory unread — had
   no rule at all, and the unscoped `.alert` component matched it instead,
   shrinking the lead text. Both now read as different from calm at a glance,
   without leaving the palette. */
.banner.urgent,
.banner.alert {
  background: var(--falck-dark-red);
  box-shadow: inset 0 0 0 1px var(--dark-red-70);
}
.banner.urgent .banner-eyebrow,
.banner.alert .banner-eyebrow { color: var(--status-attention-bg); opacity: 1; }
/* The banner is a dark surface, so a tag styled for the white card disappears
   into it — `.tag.info` was navy on navy. On any banner, tags invert. */
.banner .tag {
  background: rgba(255, 255, 255, .16); color: var(--text-on-dark);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .35);
}

/* ---- nested cards ----------------------------------------------------- */
/* A card inside a card is usually a mistake, but it happens legitimately when
   a section composes a sub-panel. Without this the padding and shadow stack
   and the inner card reads as a raised box on a raised box. */
.card .card {
  box-shadow: none; border: 1px solid var(--border);
  padding: var(--space-4); background: var(--surface);
}

/* ---- timeline --------------------------------------------------------- */
/* timeline() is a shared kit export with exactly one consumer (the case file).
   It had no base rules: the only declaration was a grid-template-columns
   inside a min-width query, with no display:grid anywhere, so it rendered as a
   default numbered <ol> at every width. Mobile-first: stacked by default, two
   columns once there is room (that existing query now has something to act on). */
.timeline { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-4); }
.timeline li {
  display: grid; gap: var(--space-1);
  padding-bottom: var(--space-4); border-bottom: 1px solid var(--border);
}
.timeline li:last-child { padding-bottom: 0; border-bottom: 0; }
/* Once there is room, the timestamp moves into its own column so the entries
   line up and become scannable. This is the rule that used to live alone in an
   earlier media query with nothing to act on. */
@media (min-width: 34rem) {
  .timeline li { grid-template-columns: minmax(140px, auto) 1fr; gap: var(--space-4); }
}
.tl-when {
  font-size: var(--step-caption); color: var(--text-muted);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* `justify-items: start` matters: the body holds a tag and a name as separate
   grid items, and grid stretches its items by default — without this the status
   pill spans the full column and reads as a banner rather than a tag. */
.tl-body { display: grid; gap: var(--space-1); justify-items: start; }
.tl-body p { margin: 0; }
/* Audit detail is machine-written — `exception=missing_out; in=2026-08-05…` —
   with long unbroken runs that push the card wider than the phone otherwise. */
.timeline { overflow-wrap: anywhere; }

/* ---- comment thread --------------------------------------------------- */
/* Also a shared kit export that shipped with no styling. */
.comments { display: grid; gap: var(--space-3); margin-top: var(--space-3); }
.comment {
  padding: var(--space-3); border-radius: var(--radius-md);
  background: var(--surface-muted);
}
/* Staff and employee need to be told apart at a glance in a case file. */
.comment.staff { background: var(--surface); border: 1px solid var(--border); }
.comment-head {
  display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-2);
  align-items: baseline; margin-bottom: var(--space-1);
}
.comment p { margin: 0; }

/* ---- notice body ------------------------------------------------------ */
/* The body copy of every notice. Had no rule, so it inherited nothing and its
   paragraphs ran together. */
.noticebody { display: grid; gap: var(--space-3); }
.noticebody p { margin: 0; }

/* ---- legal-hold thread ------------------------------------------------ */
.thread-item {
  display: grid; gap: var(--space-1);
  padding: var(--space-3) 0; border-bottom: 1px solid var(--border);
}
.thread-item:last-child { border-bottom: 0; }

/* ---- scheduling action card ------------------------------------------- */
.sch-action-card {
  display: flex; flex-wrap: wrap; gap: var(--space-3);
  align-items: center; justify-content: space-between;
}

/* Testimony in a case file: what someone actually said, as opposed to the
   fields around it. Full width and left-aligned, because `rows()` right-aligns
   its values for figures and that is wrong for three sentences of prose. */
.casesaid { margin-top: var(--space-3); }
.casesaid .lbl { margin: 0 0 var(--space-1); }
.casesaid p:last-child {
  margin: 0; padding-left: var(--space-3);
  border-left: 2px solid var(--border-strong);
  
}

/* ---- sortable table headers ------------------------------------------- */
/* `table()` renders a sortable column as a button inside the <th> so it is
   reachable by keyboard and announced as pressable. `aria-sort` on the <th>
   carries the state for screen readers; the arrow is decorative. */
.card.flush { padding: 0; }
/* The padding moved into `.empty-state`, which `table()` now wraps the empty
   row in — the cell only has to stop being `nowrap`, since every other table
   cell is and this one holds a sentence. */
.tablempty { padding: 0; white-space: normal; }

th .th-sort {
  display: inline-flex; align-items: center; gap: var(--space-1);
  width: 100%; min-height: 44px;         /* the header IS the tap target */
  background: none; border: 0; padding: 0;
  font: inherit; color: inherit; text-align: left;
  text-transform: inherit; letter-spacing: inherit;
  cursor: pointer;
}
th .th-sort:hover { color: var(--accent-text); }
th .th-sort:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* Reserve the arrow's width at all times so the header row does not reflow
   when a column becomes the sorted one. */
.th-arrow { width: .7em; flex: none; opacity: .35; }
.th-arrow::after { content: '↕'; }
th[aria-sort="ascending"]  .th-arrow { opacity: 1; }
th[aria-sort="ascending"]  .th-arrow::after { content: '↑'; }
th[aria-sort="descending"] .th-arrow { opacity: 1; }
th[aria-sort="descending"] .th-arrow::after { content: '↓'; }
th[aria-sort="ascending"] .th-sort,
th[aria-sort="descending"] .th-sort { color: var(--accent-text); }

/* ---- record header ----------------------------------------------------- */
/* detailHeader(): the heading block for a single record. Replaces six
   hand-built copies that each nudged the <h1> with an inline margin-top
   because the tag above it left no room. The gap belongs to the container. */
.detailhead { display: grid; gap: var(--space-2); }
.detailhead h1 { margin: 0; }
.detailhead-eyebrow { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.detailhead-meta, .detailhead-lead { margin: 0; }
.detailhead-actions { margin-top: var(--space-2); }

/* ---- charts ------------------------------------------------------------ */
/* A chart wider than its card scrolls sideways in its own lane, the same
   bargain wide tables already make. NOT .tablewrap: that is display:none under
   60rem inside .listcard, which would blank a chart on phones. */
.chart-scroll {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  border-radius: var(--radius-md);
}
.chart-scroll:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.chart-tablewrap { overflow-x: auto; -webkit-overflow-scrolling: touch; border-radius: var(--radius-md); }
.chart-fallback { margin-top: var(--space-3); }
.chart-fallback > summary {
  cursor: pointer; display: inline-block; padding: var(--space-2) 0;
  min-height: 44px;
}
.chart-fallback > summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.chart-fallback[open] > summary { margin-bottom: var(--space-2); }
/* An inline <svg> sits on the text baseline by default, which leaves a few
   pixels of gap under every chart. */
.chart-svg, .chart-spark { display: block; }

/* THIS RULE IS THE NO-JAVASCRIPT FALLBACK. It is not how the chart is normally
 * sized, and changing it will not change what most readers see.
 *
 * A chart arrives drawn at an intrinsic 720px with a matching viewBox, and
 * core/charts.js ships a small script beside it that re-runs the SAME layout at
 * the lane's measured width and re-states width, height, min-width and max-width
 * inline. Inline wins, so for anyone running scripts the chart is exactly as
 * wide as its card at every width and one user unit is one CSS pixel — which is
 * what keeps a --step-caption axis label at --step-caption on a 1600px monitor.
 *
 * What is left here is the answer for the reader that script never reaches, and
 * both limits are arithmetic on the type under the viewBox's uniform scale:
 *
 *   floor — `max-content` resolves to the SVG's own width attribute, so the
 *     drawing is never rendered smaller than it was laid out. Measured at a
 *     314px lane a scaled 720px chart came out at 0.72x, putting a
 *     --step-caption axis label at 8.7px, which defeats the point of that step
 *     clamping at 12px. Below the drawn width the chart holds its size and
 *     .chart-scroll takes it sideways — legible and awkward beats fitted and
 *     unreadable on a phone.
 *
 *   ceiling — 880px is 1.22x of 720, and 1.22 x 13px is 15.9px: at full stretch
 *     an axis label is body-sized and never louder than the prose above it.
 *     Measured at a 2200px window an unbounded chart ran to 1857px, i.e. 2.58x,
 *     with 33.5px axis labels level with the page's h1 and 5.2px strokes.
 *
 * Scoped to charts inside .chart-scroll, which is exactly the set that has a
 * lane to fill. A sparkline has none: it is 120x28 of decoration inside a stat
 * tile, and while it wore .chart-svg this rule inflated it to 520x121 — over
 * the number it annotates, and wide enough to push the phone layout sideways. */
.chart-scroll > .chart-svg { width: 100%; height: auto; min-width: max-content; max-width: 880px; }

/* The default preserveAspectRatio scales proportionally, which is what is wanted:
   the chart gets bigger rather than stretched. Forcing "none" would widen the
   drawing without widening the strokes and text with it. */
/* The line chart's end-point labels and last gridline sit fractionally outside
   the viewBox and would otherwise be clipped. */
.chart-svg-overflow { overflow: visible; }

/* The updates list on the employee home. Deliberately plain: it is a short list
   of links, and anything more decorated would compete with the shift cards
   underneath, which are what the page is actually for. */
.tk-updates { list-style: none; margin: 0; padding: 0; }
.tk-updates li + li { margin-top: .4rem; padding-top: .4rem; border-top: 1px solid var(--border); }
.tk-updates a { font-weight: var(--fw-bold); }

/* Roadmap goals. A list of claims, each with its reasoning underneath — the note
   matters as much as the status, so it is not tucked away smaller than it needs. */
.roadmap-overview-head { display: flex; justify-content: space-between; gap: var(--space-4);
  align-items: flex-start; flex-wrap: wrap; }
.roadmap-overview-head p { margin: .15rem 0 0; }
.roadmap-tools { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.roadmap-toggle { display: inline-flex; align-items: center; gap: .4rem; font-size: var(--fs-body-sm);
  color: var(--text-muted); cursor: pointer; }
.roadmap-index { list-style: none; margin: var(--space-4) 0 0; padding: 0; display: grid;
  gap: var(--space-2); grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr)); }
.roadmap-index a { display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto auto;
  column-gap: var(--space-2); row-gap: .25rem; align-items: center; padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border); border-radius: var(--radius-md); text-decoration: none;
  color: inherit; background: var(--surface); }
.roadmap-index a:hover { border-color: var(--border-strong); }
.roadmap-index-version { font-weight: var(--fw-heavy); color: var(--accent-text); }
.roadmap-index-name { font-weight: var(--fw-bold); min-width: 0; overflow-wrap: anywhere; }
.roadmap-index .roadmap-bar { grid-column: 1 / -1; }
.roadmap-index .small { grid-column: 1 / -1; }

/* One bar per release: how much of it is real yet. Segments are proportional
   to goal counts; the colours are the status tones, so the bar reads the same
   way the chips do. */
.roadmap-bar { display: flex; height: 6px; border-radius: var(--radius-pill); overflow: hidden;
  background: var(--surface-muted); min-width: 6rem; }
.roadmap-bar-seg.is-shipped { background: var(--status-ok); }
.roadmap-bar-seg.is-partial { background: var(--status-warn); }
.roadmap-bar-seg.is-planned { background: var(--border-strong); }
.roadmap-bar-seg.is-unsure { background: var(--border); }

.roadmap-release { scroll-margin-top: var(--space-6); }
.roadmap-release-head { display: flex; justify-content: space-between; align-items: flex-end;
  gap: var(--space-4); flex-wrap: wrap; margin-bottom: var(--space-2); }
.roadmap-release-head .eyebrow { margin: 0; }
.roadmap-release-head h2 { margin: 0; }
.roadmap-release-tally { display: grid; gap: .25rem; min-width: 14rem; justify-items: end; }
.roadmap-release-tally .roadmap-bar { width: 14rem; }
.roadmap-intent { color: var(--text-muted); margin: 0 0 var(--space-4); }

/* Goals. The line a reader scans is status, title, one sentence. Everything a
   claim rests on is one click away underneath, and never shown smaller than
   body text once opened — the reasoning is the point of the page. */
.roadmap-goals { list-style: none; margin: 0; padding: 0; }
.roadmap-goal { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: var(--space-3);
  padding: var(--space-3) 0; border-top: 1px solid var(--border); }
.roadmap-goal:first-child { border-top: 0; padding-top: 0; }
.roadmap-goal-status { padding-top: .1rem; }
.roadmap-goal-title { display: flex; align-items: baseline; gap: var(--space-2); flex-wrap: wrap; }
.roadmap-date::before { content: '·'; margin-right: var(--space-2); }
.roadmap-gist { margin: .2rem 0 0; }
.roadmap-more { margin-top: .35rem; }
.roadmap-more > summary { cursor: pointer; color: var(--accent-text); font-size: var(--fs-body-sm);
  font-weight: var(--fw-bold); list-style: none; display: inline-flex; align-items: center; gap: .35rem; }
.roadmap-more > summary::-webkit-details-marker { display: none; }
.roadmap-more > summary::before { content: ''; width: .45rem; height: .45rem; border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor; transform: rotate(-45deg); transition: transform .15s; }
.roadmap-more[open] > summary::before { transform: rotate(45deg); }
.roadmap-more > summary:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.roadmap-more > p { margin: var(--space-2) 0 0; }
.roadmap-unfinished .roadmap-goal.is-shipped { display: none; }
@media (max-width: 40rem) {
  .roadmap-goal { grid-template-columns: 1fr; gap: .25rem; }
  .roadmap-release-tally { justify-items: start; }
}

/* Contact values are deliberately shown as pairs: the locally preferred
   destination and the directory source that remains on record. */
.contact-summary { margin: 0 0 var(--space-4); }
.contact-line { display: grid; grid-template-columns: minmax(8rem, 12rem) minmax(0, 1fr);
  gap: var(--space-2) var(--space-4); padding: var(--space-2) 0;
  border-bottom: 1px solid var(--border); }
.contact-line dt { font-weight: var(--fw-bold); }
.contact-line dd { margin: 0; overflow-wrap: anywhere; }
.contact-notice { margin: var(--space-6) 0; padding: var(--card-pad);
  border: 1px solid var(--border-control); border-radius: var(--radius-md); }
.contact-notice legend { padding-inline: var(--space-2); font-weight: var(--fw-heavy); }
.home-contact-panel .card { height: 100%; }

/* Personal dashboard. The profile is the primary surface; module panels are
   summaries beneath it, not a second aggregate dashboard. */
.profile-dashboard { max-width: 1180px; }
.profile-dashboard > .card:first-child { border-top: 4px solid var(--falck-red); }
.profile-identity {
  display: flex; align-items: center; gap: var(--space-4);
  margin-bottom: var(--space-6);
}
.profile-avatar {
  width: clamp(4rem, 12vw, 6rem); aspect-ratio: 1; flex: none;
  display: grid; place-items: center; border-radius: 50%;
  background: var(--surface-inverse); color: var(--text-on-dark);
  font-size: clamp(1.5rem, 4vw, 2.25rem); font-weight: var(--fw-heavy);
  letter-spacing: -.03em;
}
.profile-title { min-width: 0; }
.profile-label {
  color: var(--accent-text); font-size: var(--step-caption); font-weight: var(--fw-bold);
  letter-spacing: .06em; text-transform: uppercase; margin-bottom: var(--space-1);
}
.profile-title h2 { font-size: var(--step-h1); margin-bottom: var(--space-1); }
.profile-title p { color: var(--text-muted); margin: 0; }
.profile-note {
  margin-top: var(--space-4); color: var(--text-muted); font-size: var(--step-small);
}
.employment-card { margin-top: var(--space-6); overflow: hidden; }
.employment-card-head, .employment-section-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-3);
}
/* --space-5 does not exist; the ramp is 1, 2, 3, 4, 6, 8, 12. Written with it,
   this margin dropped entirely and the card head sat flush on the metrics. */
.employment-card-head { margin-bottom: var(--space-6); }
.employment-card-head h2, .employment-section-head h3 { margin: 0; }
.employment-section-head { align-items: baseline; margin-bottom: var(--space-3); }
.employment-section-head h3 { font-size: var(--step-body); }
.employment-section-head span { color: var(--text-muted); font-size: var(--step-small); }
.employment-metrics {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-2); margin: 0;
}
.employment-metric {
  min-width: 0; padding: var(--space-3); border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--surface-muted);
}
.employment-metric dt, .employment-facts span {
  color: var(--text-muted); font-size: var(--step-small); font-weight: var(--fw-bold);
}
.employment-metric dd { display: flex; align-items: baseline; gap: .35rem; margin: var(--space-1) 0 0; }
/* Was --navy, which is not a token. Figures on a profile take the brand's Dark
   Blue for the same reason the dashboard's do: --text is Falck dark red, so a
   figure left to inherit is the same maroon as the prose around it and nothing
   can be more important than anything else. Named directly rather than through
   --surface-inverse, which means "a dark ground to put light text on" and is
   the wrong idea for ink. */
.employment-metric dd strong { color: var(--falck-dark-blue); font-size: clamp(1.35rem, 3vw, 2rem);
  font-variant-numeric: tabular-nums; line-height: 1; }
.employment-metric dd span { color: var(--text-muted); font-size: var(--step-caption); }
.employment-observation { margin: var(--space-2) 0 var(--space-6); color: var(--text-muted);
  font-size: var(--step-small); }
.employment-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-3); padding-block: var(--space-4); border-block: 1px solid var(--border); }
.employment-facts > div { display: grid; gap: var(--space-1); }
.employment-facts strong { font-size: var(--step-body); }
.profile-unlinked {
  border-left: 4px solid var(--status-attention-bg); background: var(--surface-muted);
  padding: var(--space-4); border-radius: var(--radius-sm);
}
.profile-unlinked p { margin-top: var(--space-1); }
.profile-records { margin-top: var(--space-8); }
.profile-records-head { margin-bottom: var(--space-4); }
.profile-records-head h2 { font-size: var(--step-h2); margin-bottom: var(--space-1); }
.profile-records-head p { color: var(--text-muted); }
.profile-panels { display: grid; grid-template-columns: 1fr; gap: var(--stack); }
.profile-panels > .card { margin-bottom: 0; min-width: 0; }
@media (min-width: 52rem) {
  .profile-panels { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
}
@media (max-width: 42rem) {
  .employment-card-head, .employment-section-head { flex-direction: column; }
  .employment-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 25rem) {
  .employment-metrics, .employment-facts { grid-template-columns: 1fr; }
}

/* Trends explanations are working definitions, not captions. Give them a
   longer measure on wide screens while keeping phone lines naturally short. */
.trend-explanations, .trend-explanation { max-width: none; }

/* ══════════════════════════════════════════════════════════════════════════
   THE TOOLBAR ABOVE A LIST
   ══════════════════════════════════════════════════════════════════════════

   Six control shapes accumulated in the review queue's filter bar and none of
   them agreed with the others: heights of 40 and 44, borders of 1px and 2px,
   radii of 4px and pill, and three different opinions about who owns the free
   space. The result rendered as four ragged rows — see the note on
   `.status-filter` above for the single declaration that caused the worst of
   it.

   The rules this block enforces, and they are not negotiable per-caller:

     · 44px minimum on everything you can hit. Not "on touch" — 44 everywhere,
       because a filter bar is used with a finger on the queue and a mouse on
       the same page ten minutes later, and a control that changes size between
       them is two controls.
     · 2px --border-control on anything that reads as a field. That is the only
       edge in this palette that clears WCAG 1.4.11's 3:1, which is what makes a
       control look like a control.
     · 4px (--radius-sm) for fields and for chips-that-are-filters. The pill is
       reserved for buttons. A pill-shaped filter and a pill-shaped button are
       indistinguishable at a glance, and one of them navigates.
     · The submit is quiet. --btn's filled Falck Red would make "Filter" the
       loudest thing on a page whose actual job is approving corrections.

   Mobile-first: everything wraps by default and the trailing group takes its
   own line; the bar becomes one row when there is room for one.  */

/* TWO COLUMNS, NOT ONE WRAPPING FLEX LINE.
 *
 * The filters own the left column and wrap inside it; the ordering is anchored
 * to the top right and stays out of their way. In one flex container the
 * trailing Order control is a sibling of the entire wrapping block, so it
 * centres itself against every line and lands beside whichever one is widest —
 * measured at 1280px it sliced the old status chip row in half, which reads as a
 * rendering fault rather than as "scroll for more". One column below 46rem,
 * where there is no room to anchor anything.
 *
 * WITH SELECTS RATHER THAN A CHIP ROW, THE LEFT COLUMN IS ONE LINE. Nothing in
 * it claims a full line any more, so search / area / status / flagged / Filter
 * sit on one row beside the ordering from about 1200px up, and below that they
 * wrap onto two and then three lines. They wrap — they never scroll sideways:
 * every control is either fixed-width and narrow enough to fit a 320px screen,
 * or flexible with `min-width: 0` so it shrinks instead of pushing the page
 * wide. `.toolbar-chips` is still a scroll container and still claims its line,
 * because the archive's five endings are a set you can see all of at once. */
.toolbar {
  display: grid; grid-template-columns: minmax(0, 1fr);
  align-items: start; gap: var(--space-2) var(--space-3);
  margin-bottom: var(--stack);
}
.toolbar-main, .toolbar-end {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2) var(--space-3); min-width: 0; margin: 0;
}
/* 44px so the trailing group's baseline matches the search field's on the first
   row, whether or not it happens to hold a 44px control itself. */
.toolbar-end { min-height: 44px; }
@media (min-width: 46rem) {
  .toolbar { grid-template-columns: minmax(0, 1fr) auto; }
  .toolbar-end { justify-content: flex-end; }
}

/* SMALL BASES THAT GROW, NOT LARGE ONES THAT WRAP. Flex wraps on an item's
   flex-basis, not on how narrow it could be made — so a comfortable basis is a
   line break, and the only way to hold six controls on one row at 1280 (where
   the rail leaves the card about 950px) is to declare each one's minimum and
   share the slack out afterwards. The search takes three shares to a select's
   one, because that is the field a name is typed into. */
.toolbar-search {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
  flex: 3 1 10rem; min-width: 0;
}
.toolbar .toolbar-input {
  flex: 1 1 6rem; width: auto; min-width: 0;
  min-height: 44px; padding: 10px var(--space-3); margin: 0;
}
.toolbar-count { white-space: nowrap; }
/* `min-width: 0` is the load-bearing half: a <select> is otherwise as wide as
   its widest option, so "Needs investigation (0)" would set the width of the
   status picker at every screen size and push the bar onto a second row for the
   sake of a status nobody has. It truncates instead, and the full text is in the
   open picker, which is where it is being read. `max-width` stops the same
   control from sprawling once there is slack to hand out. */
.toolbar .toolbar-select {
  flex: 1 1 8rem; width: auto; min-width: 0; max-width: 12rem;
  min-height: 44px; padding: 10px var(--space-3);
  text-overflow: ellipsis;
}
/* A select whose values are sentences, not codes. Without this the queue's Area
   ("All areas", "US13") and its Status ("Needs your action (0)") are given the
   same width, so one truncates while the other sits half empty. */
.toolbar .toolbar-select.is-wide { flex: 2 1 11rem; max-width: 18rem; }
/* On the narrowest phone the card is 260px wide, and an 8rem basis puts Area and
   Status on a line each — five bands for six controls. Two selects side by side
   is the same information in one fewer band, and ~124px still holds a value.
   An existing breakpoint value; the set in DESIGN-OPEN-ITEMS.md is unchanged. */
@media (max-width: 22.49rem) {
  .toolbar .toolbar-select, .toolbar .toolbar-select.is-wide { flex-basis: 7rem; }
}
/* A status with nothing in it stays on the list — knowing a filter is empty is
   worth a line — and is muted where a browser will paint an <option>. It also
   says "(0)", because most of them will not. */
.toolbar .toolbar-select option.is-empty { color: var(--text-muted); }

/* ---- the multi-select pickers ---------------------------------------------
   Area and Status. One control each in the row, 44px like everything beside
   them, showing what is in force and opening onto a panel of checkboxes.

   THE PANEL IS A POPOVER, NOT A BLOCK IN FLOW. Eleven 44px rows is roughly
   500px tall; in flow that shoves the queue it filters off the bottom of the
   screen every time somebody opens it. Absolutely positioned, with its own
   scroll rather than the page's.

   AND IT MUST NOT PUSH THE PAGE WIDE. A popover that overflows the viewport
   gives the whole document a horizontal scrollbar, which is the one thing this
   bar was rebuilt to stop. Three rules keep it inside at every width: it is
   capped to the viewport less a gutter; the second picker in the row is
   anchored to its own right edge, because the first one's left edge is where
   the space runs out; and below 46rem each picker claims its own line so the
   panel starts at the card's left edge with the full width to grow into. */
.toolbar-picker { position: relative; flex: 1 1 8rem; min-width: 0; max-width: 12rem; }
.toolbar-picker.is-wide { flex: 2 1 11rem; max-width: 18rem; }
.toolbar-picker > summary {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: 44px; padding: 8px var(--space-3);
  /* The same 2px field edge as the search box beside it. At 1px it would be the
     only control in the bar drawn on a different weight of line. */
  border: 2px solid var(--border-control); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
  font-size: var(--step-small); cursor: pointer;
  list-style: none; white-space: nowrap; overflow: hidden;
}
.toolbar-picker > summary::-webkit-details-marker { display: none; }
/* The caret is the disclosure's own affordance and it must not be the thing
   that gets truncated, so it is drawn after the value and never shrinks. */
.toolbar-picker > summary::after {
  content: ""; flex: none; margin-left: auto;
  width: .5rem; height: .5rem; border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted); transform: translateY(-2px) rotate(45deg);
}
.toolbar-picker[open] > summary::after { transform: translateY(2px) rotate(225deg); }
.toolbar-picker > summary:hover { border-color: var(--accent-text); }
.toolbar-picker > summary:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }
/* What the control is filtered to, and nothing else. The group's name is on the
   summary's aria-label and on the legend inside the panel: "Area  All areas"
   is the same word twice, and repeating it is what made "All statuses"
   truncate at the width this bar has to stay one row at. */
.toolbar-picker-value {
  min-width: 0; overflow: hidden; text-overflow: ellipsis;
  color: var(--accent-text); font-weight: var(--fw-bold);
}
.toolbar-picker-body {
  position: absolute; z-index: 6; top: calc(100% + var(--space-1)); left: 0;
  width: max(100%, 16rem); max-width: min(22rem, calc(100vw - 2rem));
  max-height: min(60vh, 30rem); overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: var(--space-3); background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  box-shadow: var(--shadow-pop);
}
.toolbar-picker + .toolbar-picker .toolbar-picker-body { left: auto; right: 0; }
.toolbar-picker-set { min-width: 0; margin: 0; padding: 0; border: 0; }
.toolbar-picker-set > legend {
  margin-bottom: var(--space-2); padding: 0; font-size: var(--step-caption);
  font-weight: var(--fw-bold); color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .04em;
}
/* The reset and the presets: named selections rather than values, so they sit
   above the list and are drawn as the buttons they are. Both are real links, so
   the panel does something useful with JavaScript off. */
.toolbar-picker-actions {
  display: flex; flex-wrap: wrap; gap: var(--space-2);
  margin-bottom: var(--space-2); padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}
.toolbar-picker-reset, .toolbar-picker-preset {
  display: inline-flex; align-items: center; min-height: 44px;
  padding: 0 var(--space-3);
  border: 2px solid var(--border-control); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--accent-text);
  font-size: var(--step-small); font-weight: var(--fw-bold);
  text-decoration: none; white-space: nowrap; cursor: pointer;
}
.toolbar-picker-reset:hover, .toolbar-picker-preset:hover {
  border-color: var(--accent-text); background: var(--dark-red-10); text-decoration: none;
}
.toolbar-picker-reset.on, .toolbar-picker-preset.on {
  border-color: var(--accent); background: var(--dark-red-10);
}
.toolbar-picker-reset:focus-visible, .toolbar-picker-preset:focus-visible {
  outline: 3px solid var(--focus-ring); outline-offset: 2px;
}
.toolbar-picker-options { display: grid; gap: 2px; }
/* A full-width 44px row, not a 16px checkbox with a word next to it: the whole
   row is the target, at the same height as every other control in this bar. */
.toolbar-picker-option {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: 44px; padding: 0 var(--space-2); border-radius: var(--radius-sm);
  color: var(--text); font-size: var(--step-small); cursor: pointer;
}
.toolbar-picker-option:hover { background: var(--surface-muted); }
.toolbar-picker-option input {
  width: 18px; height: 18px; min-height: 0; flex: none; margin: 0;
  accent-color: var(--accent);
}
.toolbar-picker-option-label { min-width: 0; overflow-wrap: anywhere; }
/* `.on` is the server-rendered state, `:has()` the live one — both, so the
   panel is correct before any script runs and stays correct after a tick. */
.toolbar-picker-option.on, .toolbar-picker-option:has(input:checked) {
  color: var(--accent-text); font-weight: var(--fw-bold);
}
.toolbar-picker-option:focus-within { outline: 3px solid var(--focus-ring); outline-offset: -2px; }
/* The count is why this beats a bare menu: "Rejected 0" is a filter you now know
   not to press. Tabular so a column of them does not jitter. */
.toolbar-picker-count {
  margin-left: auto; padding-left: var(--space-2);
  color: var(--text-muted); font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}
.toolbar-picker-option.on .toolbar-picker-count,
.toolbar-picker-option:has(input:checked) .toolbar-picker-count { color: var(--accent-text); }
/* Below 46rem the bar is one column, so a picker takes the line and the panel
   takes the line's width — which is the only anchoring that cannot overflow a
   320px screen. An existing breakpoint value; the set in DESIGN-OPEN-ITEMS.md
   is unchanged. */
@media (max-width: 45.99rem) {
  .toolbar-picker, .toolbar-picker.is-wide { flex: 1 1 100%; max-width: none; }
  .toolbar-picker-body,
  .toolbar-picker + .toolbar-picker .toolbar-picker-body {
    left: 0; right: auto; width: 100%; max-width: 100%;
  }
}

/* ---- the status chip row --------------------------------------------------
   One row, always one row. It replaces a <details> holding 23 checkboxes in two
   parallel columns — 560px tall open on a desktop, 970px on a phone, which is
   more vertical space than the queue it filters.

   It scrolls sideways when it overflows rather than wrapping to four lines,
   because the chips are ordered by how often they are used and a wrapped row
   loses that ordering the moment the window changes width.

   THE EDGE FADES ARE NOT DECORATION AND THEY HIDE THEMSELVES. Four background
   layers: two opaque covers attached `local` (so they travel with the content)
   painted over two gradients attached `scroll` (so they stay pinned to the
   box). At rest each cover sits exactly on top of its gradient and the row
   looks plain; scroll, and the cover slides away to reveal the fade on the side
   there is more to see. A row with nothing to scroll to shows no fade at all,
   which is the entire point — a permanent gradient is a permanent lie about
   there being more. */
.toolbar-chips {
  display: flex; align-items: center; gap: var(--space-2);
  flex: 1 1 100%; min-width: 0;
  padding-block: 3px;              /* room for a focus ring, top and bottom */
  overflow-x: auto; overflow-y: hidden;
  -webkit-overflow-scrolling: touch;   /* momentum scroll on iOS */
  scrollbar-width: none;
  background:
    linear-gradient(var(--surface), var(--surface)) left center / 1.75rem 100% no-repeat local,
    linear-gradient(var(--surface), var(--surface)) right center / 1.75rem 100% no-repeat local,
    linear-gradient(to right, rgb(0 0 0 / .17), rgb(0 0 0 / 0)) left center / 1.5rem 100% no-repeat scroll,
    linear-gradient(to left,  rgb(0 0 0 / .17), rgb(0 0 0 / 0)) right center / 1.5rem 100% no-repeat scroll;
}
.toolbar-chips::-webkit-scrollbar { display: none; }

/* A chip and a toggle are the same object with different arity — one of many
   versus one of one — so they are drawn identically and differ only in what
   they say. Both are checkbox-backed, which is what makes them work with the
   keyboard, with a screen reader, and inside a GET form with no JavaScript at
   all. Same mechanic as `.areachip`, at filter radius instead of pill. */
.toolbar-chip, .toolbar-toggle {
  display: inline-flex; align-items: center; gap: var(--space-2); flex: none;
  min-height: 44px; padding: 0 var(--space-3);
  border: 2px solid var(--border-control); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
  font-family: inherit; font-size: var(--step-small); font-weight: var(--fw-medium);
  text-decoration: none; white-space: nowrap; cursor: pointer;
}
.toolbar-chip input, .toolbar-toggle input {
  width: 16px; height: 16px; min-height: 0; flex: none; margin: 0;
  accent-color: var(--accent);
}
.toolbar-chip:hover, .toolbar-toggle:hover { border-color: var(--accent-text); }
/* `.on` is the server-rendered state and `:has()` is the live one. Both, so a
   GET-form bar is correct before any script runs and a client-side bar stays
   correct after a chip is tapped. */
.toolbar-chip.on, .toolbar-chip:has(input:checked),
.toolbar-toggle.on, .toolbar-toggle:has(input:checked) {
  border-color: var(--accent); background: var(--dark-red-10);
  color: var(--accent-text); font-weight: var(--fw-bold);
}
.toolbar-chip:focus-within, .toolbar-toggle:focus-within {
  outline: 3px solid var(--focus-ring); outline-offset: 2px;
}
/* The count is why a chip beats a checkbox: "Rejected 0" is a filter you now
   know not to press. Tabular so a row of them does not jitter. */
.toolbar-chip-count {
  color: var(--text-muted); font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}
.toolbar-chip.on .toolbar-chip-count,
.toolbar-chip:has(input:checked) .toolbar-chip-count { color: var(--accent-text); }
/* "All" carries no checkbox because it is not a value — it is the absence of
   every value, and it is on exactly when nothing else is. NOT dashed: a dashed
   border reads as a dropzone, and this is a button. */
.toolbar-chip.is-reset { font-weight: var(--fw-bold); }

.toolbar-seg { display: inline-flex; align-items: center; gap: var(--space-2); flex: none; }
.toolbar-seg-label {
  color: var(--text-muted); font-size: var(--step-small);
  font-weight: var(--fw-bold); white-space: nowrap;
}
.toolbar-segset {
  display: inline-flex; align-items: stretch; min-height: 44px;
  border: 2px solid var(--border-control); border-radius: var(--radius-sm);
  background: var(--surface); overflow: hidden;
}
.toolbar-segitem {
  display: inline-flex; align-items: center; justify-content: center;
  /* 44px on the item itself, not just the set: the set's own 2px borders eat
     into a 44px set, leaving 40px anchors — the only sub-44 target on the bar. */
  min-height: 44px;
  min-width: 5rem; padding: 0 var(--space-3);
  border-left: 2px solid var(--border-control);
  color: var(--accent-text); text-decoration: none;
  font-size: var(--step-small); font-weight: var(--fw-bold); white-space: nowrap;
}
.toolbar-segitem:first-child { border-left: 0; }
.toolbar-segitem:hover { background: var(--dark-red-10); text-decoration: none; }
/* Navy at 16.7:1 rather than an accent fill: this is a statement of which
   ordering is in force, not a call to action, and the two must not look alike. */
.toolbar-segitem.on { background: var(--surface-inverse); color: var(--text-on-dark); }
.toolbar-segitem.on:hover { background: var(--surface-inverse); }
/* Inset, because the set clips its children and an outside ring would be cut. */
.toolbar-segitem:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: -3px; }

/* The queue's freshness line and manual-sync disclosure. The line keeps list
   rhythm; the summary is a real 44px target like every other control. */
.queue-fresh { margin: var(--space-2) 0 var(--space-3); }

/* THE STATUS ROW WRAPS, IT DOES NOT SCROLL. Same rule as the view tabs beside
   it: on a phone these become stacked lines, never a sideways scroller with the
   case count hidden off the right edge. The count is pushed to the end on a
   wide screen and simply follows in order once things wrap, because auto
   margins collapse when the row breaks. The control keeps its 44px target. */
.syncbar {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--space-2) var(--space-3);
}
/* Saved views and the ESO status share one card, divided by a rule rather than
   by a gap and a second shadow. The rule is the only thing that says they are
   two subjects; the card says they are one piece of furniture. */
.queuehead .savedviews { margin-bottom: var(--space-3); }
.queuehead-sync {
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
}
.syncbar-status {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--space-2) var(--space-3);
  flex: 1 1 22rem; min-width: 0;
}
.syncbar-title {
  margin: 0; font-size: var(--step-small); font-weight: var(--fw-bold);
}
.syncbar-status > [data-sync-phase] { align-self: center; }
.syncbar-msg {
  margin: 0; min-width: 0; flex: 1 1 16rem;
  font-size: var(--step-small); overflow-wrap: anywhere;
}
.syncbar-when, .syncbar-count {
  margin: 0; font-size: var(--step-small); color: var(--text-muted);
  min-width: 0; overflow-wrap: anywhere;
}
.syncbar-count { margin-left: auto; font-variant-numeric: tabular-nums; }
.syncbar .queue-sync { margin: 0; }
.syncbar .queue-sync > summary { min-height: 44px; }
.syncbar .filterform { margin-top: var(--space-2); }
.queue-sync > summary,
.closecase > summary {
  display: inline-flex; align-items: center; min-height: 44px;
  cursor: pointer; color: var(--accent-text); font-weight: var(--fw-bold);
  font-size: var(--step-small);
}

.toolbar .toolbar-submit {
  flex: none; min-height: 44px; padding: 9px var(--space-4);
  background: transparent; color: var(--accent-text);
  box-shadow: inset 0 0 0 2px var(--border-control);
  font-size: var(--step-small); font-weight: var(--fw-bold);
}
.toolbar .toolbar-submit:hover {
  background: var(--dark-red-10); color: var(--accent-text);
  box-shadow: inset 0 0 0 2px var(--accent-text); filter: none;
}

/* ---- nothing here --------------------------------------------------------
   Two different facts wore the same grey sentence everywhere: "there is nothing
   and that is fine", and "your filters excluded everything". Both were padded
   inline, per caller, at whatever value that caller picked. One shape, padded
   by a class, with room for the way out. */
.empty-state {
  display: grid; justify-items: center; gap: var(--space-2);
  padding: var(--space-6) var(--space-4);
  text-align: center; color: var(--text-muted);
}
.empty-state strong { color: var(--text); font-size: var(--step-body); }
.empty-state p { margin: 0; }
.empty-state-action { margin-top: var(--space-2); }

/* ══════════════════════════════════════════════════════════════════════════
   PRESENCE: THE CASE BAR AND THE AVATAR CLUSTER
   ══════════════════════════════════════════════════════════════════════════

   Presence is advisory — it never changes a case and never enters the audit
   history — and that is exactly why it cannot be a card headed "Live activity"
   below the fold. The one fact it carries is "somebody else is typing into this
   right now", and delivered after the collision it is not a fact, it is an
   explanation.

   So it moves into a bar that stays on screen: back-link, reference, status,
   and who else is in here. Sticky under the page header from 60rem, where the
   tool rail is a left column and there is nothing to collide with. Below that
   it scrolls with the page — a phone already gives 117px to the header and the
   rail strip, and a third pinned bar would leave the case file reading through
   a letterbox. */
.casebar {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2) var(--space-3);
  /* THE BAR IS AS TALL AS ITS TAP TARGET AND NO TALLER. The back link is a
     44px target by rule, and the bar added --space-2 above and below it on top
     of a 52px floor — so a row of one link and two chips stood 60px high with
     its content marooned in the middle. --space-1 either side of a 44px target
     is exactly the 52px floor, so the floor now describes the bar instead of
     fighting it, and the target is untouched.

     The margin below is --space-3 rather than --stack because what follows is
     the case's own action row: the bar, Set status and Close case are one
     header group, and a full stack between them read as two. */
  min-height: 52px; padding: var(--space-1) var(--space-4);
  margin-bottom: var(--space-3);
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius-md);
}
.casebar-back {
  flex: none; display: inline-flex; align-items: center; gap: var(--space-1);
  min-height: 44px; padding-inline: var(--space-1);
  color: var(--accent-text); text-decoration: none;
  font-size: var(--step-small); font-weight: var(--fw-bold);
}
.casebar-back:hover { text-decoration: underline; text-decoration-thickness: 2px; }
.casebar-ref {
  font-family: var(--font-mono); font-size: var(--step-small);
  color: var(--text-muted); white-space: nowrap;
}
/* The cluster holds up to four facts at once — the derived work state, a status
   somebody set by hand, a source resolution and the demo mark — and none of them
   can be dropped, because each is true and each changes what the others mean. On
   a 320px phone three of them already exceed the line, and an inline-flex row
   that cannot wrap takes the whole page sideways with it. So it wraps. */
.casebar-status {
  display: inline-flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-1); min-width: 0;
}
.casebar-end {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2) var(--space-3); margin-left: auto;
}
.casebar-actions { display: flex; align-items: center; gap: var(--space-2); }
/* The polling script's slot inside the bar; empty when the viewer is alone. */
.casebar-presence { display: inline-flex; align-items: center; min-height: 32px; }
@media (min-width: 60rem) {
  /* z-index 14: under the rail's 15 and the header's 20. It is the innermost
     of the three bars and must never paint over either. */
  .casebar {
    position: sticky; top: var(--topbar-h); z-index: 14;
    box-shadow: var(--shadow-card);
  }
}
/* Every sticky bar unpins together in phone landscape — see the topbar and rail
   rule further up. A casebar still offset by --topbar-h under a static header
   would hover in the middle of the page. */
@media (max-height: 30rem) and (orientation: landscape) {
  .casebar { position: static; }
}

/* One initial-avatar per person, overlapped so a crowd stays countable at a
   glance and costs the bar 22px per extra head instead of a whole pill. The
   cluster carries the accessible name — every avatar inside is aria-hidden —
   because "DW MK +2" read out one letter-pair at a time is noise. */
.presence-cluster { display: inline-flex; align-items: center; flex: none; padding-left: 8px; }
.presence-avatar {
  position: relative; display: inline-grid; place-items: center;
  width: 32px; height: 32px; flex: none; margin-left: -8px;
  border-radius: 50%;
  background: var(--dark-red-20); color: var(--accent-text);
  font-size: var(--step-caption); font-weight: var(--fw-bold); line-height: 1;
  /* A ring in the bar's own colour, so overlapping heads stay separable. */
  box-shadow: 0 0 0 2px var(--surface);
}
/* Amber and louder for editing, because "reading the same case" and "typing
   into the same case" are different problems and only one of them is yours to
   act on. Navy on the cream tint is 15:1; the amber is the RING, where it is a
   non-text boundary held to 3:1 rather than text held to 4.5. */
.presence-avatar.is-editing {
  background: var(--surface-attention); color: var(--status-attention-text);
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--status-warn);
}
.presence-avatar.is-more { background: var(--surface-muted); color: var(--text-muted); }
/* The ring. Present at all times as a static halo — that IS the reduced-motion
   state, not a downgrade of it — and animated only where motion is welcome. */
.presence-avatar::after {
  content: ''; position: absolute; inset: -3px;
  border: 2px solid currentColor; border-radius: 50%; opacity: .35;
}
.presence-avatar.is-editing::after { border-color: var(--status-warn); }
.presence-avatar.is-more::after { display: none; }
@media (prefers-reduced-motion: no-preference) {
  .presence-avatar.is-viewing::after { animation: presence-pulse 2.8s ease-out infinite; }
  .presence-avatar.is-editing::after { animation: presence-pulse 1.7s ease-out infinite; }
  .presence-pill.is-viewing .presence-dot::after,
  .presence-pill.is-editing .presence-dot::after { animation: presence-pulse 2.8s ease-out infinite; }
}
@keyframes presence-pulse {
  0%   { transform: scale(.85); opacity: .5; }
  70%  { transform: scale(1.4);  opacity: 0; }
  100% { transform: scale(1.4);  opacity: 0; }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE PUNCH GRID
   ══════════════════════════════════════════════════════════════════════════

   Four cells across, and the whole problem is that they are never the same
   cell: one is missing and asks to be filled, two came from the clock and are
   locked, one has a note under it. Left to size themselves, the label of the
   cell carrying "Needs your input" pushed its input 22px below its neighbours',
   so the row of four fields the employee has to read across was a staircase.

   Every cell therefore has an IDENTICAL vertical skeleton, in three rows with
   floors rather than content-sized heights:

       label   fixed floor — name, sub-label, and room for a state chip
       input   the control
       hint    fixed floor — present even when empty

   Reserving the chip's line is the point. Adding "Needs your input" to a cell
   must not move the input underneath it, or the form reflows while somebody is
   reading it.  */
/* THE CARD'S ONE HEADER LINE. Title, how old the ESO reading is, and the button
   that refreshes it — on one line, because they are one thought: "this is what
   we hold, from then, press here for newer". They were a heading, a paragraph
   three rows below it and a full-width button under that. The stamp sits with
   the title rather than near the button so it reads as a property of the card,
   and it is baseline-aligned to the heading it qualifies. Wraps to stacked
   lines on a phone; the button keeps its own tap target. */
/* THE COMPOSER'S ONE DECISION LINE. How it travels, and the button that puts
   the app's own wording in the box — side by side above the field, because both
   are choices made before typing rather than after. The selector is capped so
   it reads as a control and not as a full-width form field; Insert template
   sits to its right and drops under it on a phone. */
.convo-head {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
}
.convo-channel { margin: 0; flex: 1 1 auto; min-width: 0; }
/* The select and Insert template on one row, matched at the bottom edge so
   neither floats against the other's label. The select takes the slack and the
   button keeps its own width; under 30rem the button drops beneath it rather
   than squeezing the control it belongs to. */
.convo-channel-row {
  display: flex; flex-wrap: wrap; align-items: flex-end;
  gap: var(--space-2) var(--space-3);
}
.convo-channel-row select { flex: 1 1 12rem; min-width: 0; max-width: 18rem; }
.convo-insert { flex: none; min-height: 44px; }
/* One option is not a choice: it states the channel instead of offering it, and
   keeps the label above it so the line still reads the same way. */
.convo-channel-fixed {
  margin: 0; min-height: 44px; display: flex; align-items: center;
  font-size: var(--step-small); color: var(--text-muted);
}
/* Said once, under the box it qualifies, and only for the channel it is true
   of — the script hides it for a note or an email, which get no wrapper. */
.convo-wrap { margin: var(--space-1) 0 0; }
.convo-insert { margin: var(--space-1) 0 0; }
.convo-hold { margin: 0 0 var(--space-3); }

.punchcard-head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--space-2) var(--space-3);
  margin-bottom: var(--stack);
}
.punchcard-head > h2 { margin: 0; }
.punchcard-source {
  margin: 0; flex: 1 1 auto; min-width: 0;
  font-size: var(--step-small); color: var(--text-muted);
}
.punchcard-head > form { margin: 0; flex: none; }
/* The instruction, tightened against the grid it introduces: it is a caption
   for the fields, not a paragraph in its own right. */
.punchcard-lead { margin: 0 0 var(--space-3); max-width: var(--measure); }

.punchgrid { --punch-label-h: 3.75rem; --punch-hint-h: 2.25rem; }
.punchgrid .punchcell {
  position: relative; display: grid; min-width: 0; margin-bottom: 0;
  grid-template-rows: minmax(var(--punch-label-h), auto) auto minmax(var(--punch-hint-h), auto);
  gap: var(--space-1);
  /* `align-content: start`, AND WITHOUT IT THE FLOORS DO NOTHING.
     The cells are grid items in a row, so they all stretch to the tallest of
     them; a stretched grid container hands its spare height to whichever of its
     rows can absorb it, and `minmax(…, auto)` can. So the cell with the tallest
     label got its spare height in one place and its neighbours got theirs in
     another, and the four inputs sat at three different heights — measured 12px
     apart. Packed to the start, every cell spends its rows identically and the
     leftover collects at the bottom where nothing is aligned to it. */
  align-content: start;
}
.punchcell-label {
  display: flex; flex-wrap: wrap; align-content: flex-start;
  gap: 2px var(--space-2);
  padding-right: 2.5rem;      /* the flag button's corner, kept clear of text */
}
.punchcell-name {
  flex: 1 1 100%; font-weight: var(--fw-bold); font-size: var(--step-small);
}
/* A cell carries a sub-label OR a state chip on its second line, never both.
   Two lines of secondary text under a two-word name is a cell that is mostly
   caption, and the floor above reserves exactly one of them — which is what
   keeps the four inputs on one line when one cell gains a chip. */
.punchcell-sub { font-size: var(--step-caption); color: var(--text-muted); }
.punchcell-state { display: inline-flex; align-items: center; }
.punchcell-hint { margin: 0; font-size: var(--step-caption); color: var(--text-muted); }
/* min-width: 0 on the input, not a wider track: a time input's UA min-content
   (~10rem) is a few px wider than the 2-and-4-up tracks at narrow widths, and
   a track floor wide enough to hold it would break the 4-up under 44rem. The
   input shrinks instead; its value never needs the spare px. */
.punchgrid .punchcell > input { margin: 0; min-width: 0; width: 100%; }
.punchcell.is-needed > input { border-color: var(--accent); background: var(--surface); }

/* A locked cell is FILLED, not fenced.
 *
 * It was drawn with a dashed border, and a dashed border is the web's universal
 * "drop something here" — the exact opposite of "this value came from the clock
 * and you cannot change it". A solid field on the muted surface says read-only
 * the way every read-only field in every application says it, and the lock mark
 * beside the label says which kind of read-only. --border-control holds 3.22:1
 * against the muted tint, so the field is still a visible control. */
.punchcell.is-locked > input {
  background: var(--surface-muted); color: var(--text);
  border-color: var(--border-control); border-style: solid;
}
.punchcell-lock {
  display: inline-block; width: 13px; height: 13px; flex: none;
  align-self: center; color: var(--text-muted); background: currentColor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.5 7V4.75a3.5 3.5 0 0 1 7 0V7H13v8H3V7h1.5zm2 0h3V4.75a1.5 1.5 0 0 0-3 0V7z'/%3E%3C/svg%3E") center / contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.5 7V4.75a3.5 3.5 0 0 1 7 0V7H13v8H3V7h1.5zm2 0h3V4.75a1.5 1.5 0 0 0-3 0V7z'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* "A recorded ESO value appears incorrect" used to be one checkbox at the
 * bottom of the form, four fields away from any of the four values it might be
 * about — so the timekeeper ticked it and then had to say which one in prose.
 * The claim now lives ON the value it refers to: one small ghost button in the
 * cell's corner, checkbox-backed so it submits with the form and needs no
 * script, with the field it flags tinting to match. */
.punchcell-flag {
  position: absolute; top: 0; right: 0; z-index: 1;
  display: grid; place-items: center; width: 44px; height: 44px;
  padding: 0; border: 0; border-radius: var(--radius-sm);
  background: transparent; color: var(--text-muted); cursor: pointer;
}
.punchcell-flag input {
  position: absolute; width: 1px; height: 1px; min-height: 0; margin: 0; opacity: 0;
}
.punchcell-flag::before {
  content: ''; width: 17px; height: 17px; background: currentColor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 1h1.6v14H3V1zm2.6 1h8.2l-2 3 2 3H5.6V2z'/%3E%3C/svg%3E") center / contain no-repeat;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 1h1.6v14H3V1zm2.6 1h8.2l-2 3 2 3H5.6V2z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.punchcell-flag:hover { color: var(--accent-text); background: var(--dark-red-10); }
.punchcell-flag:focus-within { outline: 3px solid var(--focus-ring); outline-offset: -2px; }
/* `.is-flagged` is the server-rendered state, `:has()` the live one — both, so
   the cell is right on the way back from a failed submit as well as on the tap. */
.punchcell.is-flagged .punchcell-flag,
.punchcell:has(.punchcell-flag input:checked) .punchcell-flag {
  color: var(--status-warn); background: var(--surface-attention);
}
.punchcell.is-flagged > input,
.punchcell:has(.punchcell-flag input:checked) > input {
  border-color: var(--status-warn); background: var(--surface-attention);
}
.punchcell.is-flagged .punchcell-hint,
.punchcell:has(.punchcell-flag input:checked) .punchcell-hint { color: var(--status-warn); }

/* ---- Your team (modules/timekeeping/team-views.js) ----
   Charts drawn as elements, sized by custom properties set on each mark
   (--h, --x, --from, --to), so they lay out at the card's own width with no
   fitting script. Office and remote are identity, so they take two of the
   validated series hues (blue and purple: CVD ΔE 19.1, normal 23.2, both
   ≥3:1 on white). Weekend is not an identity worth a hue; it is muted ink.
   Every mark sits inside a hit area larger than itself, and the tooltip is
   the mark's own data-tip, anchored away from the edge it is nearest to so
   it can never push the page sideways. */
.desk-card, .desk-legend { --desk-office: var(--chart-series-2); --desk-remote: var(--chart-series-3);
  --desk-weekend: var(--text-muted); }
.desk-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
  gap: var(--space-2) var(--space-4); }
.desk-head h2 { margin: 0; }
.desk-head + p { margin: var(--space-2) 0 0; }
.desk-legend { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-4); list-style: none;
  margin: 0; padding: 0; font-size: var(--step-caption); color: var(--text-muted); }
.desk-legend li { display: inline-flex; align-items: center; gap: var(--space-2); }
.desk-key { width: 10px; height: 10px; border-radius: 2px; background: var(--desk-office); }
.desk-key.is-remote { background: var(--desk-remote); }
.desk-key.is-weekend { background: var(--desk-weekend); opacity: .5; }
.desk-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.desk-who strong { overflow-wrap: anywhere; }
.desk-section { margin: var(--space-6) 0 var(--space-3); }
.desk-footnote { margin-top: var(--space-4); }

/* The shared tooltip. */
/* Every tipped mark is already positioned (the columns relative, the dots and
   ranges absolute), which is all ::after needs. A blanket `position: relative`
   here out-ranked `.desk-dot` and flattened every dot and range bar. */
.desk-card [data-tip]:hover::after {
  content: attr(data-tip); position: absolute; bottom: calc(100% + 6px); left: 50%;
  transform: translateX(-50%); width: max-content; max-width: 15rem; z-index: 5;
  padding: var(--space-1) var(--space-2); border-radius: var(--radius-sm);
  background: var(--surface-inverse); color: var(--text-on-dark);
  font-size: var(--step-caption); line-height: var(--lh-tight, 1.3); font-weight: var(--fw-medium);
  pointer-events: none; text-align: left; }
.desk-card [data-tip].tip-start:hover::after { left: 0; transform: none; }
.desk-card [data-tip].tip-end:hover::after { left: auto; right: 0; transform: none; }

/* Every day: one strip per person. */
.desk-strips { display: grid; gap: var(--space-4); margin-top: var(--space-4); }
.desk-strip { display: grid; grid-template-columns: minmax(7rem, 10rem) minmax(0, 1fr) 4.5rem;
  gap: var(--space-3); align-items: end; }
.desk-plot { position: relative; height: 64px; display: grid;
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); column-gap: 2px;
  border-bottom: 1px solid var(--border); }
.desk-grid { position: absolute; left: 0; right: 0; top: 0; border-top: 1px dashed var(--border);
  pointer-events: none; }
.desk-col { position: relative; }
.desk-col::before { content: ''; position: absolute; left: 0; right: 0; bottom: 0;
  height: var(--h, 2px); border-radius: 2px 2px 0 0; background: var(--desk-office); }
.desk-col.is-remote::before { background: var(--desk-remote); }
.desk-col.is-weekend::before { background: var(--desk-weekend); opacity: .5; }
.desk-col.is-idle::before { height: 2px; border-radius: 0; background: var(--border-strong); }
.desk-col.is-before::before { height: 2px; border-radius: 0;
  background: repeating-linear-gradient(90deg, var(--border) 0 2px, transparent 2px 4px); }
.desk-col.is-today::before { opacity: .55; }
.desk-col:hover::before { filter: brightness(.85); }
.desk-strip-total { display: flex; flex-direction: column; align-items: flex-end; line-height: 1.2;
  font-variant-numeric: tabular-nums; }
.desk-strip-axis { align-items: start; margin-top: calc(-1 * var(--space-3)); }
.desk-axis { display: flex; justify-content: space-between; font-size: var(--step-caption);
  color: var(--text-muted); }

/* Office and remote: a dumbbell per person per measure. */
.desk-panels { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--space-4) var(--space-6); margin-top: var(--space-4); }
.desk-panel h3 { font-size: var(--step-body); margin: 0 0 var(--space-2); }
.desk-dumbbell { display: grid; grid-template-columns: 4.25rem minmax(0, 1fr) auto; align-items: center;
  gap: var(--space-2); min-height: 32px; }
.desk-dumbbell-name { font-size: var(--step-small); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.desk-track { position: relative; height: 24px; }
.desk-track::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; border-top: 1px solid var(--border); }
.desk-link { position: absolute; top: calc(50% - 1px); height: 2px; left: var(--from);
  width: calc(var(--to) - var(--from)); background: var(--text-muted); }
.desk-dot { position: absolute; top: 50%; left: var(--x); width: 12px; height: 12px; margin: -6px 0 0 -6px;
  border-radius: 50%; background: var(--desk-office); box-shadow: 0 0 0 2px var(--surface); }
.desk-dot.is-remote { background: var(--desk-remote); }
.desk-dot::before { content: ''; position: absolute; inset: -8px; }
.desk-dumbbell-values { display: grid; grid-template-columns: 3.4rem 3.4rem; gap: var(--space-1);
  justify-items: end; font-variant-numeric: tabular-nums; }
.desk-v { display: inline-flex; align-items: center; gap: 4px; }
.desk-v::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--desk-office); }
.desk-v.is-remote::before { background: var(--desk-remote); }
.desk-coverage { list-style: none; padding: 0; margin: var(--space-4) 0 0; display: flex; flex-wrap: wrap;
  gap: var(--space-1) var(--space-6); }

/* The working day: first to last action, office and remote. */
.desk-ranges { display: grid; gap: var(--space-4); margin-top: var(--space-4); }
.desk-range-person { display: grid; grid-template-columns: minmax(7rem, 10rem) minmax(0, 1fr);
  gap: var(--space-3); align-items: center; }
.desk-range-row { display: grid; grid-template-columns: 3.5rem minmax(0, 1fr) 8rem; gap: var(--space-2);
  align-items: center; min-height: 26px; }
.desk-range-values { font-variant-numeric: tabular-nums; text-align: right; }
.desk-range-track { position: relative; height: 10px; border-radius: 5px; background: var(--surface-muted); }
.desk-range { position: absolute; top: 0; bottom: 0; left: var(--from); width: calc(var(--to) - var(--from));
  border-radius: 5px; background: var(--desk-office); }
.desk-range.is-remote { background: var(--desk-remote); }
.desk-range-axis { margin-top: calc(-1 * var(--space-2)); }
.desk-range-axis .desk-range-track { background: none; height: 1.4em; }
.desk-tick { position: absolute; top: 0; left: var(--x); transform: translateX(-50%);
  font-size: var(--step-caption); color: var(--text-muted); white-space: nowrap; }
.desk-tick:first-child { transform: none; }
.desk-tick:last-child { transform: translateX(-100%); }

@media (max-width: 40rem) {
  .desk-strip { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: 'who total' 'plot plot';
    row-gap: var(--space-1); }
  .desk-strip > .desk-who { grid-area: who; }
  .desk-strip > .desk-plot { grid-area: plot; }
  .desk-strip > .desk-strip-total { grid-area: total; }
  .desk-strip-axis { grid-template-areas: 'plot plot'; margin-top: calc(-1 * var(--space-2)); }
  .desk-strip-axis > span { display: none; }
  .desk-strip-axis > .desk-axis { grid-area: plot; }
  .desk-range-person { grid-template-columns: minmax(0, 1fr); }
  /* Label and times on one line, the bar under them, so a time can never be
     read as belonging to the bar below it. */
  .desk-range-row { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: 'label values' 'track track';
    row-gap: 2px; }
  .desk-range-label { grid-area: label; }
  .desk-range-values { grid-area: values; }
  .desk-range-track { grid-area: track; }
  .desk-range-axis > span, .desk-range-axis .desk-range-row > span:not(.desk-range-track) { display: none; }
  .desk-range-axis .desk-range-row { grid-template-areas: 'track track'; }
  .desk-tick:nth-child(even) { display: none; }
}

/* ==========================================================================
   The dashboard
   ==========================================================================
   Everything here is scoped under `.dash`. Not for tidiness: `.hint` already
   exists in this stylesheet at --step-caption, which is ~12px, and this page
   deliberately bottoms out at --step-body. Two rules with one name and different
   intentions is how a page ends up depending on source order.

   THE COLOUR DECISION. --text is Falck dark red, so left alone every word AND
   every number on this page is the same maroon and nothing can be more important
   than anything else. Figures take the brand navy — cooler, darker, reads as
   data rather than prose. Maroon stays for words. Red is spent only on what
   wants acting on. Three jobs, three colours, and see core/dashboard-views.js
   for the rule that decides which rows get the third.

   TYPE. --step-small and --step-caption clamp at 13px and 12px. That is right
   for a caption on a page somebody opens twice a year and wrong for the one they
   open every morning, so nothing here starts below --step-body. The figure gets
   its own fluid clamp so it grows with the viewport instead of snapping between
   two sizes.

   NOTE ON SPACING TOKENS: there is no --space-5 and no --space-7. An undefined
   custom property in a shorthand does not fall back, it drops the declaration —
   so a gap written with one silently collapses to nothing. The ramp is
   1, 2, 3, 4, 6, 8, 12. */
.dash {
  /* Layout below is sized by the page's own content width, not the window: the
     side rail takes 15rem on a laptop, and a rule keyed to the window counted
     that room twice. */
  container: dash / inline-size;
  --fig: var(--falck-dark-blue);
  --fig-quiet: var(--falck-dark-grey);
  --dash-gap: clamp(0.75rem, 0.4rem + 0.8vw, 1.25rem);
}

/* ---------- page head ---------- */
.dash .dash-head { padding: clamp(1.25rem, 2.5vw, 2.5rem) 0 var(--space-6); }
.dash .dash-head h1 { font-size: clamp(1.875rem, 1.3rem + 2.2vw, 3rem); margin: 0 0 8px;
  line-height: .95; letter-spacing: -0.03em; }
.dash .dash-head .scope { display: block; font-size: var(--step-body);
  color: var(--text-muted); line-height: 1.4; }

/* ---------- figures ----------
   Mobile first: two across from the narrowest phone up, so four figures are a
   two-by-two block in a hand and on a laptop. Wider, the column count follows how many figures
   there are (data-count, set by the view) so the last row is never a lone
   tile: four or eight go four across, three, six or nine go three across.
   Explicit counts and NOT auto-fit: auto-fit packs in as many as will
   technically fit, which on a 1400px page is seven slivers nobody can read. */
.dash .figs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--dash-gap); margin: 0 0 clamp(1.5rem, 3vw, 2.5rem); }
.dash .figs[data-count="1"] { grid-template-columns: minmax(0, 1fr); }
@container dash (min-width: 44rem) {
  .dash .figs[data-count="3"], .dash .figs[data-count="6"], .dash .figs[data-count="9"] {
    grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
/* Four across only when each tile keeps about 15rem: narrower than that, the
   sentence under the number turns a tile into a column of words. */
@container dash (min-width: 64rem) {
  .dash .figs { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .dash .figs[data-count="1"] { grid-template-columns: minmax(0, 1fr); }
  .dash .figs[data-count="2"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash .figs[data-count="3"], .dash .figs[data-count="6"], .dash .figs[data-count="9"],
  .dash .figs[data-count="5"] { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.dash .figure { display: flex; flex-direction: column; text-decoration: none; color: inherit;
  background: var(--surface); border: 1px solid var(--border);
  border-top: 4px solid var(--dark-red-30);
  padding: clamp(0.875rem, 1.6vw, 1.5rem);
  transition: border-color .12s ease; min-width: 0;
  /* The number sizes to its own tile, not the window: a two-up tile on a phone
     and a four-up tile on a laptop are about the same width. */
  container-type: inline-size; }
.dash a.figure:hover { border-color: var(--fig); border-top-color: var(--fig); }
.dash .figure.attn { border-top-color: var(--falck-red); }
.dash a.figure.attn:hover { border-color: var(--falck-red); border-top-color: var(--falck-red); }
.dash a.figure:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
/* A tile whose feed did not answer. Grey rather than red: nothing here is
   urgent, because nothing here is known. */
.dash .figure.out { border-top-color: var(--border); }
.dash .figure .n { font-size: clamp(1.75rem, 19cqi, 3.5rem); font-weight: var(--fw-heavy); line-height: .9;
  font-variant-numeric: tabular-nums; letter-spacing: -0.035em; color: var(--fig); }
.dash .figure.attn .n { color: var(--falck-dark-red); }
.dash .figure .n.zero { color: var(--fig-quiet); font-weight: var(--fw-medium);
  letter-spacing: -0.01em; }
/* Words in the number's place ("Reading…", "Unavailable", "0 of 514") are
   longer than a figure, so they take a smaller size rather than overflowing a
   two-up tile. The view adds .word to anything that is not a plain number. */
.dash .figure .n.word { font-size: clamp(1.25rem, 12cqi, 2.25rem); }
.dash .figure .k { font-size: var(--step-body); line-height: 1.3; margin-top: 10px;
  font-weight: 600; color: var(--text); overflow-wrap: break-word; }
.dash .figure .d { font-size: var(--step-small); color: var(--text-muted); margin-top: auto;
  padding-top: 10px; line-height: 1.5; }

/* A shape, not a second number. Aria-hidden, because the sentence beneath
   already gives the direction in words and a screen reader does not want a path
   element read to it. Rising takes the red — see the view for why that is the
   only direction that earns a colour. */
.dash .spark { margin-top: 14px; }
.dash .spark svg { display: block; width: 100%; height: 44px; }
.dash .spark path { stroke: var(--fig); }
.dash .spark circle { fill: var(--fig); }
.dash .spark.rising path { stroke: var(--falck-red); }
.dash .spark.rising circle { fill: var(--falck-red); }

/* ---------- section rhythm ---------- */
.dash .sec { margin-top: clamp(1.75rem, 3.5vw, 3rem); border-top: 1px solid var(--border);
  padding-top: var(--space-6); }
.dash .sec h2 { font-size: var(--step-h2); margin: 0 0 8px; letter-spacing: -0.02em; }
.dash .sec .lead { font-size: var(--step-body); color: var(--text-muted);
  margin: 0 0 var(--space-6); line-height: 1.6; }
.dash .sec .hint { margin-top: var(--space-4); line-height: 1.6;
  color: var(--text-muted); font-size: var(--step-small); }

/* ---------- the attention list ----------
   Mobile first, and sized by the box it sits in rather than by the window. On a
   wide screen the sections sit two side by side, so a list can be phone-narrow
   on a 1400px page; sizing by the window is what squeezed "Whose move" to one
   letter per line. So every list starts as a stack of rows, and becomes a table
   only once its own section is wide enough to give each column real room.

   In the stack each row is the count on the left (with its column name under
   it), the name, then the two detail cells, each led by its column name from
   data-label, because without the header row "74 hours" does not say it is
   double-time. */
.dash .sec { container-type: inline-size; }
.dash .todo, .dash .todo tbody { display: block; width: 100%; }
.dash .todo colgroup, .dash .todo thead { display: none; }
.dash .todo tr { display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas: "cnt what go" "cnt det go" "cnt wait go";
  column-gap: var(--space-4); row-gap: 2px; align-items: baseline;
  padding: 14px 0; border-bottom: 1px solid var(--border); }
.dash .todo td { display: block; padding: 0; border: 0; white-space: normal;
  overflow-wrap: break-word; min-width: 0; }
.dash .todo .cnt { grid-area: cnt; align-self: start; text-align: center; min-width: 3.25rem;
  font-variant-numeric: tabular-nums; font-weight: var(--fw-heavy);
  font-size: var(--step-h2); line-height: 1; letter-spacing: -0.035em; color: var(--fig); }
.dash .todo .cnt::after { content: attr(data-label); display: block; margin-top: 4px;
  font-size: var(--step-small); font-weight: var(--fw-bold); letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-muted); }
.dash .todo .what { grid-area: what; font-weight: 600; color: var(--text);
  font-size: var(--step-h3); line-height: 1.3; letter-spacing: -0.01em; }
.dash .todo .who { color: var(--text-muted); font-size: var(--step-body); line-height: 1.45; }
.dash .todo .det { grid-area: det; }
.dash .todo .wait { grid-area: wait; }
.dash .todo .who:empty { display: none; }
.dash .todo .who[data-label]:not([data-label=""])::before { content: attr(data-label) ": ";
  color: var(--text-muted); font-weight: var(--fw-bold); }
.dash .todo .go { grid-area: go; align-self: center; color: var(--dark-red-40);
  font-size: var(--step-h3); }
/* Colour carries URGENCY, never size. Red means act this week; amber is anything
   with more road left. The number already carries how big. */
.dash .todo tr.urgent .cnt { color: var(--falck-red); }
.dash .todo tr.soon .cnt { color: var(--status-warn); }
.dash .todo tbody tr.rowlink:hover { background: var(--surface-muted); }
.dash .todo a { text-decoration: none; color: inherit; }
.dash .todo tbody tr.rowlink:hover .what { text-decoration: underline; }
.dash .todo tbody tr.rowlink:hover .go { color: var(--falck-red); }
.dash .todo a:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* Wide enough for a real table. Fixed layout with declared widths, because
   auto layout sizes a column to its longest unbroken content and app.css sets
   white-space: nowrap on table cells; together those once pushed this table
   past the viewport. The name column takes what is left, so it is never the
   one that gets squeezed. */
@container (min-width: 40rem) {
  .dash .todo { display: table; border-collapse: collapse; table-layout: fixed; }
  .dash .todo colgroup { display: table-column-group; }
  .dash .todo thead { display: table-header-group; }
  .dash .todo tbody { display: table-row-group; }
  .dash .todo tr { display: table-row; padding: 0; }
  .dash .todo td { display: table-cell; }
  .dash .todo .who:empty { display: table-cell; }
  .dash .todo col.c-cnt  { width: 5.5rem; }
  .dash .todo col.c-what { width: auto; }
  .dash .todo col.c-det  { width: 27%; }
  .dash .todo col.c-wait { width: 22%; }
  .dash .todo col.c-go   { width: 2.5rem; }
  .dash .todo th { text-align: left; font-size: var(--step-small); letter-spacing: .06em;
    text-transform: uppercase; color: var(--text-muted); font-weight: var(--fw-bold);
    padding: 0 var(--space-4) 12px 0; border-bottom: 2px solid var(--border-strong);
    white-space: normal; overflow-wrap: normal; vertical-align: bottom; }
  .dash .todo th:first-child { text-align: right; }
  .dash .todo td { padding: 18px var(--space-4) 18px 0; border-bottom: 1px solid var(--border);
    vertical-align: baseline; }
  .dash .todo .cnt { text-align: right; min-width: 0; font-size: var(--step-h1); }
  .dash .todo .cnt::after { content: none; }
  .dash .todo .who[data-label]:not([data-label=""])::before { content: none; }
  .dash .todo .go { text-align: right; }
  .dash .todo tbody tr.rowlink:hover { background: none; }
  .dash .todo tbody tr.rowlink:hover td { background: var(--surface-muted); }
}

/* ---------- a stated absence ----------
   "Nothing lapsing this week" and "the certificate feed could not be read" are
   opposite facts and must not share a look. The first keeps the navy rule; the
   second is grey, because a colour that means "attention" on an unread feed is
   attention paid to nothing. */
.dash .nothing { padding: var(--space-6); background: var(--surface);
  border: 1px solid var(--border); border-left: 4px solid var(--fig);
  font-size: var(--step-body); line-height: 1.65; }
.dash .nothing strong { display: block; margin-bottom: 8px; font-size: var(--step-h3);
  letter-spacing: -0.01em; }
.dash .nothing.out { border-left-color: var(--fig-quiet); }

/* ---------- while the warehouse answers ----------
   The placeholder is the finished tile's shape, so nothing on the page moves
   when the real one lands. Two sentences live in the markup and CSS picks one:
   that is what lets the script contain no interpolation at all. */
.dash .live-fail { display: none; }
.dash .is-failed .live-wait { display: none; }
.dash .is-failed .live-fail { display: inline; }
.dash .is-loading .n { color: var(--fig-quiet); }
.dash .is-failed .nothing, .dash .is-failed.figure { border-left-color: var(--fig-quiet);
  border-top-color: var(--border); }

/* Preserve row-major reading order while using the wide dashboard as two
   useful columns, and only once each column is wide enough for its list to be a
   real table (40rem, see the attention list). Narrower, one column. */
@container dash (min-width: 84rem) {
  .dash .dash-sections { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(1.5rem, 2.5vw, 3rem); align-items: start; }
  .dash .dash-section { min-width: 0; }
  .dash .dash-section .sec { margin-top: 0; }
}

/* ---------- phone ----------
   Two-up tiles drop their sentence on a phone; the number and its label are the
   glance, and the tile opens the page that explains it. The lists need nothing
   here: they stack by their own width (see the attention list above). */
@media (max-width: 40rem) {
  .contact-line { grid-template-columns: 1fr; gap: 0; }
  .dash .figure .d { display: none; }
}

/* ============ ESO handoff workbench ============ */
.handoff .eyebrow, .parallel-run .eyebrow { margin: 0 0 4px; color: var(--accent-text); font-size: var(--step-small);
  font-weight: var(--fw-heavy); letter-spacing: .07em; text-transform: uppercase; }
.handoff-head { margin-bottom: var(--space-4); }
.handoff-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2); min-width: min(100%, 30rem); }
.handoff-progress { display: grid; min-width: 10rem; padding: var(--space-3) var(--space-4);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); }
.handoff-progress strong { color: var(--falck-dark-red); font-size: var(--step-h2);
  font-variant-numeric: tabular-nums; line-height: 1.1; }
.handoff-tabs { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-3); }
.handoff-tools { display: flex; gap: var(--space-2); padding: var(--space-3); margin-bottom: var(--space-2); }
.handoff-tools input { flex: 1 1 14rem; min-width: 0; }
.handoff-tools select { flex: 0 1 12rem; }
.handoff-tools input, .handoff-tools select { min-height: 44px; padding: 9px var(--space-3); }
.handoff-result { margin: var(--space-3) 0; }
.handoff-grid { display: grid; gap: var(--space-4); }
.handoff-card { overflow: hidden; background: var(--surface); border: 1px solid var(--border);
  /* --radius-lg is not a token; the ramp is sm / md / pill. This is a card and
     wants the card radius. */
  border-left: 5px solid var(--accent); border-radius: var(--radius-md);
  box-shadow: var(--shadow-card); transition: opacity .16s, transform .16s; }
.handoff-card[data-state='entered'] { border-left-color: var(--status-ok); }
.handoff-card-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-3); padding: var(--space-4) var(--space-6); }
.handoff-card-head h2 { margin: 0 0 3px; }
.handoff-card-head p { margin-bottom: 0; }
.handoff-state { flex: none; }
.handoff-values { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  border-block: 1px solid var(--border); background: var(--surface-muted); }
.handoff-value { position: relative; display: grid; align-content: start; min-width: 0;
  gap: 5px; padding: var(--space-3) var(--space-4); border-right: 1px solid var(--border); }
.handoff-value:last-child { border-right: 0; }
.handoff-value .lbl { color: var(--text-muted); font-size: var(--step-small);
  font-weight: var(--fw-bold); text-transform: uppercase; letter-spacing: .035em; }
.handoff-value code { color: var(--text); font-family: var(--font-mono);
  font-size: var(--step-small); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
/* One ESO field per part: a date and a time, each copied on its own. The button
   sits in the flow rather than pinned to the cell corner, because there are now
   two of them per cell and one absolute position cannot hold both. */
.handoff-part { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.handoff-part > code { flex: 1 1 auto; min-width: 0; }
.handoff-part .lbl.part { flex: none; min-width: 2.4rem; text-transform: none;
  letter-spacing: 0; font-weight: var(--fw-normal); }
.handoff-part .copy-mini { position: static; flex: none; }
.copy-mini { position: absolute; right: 8px; bottom: 8px; min-height: 44px; border: 0;
  border-radius: var(--radius-pill); padding: 5px 9px; background: var(--surface);
  color: var(--accent-text); font: inherit; font-size: var(--step-small); font-weight: var(--fw-bold);
  box-shadow: inset 0 0 0 1px var(--border); cursor: pointer; }
.copy-mini:hover { box-shadow: inset 0 0 0 2px var(--accent); }
.handoff-card-foot { display: flex; align-items: center; justify-content: space-between;
  gap: var(--space-3); padding: var(--space-3) var(--space-6); }
.handoff-card-foot > p { margin: 0; }
.handoff-verification { border-bottom: 1px solid var(--border); background: var(--surface); }
.handoff-verification-view { padding: var(--space-3) var(--space-6); }
.handoff-verification-head { display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: var(--space-2); }
.handoff-verification-head .lbl { display: block; color: var(--text-muted);
  font-size: var(--step-small); font-weight: var(--fw-bold); text-transform: uppercase;
  letter-spacing: .035em; }
.handoff-verification-head strong { display: block; font-size: var(--step-body); }
.handoff-verification-view > p { margin: var(--space-2) 0 0; }
.handoff-verification-actions { display: flex; flex-wrap: wrap; align-items: flex-start;
  gap: var(--space-2); margin-top: var(--space-3); }
.handoff-verification-actions form { margin: 0; }
.handoff-verification-details { margin-top: var(--space-3); }
.handoff-verification-details > summary { display: inline-flex; align-items: center;
  min-height: 44px; padding: 9px 18px; border-radius: var(--radius-pill);
  color: var(--accent-text); font-size: var(--step-small); font-weight: var(--fw-bold);
  box-shadow: inset 0 0 0 2px currentColor; cursor: pointer; list-style: none; }
.handoff-verification-details > summary::-webkit-details-marker { display: none; }
.handoff-verification-details form { display: grid; gap: var(--space-2); margin-top: var(--space-2);
  max-width: 34rem; padding: var(--space-3); background: var(--surface-muted);
  border: 1px solid var(--border); border-radius: var(--radius-md); }
.handoff-verification-details .field { margin-bottom: 0; }
.handoff-verification-details textarea { width: 100%; min-height: 5rem; }
.handoff-verification-narrative { margin-top: var(--space-3); padding: var(--space-3);
  background: var(--status-attention-bg); border-left: 4px solid var(--status-attention-text);
  border-radius: var(--radius-sm); }
.handoff-verification-narrative strong { display: block; }
.handoff-verification-narrative p { margin: var(--space-1) 0 0; white-space: pre-wrap; }
.handoff-actions { display: flex; flex-wrap: wrap; justify-content: flex-end;
  align-items: center; gap: var(--space-2); }
.handoff-actions form { margin: 0; }
.handoff-reopen { position: relative; }
.handoff-reopen > summary { min-height: 44px; padding: 11px 18px; border-radius: var(--radius-pill);
  color: var(--accent-text); font-size: var(--step-small); font-weight: var(--fw-bold);
  box-shadow: inset 0 0 0 2px currentColor; cursor: pointer; list-style: none; }
.handoff-reopen > summary::-webkit-details-marker { display: none; }
.handoff-reopen form { position: absolute; z-index: 3; right: 0; top: calc(100% + 8px);
  display: grid; gap: var(--space-2); width: min(22rem, 80vw); padding: var(--space-3);
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md);
  box-shadow: var(--shadow-pop); }
.handoff-reopen textarea { width: 100%; min-height: 5rem; }
.handoff-empty { text-align: center; }
.handoff-toast { position: fixed; z-index: 20; right: var(--space-4); bottom: var(--space-4);
  max-width: min(26rem, calc(100vw - 2rem)); padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md); background: var(--surface-inverse); color: var(--text-on-dark);
  box-shadow: var(--shadow-pop); opacity: 0; transform: translateY(12px); pointer-events: none;
  transition: opacity .16s, transform .16s; }
.handoff-toast.show { opacity: 1; transform: none; }
.handoff-toast.bad { background: var(--status-error); }
.handoff-guide-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-3); margin-bottom: var(--space-4); }
.handoff-guide-grid .card, .handoff-guide-body .card { padding: var(--space-4); }
.handoff-guide-grid h2 { margin-top: 0; }
.handoff-guide-grid p:last-child, .handoff-guide-body p:last-child { margin-bottom: 0; }
.handoff-guide-body { display: grid; gap: var(--space-4); }
.handoff-guide-body li + li { margin-top: var(--space-2); }
.parallel-run-head { margin-bottom: var(--space-4); }
.parallel-run-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.parallel-readiness { display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-4); margin-bottom: var(--space-4); padding: var(--space-4);
  border: 1px solid var(--border); border-left: 5px solid var(--accent);
  border-radius: var(--radius-md); background: var(--surface); box-shadow: var(--shadow-card); }
.parallel-readiness.blocked { border-left-color: var(--status-attention-text); }
.parallel-readiness.ready { border-left-color: var(--status-ok); }
.parallel-readiness h2 { margin: 0 0 var(--space-1); }
.parallel-readiness p { margin-bottom: 0; }
.parallel-section-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-3); margin-bottom: var(--space-3); }
.parallel-section-head h2 { margin: 0; }
.parallel-percent { color: var(--accent-text); font-size: var(--step-h2);
  font-variant-numeric: tabular-nums; line-height: 1; }
.parallel-progress { height: 10px; overflow: hidden; border-radius: var(--radius-pill);
  background: var(--dark-red-20); }
.parallel-progress span { display: block; height: 100%; border-radius: inherit;
  background: var(--accent); }
.parallel-metrics { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2); margin-top: var(--space-4); }
.parallel-metric { display: grid; gap: 3px; min-width: 0; }
.parallel-metric strong { font-size: var(--step-h3); font-variant-numeric: tabular-nums; }
.parallel-criteria { display: grid; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
.parallel-criterion { display: flex; align-items: center; gap: var(--space-2);
  min-width: 0; padding: var(--space-3); border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--surface-muted); }
.parallel-criterion > div { display: grid; gap: 2px; min-width: 0; flex: 1; }
.parallel-criterion-mark { display: grid; place-items: center; width: 28px; height: 28px;
  flex: none; border-radius: 50%; background: var(--dark-red-20); color: var(--accent-text);
  font-weight: var(--fw-heavy); }
.parallel-criterion.is-pass .parallel-criterion-mark { background: var(--dark-red-20);
  color: var(--status-ok); }
.parallel-columns { display: grid; grid-template-columns: 1fr;
  gap: var(--space-4); margin-bottom: var(--space-4); }
.parallel-table-wrap { width: 100%; }
.parallel-table { table-layout: fixed; width: 100%; font-size: var(--step-small); }
.parallel-table th, .parallel-table td { min-width: 0; overflow-wrap: anywhere; }
.parallel-table th { vertical-align: bottom; }
.parallel-table td { vertical-align: middle; }
.parallel-table td .parallel-progress { min-width: 5rem; margin-bottom: 3px; }
.parallel-mismatches { display: grid; gap: var(--space-3); }
.parallel-mismatch { padding: var(--space-3); border: 1px solid var(--border);
  border-left: 4px solid var(--status-attention-text); border-radius: var(--radius-sm);
  background: var(--surface-muted); }
.parallel-mismatch-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--space-3); }
.parallel-mismatch h3 { margin: 0; font-size: var(--step-body); }
.parallel-mismatch .eyebrow { margin-bottom: 2px; }
.parallel-mismatch-note { margin: var(--space-2) 0; white-space: pre-wrap; }
.parallel-history { margin: 0; }
.parallel-history h2 { margin-top: 0; }
.parallel-history-metrics { display: flex; flex-wrap: wrap; gap: var(--space-6); }

@media (max-width: 60rem) {
  .handoff-guide-grid { grid-template-columns: 1fr; }
  .parallel-columns { grid-template-columns: 1fr; }
  .handoff-values { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .handoff-value { border-bottom: 1px solid var(--border); }
  .handoff-value:nth-child(even) { border-right: 0; }
  .handoff-value:last-child { grid-column: 1 / -1; border-bottom: 0; }
}
@media (max-width: 40rem) {
  .parallel-run-actions { flex-direction: column; align-items: stretch; }
  .parallel-run-actions .btn { width: 100%; }
  .parallel-readiness { align-items: stretch; flex-direction: column; }
  .parallel-metrics { grid-template-columns: 1fr; }
  .parallel-section-head { flex-direction: column; }
  .parallel-criterion { align-items: flex-start; }
  .parallel-criterion .tag { margin-left: auto; }
  .parallel-table, .parallel-table tbody, .parallel-table tr, .parallel-table td { display: block; width: 100%; }
  .parallel-table thead { display: none; }
  .parallel-table tr { padding: var(--space-2) 0; border-bottom: 1px solid var(--border); }
  .parallel-table tr:last-child { border-bottom: 0; }
  .parallel-table td { display: grid; grid-template-columns: minmax(7rem, 36%) minmax(0, 1fr);
    gap: var(--space-2); padding: var(--space-2) 0; border: 0; }
  .parallel-table td::before { content: attr(data-label); color: var(--text-muted);
    font-size: var(--step-small); font-weight: var(--fw-bold); }
  .parallel-table td .parallel-progress { min-width: 0; }
  .parallel-history-metrics { gap: var(--space-4); }
  .handoff-summary { grid-template-columns: 1fr; min-width: 0; width: 100%; }
  .handoff-summary .handoff-progress { min-width: 0; }
  .handoff-tools { flex-direction: column; }
  .handoff-tools input { flex-basis: auto; width: 100%; }
  .handoff-tools select { flex-basis: auto; width: 100%; }
  .handoff-values { grid-template-columns: 1fr; }
  .handoff-value, .handoff-value:nth-child(even) { border-right: 0; }
  .handoff-value:last-child { grid-column: auto; }
  .handoff-card-head, .handoff-card-foot { align-items: stretch; flex-direction: column; padding-inline: var(--space-4); }
  .handoff-card-head .handoff-state { align-self: flex-start; }
  .handoff-verification-view { padding-inline: var(--space-4); }
  .handoff-verification-actions { flex-direction: column; align-items: stretch; }
  .handoff-verification-actions > form, .handoff-verification-actions > form .btn,
  .handoff-verification-actions > details { width: 100%; }
  .handoff-verification-actions > form .btn { width: 100%; }
  .handoff-verification-details > summary { justify-content: center; width: 100%; }
  .handoff-actions { justify-content: flex-start; }
  .handoff-actions > .btn, .handoff-actions > form, .handoff-actions > form .btn { width: 100%; }
  .handoff-reopen { width: 100%; }
  .handoff-reopen > summary { text-align: center; }
  .handoff-reopen form { position: static; width: 100%; margin-top: var(--space-2); box-shadow: none; }
}

/* ---- the case page's conversation (2026-09-16) ------------------------ */
/* One thread, and a badge that says which door each line came through.
 *
 * WHY THE INTERNAL ROW LOOKS DIFFERENT AND NOT MERELY LABELLED. The one mistake
 * this card must make impossible is somebody reading a note as something the
 * employee has seen. A badge alone is a word among words; a tinted block with a
 * rule down its left edge is a different KIND of thing at a glance, before
 * anything is read. The badge and the sentence under it say the same thing in
 * words for anybody the colour never reaches. */
.convo { list-style: none; margin: 0 0 var(--stack); padding: 0;
  display: grid; gap: var(--space-3); }
.convo-item {
  padding: var(--space-3); border-radius: var(--radius-md);
  background: var(--surface); border: 1px solid var(--border);
  min-width: 0;
}
.convo-item.is-internal {
  background: var(--surface-muted); border-color: transparent;
  border-left: 4px solid var(--border-strong, var(--border));
}
/* A step that happened, rather than a thing somebody said. One line, no box:
   it is punctuation in the conversation and must not read as a message. */
.convo-item.is-system {
  padding: var(--space-2) var(--space-3); background: transparent;
  border: 0; border-left: 2px solid var(--border);
  border-radius: 0; color: var(--text-muted); font-size: var(--step-small);
}
.convo-head {
  display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-2);
  align-items: baseline; margin: 0 0 var(--space-1);
}
.convo-who { min-width: 0; overflow-wrap: anywhere; }
.convo-lead { margin: 0 0 var(--space-1); }
/* An employee's reply arrives however they typed it, line breaks and all, and
   a text message is the one thing on this page whose shape is part of what was
   said. `anywhere` because a run number or a long word must wrap rather than
   push the card wider than the phone. */
.convo-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
/* No size of its own. The sentence carries `.small`, which is the smallest step
   this page is allowed to use: the caption step bottoms out at 12px, and this
   is the line that says whether the employee has seen what is above it. */
.convo-note {
  margin: var(--space-2) 0 0; color: var(--text-muted);
}
.convo-line { display: flex; flex-wrap: wrap; gap: var(--space-1) var(--space-2); margin: 0; }
.convo-line .convo-when { font-variant-numeric: tabular-nums; }

/* The box. Full width on every screen, because what goes in it is a sentence
   somebody is composing and a narrow field invites a shorter one than they
   meant to write. */
.convo-compose { margin-top: var(--stack); }
.convo-compose textarea { width: 100%; min-height: 5.5rem; resize: vertical; }
/* The reason the tickbox is held on, beside the tickbox rather than above the
   box: it is an explanation of that control and of nothing else. */
.convo-forced {
  margin: 0 0 var(--space-3); padding: var(--space-2) var(--space-3);
  background: var(--surface-muted); border-radius: var(--radius-sm);
  color: var(--text-muted);
}
/* 44px of target on the tickbox and its label, which is the whole row: a
   checkbox drawn at its intrinsic 13px is the smallest thing on this page and
   it is the control that decides whether a message reaches a handset. */
.convo-compose .check {
  display: flex; align-items: center; gap: var(--space-2); min-height: 44px;
}
.convo-compose .check input[type="checkbox"] { width: 22px; height: 22px; flex: none; }
.convo-compose .check label { display: flex; align-items: center; min-height: 44px; }
.convo-compose .btn.convo-send { min-height: 50px; }
@media (max-width: 34rem) {
  .convo-compose .btn.convo-send { display: block; width: 100%; }
}

/* Two quiet disclosures on this page: the logging form and the audit trail.
   `.toolbar-picker` supplies the 44px summary and the caret, and its body is a
   popover — right in a filter bar, wrong here, where the content belongs in the
   flow and can be several screens long. Same summary, static body. */
.convo-log, .case-history { margin-top: var(--stack); }
.convo-log > summary, .case-history > summary {
  display: flex; align-items: center; gap: var(--space-2);
  min-height: 44px; padding: 8px var(--space-3);
  border: 2px solid var(--border-control); border-radius: var(--radius-sm);
  background: var(--surface); color: var(--text);
  font-size: var(--step-small); cursor: pointer; list-style: none;
}
.convo-log > summary::-webkit-details-marker,
.case-history > summary::-webkit-details-marker { display: none; }
.convo-log > summary::after, .case-history > summary::after {
  content: ""; flex: none; margin-left: auto;
  width: .5rem; height: .5rem; border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted); transform: translateY(-2px) rotate(45deg);
}
.convo-log[open] > summary::after,
.case-history[open] > summary::after { transform: translateY(2px) rotate(225deg); }
.convo-log > summary:focus-visible, .case-history > summary:focus-visible {
  outline: 3px solid var(--focus-ring); outline-offset: 2px;
}
.convo-log > .toolbar-picker-body, .case-history > .toolbar-picker-body {
  position: static; width: auto; max-width: none; max-height: none;
  margin-top: var(--space-3); box-shadow: none; overflow: visible;
}
.case-history .timeline { max-width: 100%; }

/* The first ask, once it is a card of its own. Its block was written to sit
   inside another card and carried no top margin of its own. */
.card.firstask .mptext-block { margin-top: 0; }

.demo-session-banner{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;padding:.75rem 1rem;background:var(--response-yellow,#ffdb58);color:var(--dark-blue,#161b43)}
.demo-session-banner form{margin:0}

/* ---------- crew on the unit, and editing an approved correction ----------
   The crew card is reference beside the punch card: names on the left, what
   they punched on the right, wrapping rather than scrolling at 320px. The
   comparison line is the one sentence the card exists to say, so it sits above
   the rows; its warning tone is the text colour and the chip, never a fill. */
/* One partner per row: who (name, role) on the left, what they punched on
   the right, each punch span on its own line with its ESO link at the end.
   Wraps to stacked lines on a phone rather than scrolling. */
.crew-list { list-style: none; margin: 0 0 var(--space-3); padding: 0;
  border-top: 1px solid var(--border); }
.crew-member { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
  gap: var(--space-2) var(--space-4); padding: var(--space-3) 0; border-bottom: 1px solid var(--border); }
.crew-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; overflow-wrap: anywhere; }
.crew-role { color: var(--text-muted); font-size: var(--step-small); }
.crew-punch { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end;
  gap: var(--space-1) var(--space-3); font-variant-numeric: tabular-nums; }
.crew-member > .crew-punch + .crew-punch { flex-basis: 100%; }
.crew-time { white-space: nowrap; }
.crew-lbl, .crew-date { color: var(--text-muted); font-size: var(--step-small); }
.crew-note { margin: 0; color: var(--text-muted); font-size: var(--step-small); }
.crew-eso { display: inline-flex; align-items: center; min-height: 44px; padding: 0 var(--space-1);
  color: var(--accent-text); font-size: var(--step-small); font-weight: var(--fw-bold);
  white-space: nowrap; }
/* Inside a sentence (the comparison line, the note under a typed time) the
   link is part of the text; the 44px target is for the row links above. */
.crew-cmp .crew-eso, .crew-hint .crew-eso { display: inline; min-height: 0; padding: 0; }
.crew-booked { margin: 0 0 var(--space-3); }
.crew-cmp { margin: 0 0 var(--space-3); }
.crew-cmp.is-warn { color: var(--status-warn); }
.crew-cmp .tag { margin-right: var(--space-2); }
.punchcell .crew-hint.is-warn { color: var(--status-warn); font-weight: var(--fw-medium); }
.edit-correction { margin-top: var(--space-4); border-top: 1px solid var(--border);
  padding-top: var(--space-3); }
.edit-correction > summary { display: inline-flex; align-items: center; min-height: 44px;
  padding: 9px 18px; border-radius: var(--radius-pill); color: var(--accent-text);
  font-size: var(--step-small); font-weight: var(--fw-bold);
  box-shadow: inset 0 0 0 2px currentColor; cursor: pointer; list-style: none; }
.edit-correction > summary::-webkit-details-marker { display: none; }
.edit-correction form { margin-top: var(--space-3); }
.edit-correction textarea { width: 100%; min-height: 5rem; }
.punchcell.is-editing > input { background: var(--surface); }

/* Governance documents (/operator/docs/requirements, /operator/docs/srs): prose
   tables whose cells are sentences, so they wrap; a diagram's source scrolls in
   its own box rather than widening the page. */
.govdoc { overflow-wrap: break-word; }
.govdoc table { margin: var(--space-3) 0 var(--space-6); }
.govdoc th, .govdoc td { white-space: normal; vertical-align: top; }
.govdoc td.id { white-space: nowrap; }
.govdoc code { white-space: normal; overflow-wrap: anywhere; }
.govdoc .tablewrap { overflow-x: auto; }
.govdoc th { position: static; }
.govdoc pre { overflow-x: auto; padding: var(--space-3); background: var(--surface-muted);
              border: 1px solid var(--border); border-radius: 6px; font-size: var(--step-small); }
.govdoc h2 { margin-top: var(--space-6); }

/* ---------- Payroll audit (modules/payroll) ---------- */
.audit-areas { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2); margin-bottom: var(--space-4); }
@media (min-width: 48rem) { .audit-areas { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (min-width: 80rem) { .audit-areas { grid-template-columns: repeat(7, minmax(0, 1fr)); } }
.audit-area-card { display: flex; flex-direction: column; gap: 2px; padding: var(--space-3);
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  color: inherit; text-decoration: none; min-height: 44px; }
.audit-area-card:hover { border-color: var(--border-strong); }
.audit-area-card.on { box-shadow: inset 0 0 0 2px var(--falck-dark-blue); border-color: var(--falck-dark-blue); }
.audit-area-name { font-weight: 700; }
.audit-area-n { font-size: 1.6rem; font-weight: 800; line-height: 1.1; font-variant-numeric: tabular-nums; }
.audit-bar { display: block; height: 4px; background: var(--dark-red-20); border-radius: 2px; overflow: hidden; margin: 4px 0 2px; }
.audit-bar i { display: block; height: 100%; background: var(--status-ok); }
.audit-list { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
.audit-head, .audit-row > summary { display: grid; gap: var(--space-3); align-items: center;
  grid-template-columns: minmax(11rem, 1.4fr) 7rem 9.5rem 6rem minmax(13rem, 2.2fr) 8rem;
  padding: var(--space-3) var(--space-4); }
.audit-head { background: var(--surface-muted); font-size: var(--step-small); font-weight: 700;
  color: var(--text-muted); text-transform: uppercase; letter-spacing: .03em; }
.audit-row { border-top: 1px solid var(--border); }
.audit-row > summary { cursor: pointer; list-style: none; min-height: 44px; }
.audit-row > summary::-webkit-details-marker { display: none; }
.audit-row[open] > summary, .audit-row > summary:hover { background: #FCFAFB; }
.audit-row small, .audit-days small { display: block; color: var(--text-muted); font-size: var(--step-small); }
.audit-name b { display: block; }
.audit-hours { font-variant-numeric: tabular-nums; }
.audit-diff { font-weight: 800; font-variant-numeric: tabular-nums; }
.audit-diff.more { color: var(--status-warn); }
.audit-diff.less { color: var(--falck-vibrant-blue); }
.audit-diff.zero { color: var(--text-muted); font-weight: 400; }
.audit-why q { display: block; color: var(--text-muted); font-size: var(--step-small);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.audit-status { font-size: var(--step-small); }
.audit-ok { color: var(--status-ok); }
.audit-detail { padding: var(--space-2) var(--space-4) var(--space-4); background: #FCFAFB;
  border-top: 1px dashed var(--border); }
.audit-days { width: 100%; border-collapse: collapse; font-size: var(--step-small); }
.audit-days th { text-align: left; color: var(--text-muted); padding: var(--space-2); border-bottom: 1px solid var(--border); }
.audit-days td { padding: var(--space-2); border-bottom: 1px solid var(--dark-red-20); vertical-align: top;
  font-variant-numeric: tabular-nums; }
.audit-days tr.flag td:first-child { box-shadow: inset 3px 0 0 var(--status-warn); }
.audit-miss { color: var(--accent); font-weight: 700; }
.audit-note { display: block; }
.audit-note b { font-size: 11px; text-transform: uppercase; letter-spacing: .03em; color: var(--text-muted); }
.audit-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; margin-top: var(--space-3); }
.audit-actions .muted { margin-left: auto; }
.audit-mark { display: inline; margin: 0; }
.audit-rules { margin-top: var(--space-3); }
@media (max-width: 900px) {
  .audit-head { display: none; }
  .audit-row > summary { grid-template-columns: 1fr auto; }
  .audit-row > summary > .audit-area { display: none; }
  .audit-why, .audit-hours { grid-column: 1 / -1; }
  .audit-detail { padding: var(--space-2) var(--space-3) var(--space-3); }
  .audit-days thead { display: none; }
  .audit-days tr { display: block; border-bottom: 1px solid var(--dark-red-20); padding: var(--space-2) 0; }
  .audit-days td { display: block; border: 0; padding: 2px var(--space-1); }
  .audit-actions .muted { margin-left: 0; }
}
.audit-flash { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2); }
.audit-help { margin: calc(-1 * var(--space-2)) 0 var(--space-3); }

/* The links under a headline card: a row of buttons that wraps on a phone. */
.clockin-links { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-3) 0 0; }
