/* ==========================================================================
   Timesheet — one stylesheet, no build step, no CDN.

   Colour is declared once as custom properties and never again. That is what
   makes the dark theme below a twenty-line override rather than a second
   stylesheet: every rule in this file reads a variable, so nothing has to be
   found and edited twice. A literal colour anywhere outside the two :root
   blocks is a bug — it will look wrong in one theme or the other.
   ========================================================================== */

:root {
  color-scheme: light;

  --bg: #f4f6f9;
  --surface: #ffffff;
  --surface-2: #f4f6f9;      /* insets: inline forms, folded-open rows */
  --field: #ffffff;          /* what you can type into */
  --ink: #16202b;
  --ink-soft: #5b6b7c;
  --line: #dde4ec;
  --line-strong: #c3cede;
  --brand: #2f6fed;
  --brand-dark: #2458c4;
  --brand-ink: #ffffff;      /* text on a brand-filled button */

  --ok: #17794a;
  --ok-bg: #e4f5ec;
  --ok-line: #b9e2cc;
  --warn: #8a5a00;
  --warn-bg: #fdf1dc;
  --warn-line: #f0d7a8;
  --bad: #b3261e;
  --bad-bg: #fbe6e5;
  --bad-line: #f2c4c1;
  --info: #2458c4;
  --info-bg: #e8f0fe;
  --info-line: #c3d6fb;
  --note: #5334a8;           /* the fifth status colour — completed */
  --note-bg: #ece7f8;

  --shadow: 0 1px 2px rgba(16, 32, 48, .06);
  --shadow-lift: 0 4px 16px rgba(16, 32, 48, .10);
  --radius: 10px;
}

/* The dark theme. Set by the toggle in the top bar, remembered in
   localStorage, and applied in <head> before first paint so the page never
   flashes white on its way to being dark. */
[data-theme="dark"] {
  color-scheme: dark;

  --bg: #10151c;
  --surface: #171f2a;
  --surface-2: #1e2836;
  --field: #111922;
  --ink: #e6edf5;
  --ink-soft: #93a4b8;
  --line: #26313f;
  --line-strong: #37465a;
  --brand: #5b92ff;
  --brand-dark: #82adff;
  --brand-ink: #0b1119;

  --ok: #56d394;
  --ok-bg: #11301f;
  --ok-line: #1d5437;
  --warn: #f0b429;
  --warn-bg: #33270a;
  --warn-line: #5a4512;
  --bad: #ff8b83;
  --bad-bg: #38181a;
  --bad-line: #5e2a2c;
  --info: #8ab4ff;
  --info-bg: #14233d;
  --info-line: #24406e;
  --note: #c3aef5;
  --note-bg: #241d3a;

  --shadow: 0 1px 2px rgba(0, 0, 0, .35);
  --shadow-lift: 0 4px 18px rgba(0, 0, 0, .45);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  /* The theme switch should not strobe. */
  transition: background-color .15s ease, color .15s ease;
}

h1 { font-size: 1.6rem; margin: 0 0 .25rem; }
h2 { font-size: 1.05rem; margin: 0 0 .75rem; }
h3 { font-size: .95rem; margin: 1.25rem 0 .5rem; color: var(--ink-soft); }
a { color: var(--brand); }

/* Every figure in this app is money, hours or a count, and all three are
   read down a column. Lining figures stop the columns from jittering. */
td, .stat-value, .money, .num, .day-total, .chip b { font-variant-numeric: tabular-nums; }

/* ---------------- layout ---------------- */

.wrap { max-width: 1180px; margin: 0 auto; padding: 1.5rem 1.25rem 3rem; }
.wrap.narrow { max-width: 460px; padding-top: 4rem; }

.grid { display: grid; gap: 1rem; }
.grid-2 { grid-template-columns: 1fr 1fr; align-items: start; }
@media (max-width: 860px) { .grid-2 { grid-template-columns: 1fr; } }

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1.1rem 1.25rem;
  margin-bottom: 1rem;
  box-shadow: var(--shadow);
}
.card.center { text-align: center; }

/* A card that is saying something rather than holding something: one state of
   one record needs a person to act. The accent is a border rather than a fill
   so the text underneath keeps the page's normal contrast, and the last
   paragraph loses its margin so the box does not sit with a gap at the
   bottom. */
.callout { border-left: 4px solid var(--line); }
.callout > :last-child { margin-bottom: 0; }
.callout-bad  { border-left-color: var(--bad);  background: var(--bad-bg); }
.callout-warn { border-left-color: var(--warn); background: var(--warn-bg); }
.callout-info { border-left-color: var(--info); background: var(--info-bg); }

/* A wide table inside a card must not push the page sideways. Scrolling the
   table rather than the document is the fallback for every table; the ones
   marked .stack below do something better. */
@media (max-width: 760px) {
  .card { overflow-x: auto; }
}

/* ---------------- top bar ---------------- */

.topbar {
  display: flex; align-items: center; gap: 1.25rem;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  padding: .65rem 1.25rem;
  flex-wrap: wrap;
  position: sticky; top: 0; z-index: 20;
  box-shadow: var(--shadow);
}
.brand {
  font-weight: 700; text-decoration: none; color: var(--ink);
  display: inline-flex; align-items: center; gap: .45rem;
}
.topbar nav { display: flex; gap: .25rem; flex: 1; flex-wrap: wrap; }
.topbar nav a {
  text-decoration: none; color: var(--ink-soft); font-weight: 500;
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .3rem .55rem; border-radius: 7px;
}
.topbar nav a:hover { color: var(--brand); background: var(--surface-2); }
/* Where you are now. Set by the active_nav block in base.html. */
.topbar nav a.on { color: var(--brand); background: var(--info-bg); font-weight: 700; }
/* A page that lives under the one before it. Tucked against its parent and
   set smaller rather than given an equal slot, because a flat row of equals
   is exactly what it is not. Kept for the developer and client bars; the
   admin bar groups instead. */
.topbar nav a.nav-sub {
  margin-left: -.2rem; font-size: .84rem; font-weight: 500;
}
.topbar nav a.nav-sub::before { content: "\21b3"; opacity: .5; }
.topbar .sep { width: 1px; background: var(--line); }

/* ---- grouped menus ----------------------------------------------------
   A category heading in the top bar that opens a short list. <details>, so
   it works with no JavaScript at all; see the note in base.html.

   The heading is styled to match a plain nav link exactly, because half of
   them still are plain links and a row where some items look clickable and
   others look like headings would read as two different things. */
.nav-group { position: relative; }
.nav-group > summary {
  list-style: none; cursor: pointer;
  color: var(--ink-soft); font-weight: 500;
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .3rem .55rem; border-radius: 7px;
  white-space: nowrap;
}
.nav-group > summary::-webkit-details-marker { display: none; }
.nav-group > summary:hover { color: var(--brand); background: var(--surface-2); }
/* You are on a page inside this group. Same treatment as a current plain
   link, so "where am I" reads the same way across the whole bar. */
.nav-group > summary.on {
  color: var(--brand); background: var(--info-bg); font-weight: 700;
}
/* The caret turns to point at the open list rather than at the heading. */
.nav-caret { width: .85em; height: .85em; opacity: .6; }
.nav-group[open] > summary .nav-caret { transform: rotate(180deg); }

.nav-menu {
  position: absolute; left: 0; top: calc(100% + .4rem); z-index: 40;
  min-width: 14rem; width: max-content;
  max-width: min(20rem, calc(100vw - 2rem));
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: 10px; box-shadow: 0 10px 28px rgba(0, 0, 0, .18);
  padding: .3rem;
  display: flex; flex-direction: column; gap: .1rem;
}
/* Rows, not a row. Inside an open menu there is no horizontal space to
   compete for, so each item gets a full line and its own icon. */
.topbar nav .nav-menu a {
  display: flex; align-items: center; gap: .5rem;
  padding: .42rem .6rem; border-radius: 7px;
}
/* The last group would otherwise push its panel off the right edge on a
   narrow window. Anchored to its own right instead. */
.nav-group:last-child .nav-menu { left: auto; right: 0; }
.user-block { display: flex; align-items: center; gap: .75rem; }
.whoami { font-weight: 600; }
.whoami small {
  display: block; font-weight: 400; color: var(--ink-soft);
  text-transform: capitalize;
}
.quiet { color: var(--ink-soft); text-decoration: none; font-size: .9rem; }

/* The light/dark switch. Icon only — it sits beside the sign-out button and
   a word there would compete with it. */
.theme-toggle {
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink-soft); border-radius: 7px; cursor: pointer;
  padding: .35rem .5rem; line-height: 1; display: inline-flex;
}
.theme-toggle:hover { color: var(--brand); border-color: var(--brand); }
/* One button, two icons: CSS picks which is showing, so there is no flash of
   the wrong one and no JavaScript needed to swap it. */
.theme-toggle .i-moon { display: none; }
[data-theme="dark"] .theme-toggle .i-sun { display: none; }
[data-theme="dark"] .theme-toggle .i-moon { display: inline; }

/* ---------------- notices ----------------
   The bell in the top bar and the panel on a home page draw the same list, so
   the rules that colour a row are shared and only the frame differs. */
.bell { position: relative; }
.bell > summary {
  list-style: none; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink-soft); border-radius: 7px;
  padding: .35rem .5rem; line-height: 1;
  display: inline-flex; align-items: center; gap: .3rem;
}
.bell > summary::-webkit-details-marker { display: none; }
.bell > summary:hover { color: var(--brand); border-color: var(--brand); }
/* Nothing waiting: still there, still openable, just not asking to be. */
.bell-quiet > summary { opacity: .55; }
.bell-count {
  min-width: 1.15rem; padding: 0 .3rem; border-radius: 999px;
  font-size: .72rem; font-weight: 700; text-align: center; color: #fff;
}
.bell-count.lvl-bad { background: var(--bad); }
.bell-count.lvl-warn { background: var(--warn); }
.bell-count.lvl-info { background: var(--info); }
.bell-count.lvl-ok { background: var(--ok); }
.bell-drop {
  position: absolute; right: 0; top: calc(100% + .4rem); z-index: 40;
  width: min(23rem, calc(100vw - 2rem));
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: 10px; box-shadow: 0 10px 28px rgba(0, 0, 0, .18);
  padding: .35rem;
}
.notice-none { margin: .5rem .6rem; }

.notice-panel h2 { margin-top: 0; }
.notice-list { list-style: none; margin: 0; padding: 0; }
.notice + .notice { margin-top: .25rem; }
.notice > a {
  display: grid; grid-template-columns: auto 1fr; gap: .15rem .55rem;
  align-items: start; text-decoration: none; color: inherit;
  padding: .5rem .6rem; border-radius: 8px;
  border-left: 3px solid var(--line-strong);
}
.notice > a:hover { background: var(--surface-2); }
.notice > a svg { grid-row: span 2; margin-top: .15rem; }
.notice strong { font-weight: 600; }
/* The level is the left edge and the icon, never the text: a notice read out
   loud, or printed, has to say the same thing it says in colour. */
.notice.lvl-bad > a { border-left-color: var(--bad); }
.notice.lvl-bad svg { color: var(--bad); }
.notice.lvl-warn > a { border-left-color: var(--warn); }
.notice.lvl-warn svg { color: var(--warn); }
.notice.lvl-info > a { border-left-color: var(--info); }
.notice.lvl-info svg { color: var(--info); }
.notice.lvl-ok > a { border-left-color: var(--ok); }
.notice.lvl-ok svg { color: var(--ok); }

/* ---------------- profile completeness ----------------
   One bar, two pages. The colour bands match the notice levels above on
   purpose: "red means somebody has to do something" should mean the same
   thing everywhere in the application. */
.meter-card h2 { margin-top: 0; }
.meter {
  height: .55rem; border-radius: 999px; overflow: hidden;
  background: var(--surface-2); border: 1px solid var(--line);
}
.meter-fill { height: 100%; border-radius: 999px; }
.meter-fill.lvl-ok { background: var(--ok); }
.meter-fill.lvl-warn { background: var(--warn); }
.meter-fill.lvl-bad { background: var(--bad); }
.meter-figure {
  display: flex; align-items: baseline; gap: .5rem;
  margin: .5rem 0 .75rem;
}
.meter-figure .stat-value { font-size: 1.3rem; }
.meter-parts {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .3rem .9rem;
}
.meter-parts li {
  display: inline-flex; align-items: center; gap: .3rem;
  font-size: .88rem;
}
/* Done is stated, not merely un-marked: a tick beside a label reads the same
   in grey as it does in green, and printing loses the colour entirely. */
.meter-parts li.done { color: var(--ink-soft); }
.meter-parts li.done svg { color: var(--ok); }
.meter-parts li.todo { font-weight: 600; }
.meter-parts li.todo svg { color: var(--warn); }
.meter-extras { margin: .75rem 0 0; }
.meter-extras .todo { color: var(--ink-soft); }

/* ---------------- utilisation ----------------
   Reuses .meter above rather than growing a second bar: two progress bars
   that look different on two pages would read as two different kinds of
   measurement, and they are the same kind. Only the things utilisation has
   and the profile meter does not are here. */
.util-card h2 { margin-top: 0; }
.util-figure { font-weight: 700; }
.util-figure.lvl-ok { color: var(--ok); }
.util-figure.lvl-warn { color: var(--warn); }
.util-figure.lvl-bad { color: var(--bad); }
/* The dash. Deliberately not red and deliberately not green: "nothing was
   expected" is not a score, and colouring it as one is the exact mistake
   this report is built to avoid. The dotted underline says there is a
   reason, which the title attribute carries. */
.util-none {
  color: var(--ink-soft); font-weight: 700;
  border-bottom: 1px dotted var(--line-strong); cursor: help;
}
.util-working {
  list-style: none; margin: .75rem 0 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .3rem 1.4rem;
  font-size: .88rem; color: var(--ink-soft);
}
.util-working li { display: inline-flex; gap: .4rem; }
.util-working strong { color: var(--ink); }
/* A third line under a statistic, for the qualifier that stops the number
   being read as the whole answer — "not in the figure", "with pending". */
.stat-note { display: block; color: var(--ink-soft); font-size: .78rem; }

/* ---------------- search ----------------
   The box in the top bar, and the results. A result is a link with a badge
   beside it and, when the match was somewhere other than the title, a line
   underneath saying where — see the argument in search.py. */

.topsearch {
  display: flex; align-items: center; gap: .4rem;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: .2rem .7rem;
}
.topsearch svg { color: var(--ink-soft); flex: none; }
.topsearch input {
  border: 0; background: none; padding: .2rem 0; margin: 0;
  width: 11rem; max-width: 30vw; color: var(--ink); font-size: .88rem;
}
.topsearch input:focus { outline: none; }
.topsearch:focus-within { border-color: var(--brand); }
/* On a narrow screen the nav has already wrapped onto its own line; the box
   takes the width it can get rather than pushing the account block off. */
@media (max-width: 640px) { .topsearch input { width: 7rem; } }

.searchbar .grow { flex: 6 1 320px; }

.hits { list-style: none; margin: .5rem 0 0; padding: 0; }
.hit { padding: .55rem 0; border-top: 1px solid var(--line); }
.hit:first-child { border-top: 0; }
.hit-row {
  display: flex; align-items: baseline; gap: 1rem;
  justify-content: space-between; flex-wrap: wrap;
}
.hit-main { text-decoration: none; color: var(--ink); min-width: 0; }
.hit-main:hover .hit-title { text-decoration: underline; }
.hit-title { font-weight: 600; }
.hit-sub { color: var(--ink-soft); font-size: .88rem; margin-left: .5rem; }
.hit-side {
  display: inline-flex; align-items: center; gap: .6rem; flex: none;
}
.hit-meta { color: var(--ink-soft); font-size: .82rem; }
/* Why this row is here. Quiet, because it is an explanation and not the
   answer — but present, because a hit whose title does not contain the term
   reads as a bug until it says what it matched. */
.hit-why {
  margin-top: .2rem; font-size: .84rem; color: var(--ink-soft);
}
.hit-field {
  font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
  font-size: .68rem; color: var(--brand); margin-right: .4rem;
}
/* A staff account that has been switched off. Searched for on purpose,
   because it is not on the default list — so it is labelled, not hidden. */
.b-disabled { background: var(--bad-bg); color: var(--bad); }

.foot {
  text-align: center; color: var(--ink-soft);
  font-size: .82rem; padding: 0 0 2rem;
}

/* ---------------- icons ---------------- */
/* Inline SVG drawn with currentColor, so an icon is the colour of the text
   it sits in and needs no dark-theme rule of its own. */

.ico {
  width: 1em; height: 1em; flex: none;
  stroke: currentColor; fill: none;
  stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
  vertical-align: -.125em;
}
.ico-lg { width: 1.25em; height: 1.25em; }

/* ---------------- flashes ---------------- */

/* The line above a list that opened with the filters from last time. Quieter
   than a flash — it is a statement of fact, not news — but it has to be
   readable, because the thing it explains is rows that are not there. */
.restored {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin-bottom: 1rem; padding: .5rem .75rem;
  border: 1px solid var(--line); border-left: 3px solid var(--info);
  border-radius: var(--radius); background: var(--info-bg);
  font-size: .9rem; color: var(--ink-soft);
}
.restored .ico { color: var(--info); flex: 0 0 auto; }
.restored a { font-weight: 600; }

/* Where a printed page came from. Never on screen — the address bar, the
   filter form and the footer all say this already, and saying it twice is
   clutter. On paper none of the three survive. */
.printed-note { display: none; }

.flashes { margin-bottom: 1rem; }
.flash {
  padding: .7rem .9rem; border-radius: var(--radius);
  margin-bottom: .5rem; border: 1px solid transparent;
  display: flex; gap: .55rem; align-items: flex-start;
  animation: flash-in .22s ease-out;
}
@keyframes flash-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
.flash .ico { margin-top: .2em; }
.flash-body { flex: 1; }
.flash-close {
  background: none; border: 0; cursor: pointer; color: inherit;
  opacity: .6; font: inherit; line-height: 1; padding: 0 .15rem;
}
.flash-close:hover { opacity: 1; }
.flash-success { background: var(--ok-bg); color: var(--ok); border-color: var(--ok-line); }
.flash-danger  { background: var(--bad-bg); color: var(--bad); border-color: var(--bad-line); }
.flash-warning { background: var(--warn-bg); color: var(--warn); border-color: var(--warn-line); }
.flash-info    { background: var(--info-bg); color: var(--info); border-color: var(--info-line); }

/* ---------------- forms ---------------- */

label {
  display: block; font-size: .85rem; font-weight: 600;
  color: var(--ink-soft); margin: .75rem 0 .25rem;
}
label.check { font-weight: 400; display: flex; gap: .4rem; align-items: center; }

input[type=text], input[type=password], input[type=email], input[type=date],
input[type=datetime-local], input[type=tel], input[type=number],
input[type=search], input:not([type]), select, textarea {
  width: 100%; padding: .5rem .6rem;
  border: 1px solid var(--line); border-radius: 7px;
  background: var(--field); color: var(--ink); font: inherit;
}
textarea { resize: vertical; }
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--brand); border-color: var(--brand);
}

.form-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0 1rem; align-items: end;
}
.form-actions { display: flex; gap: .5rem; align-items: end; margin-top: 1rem; }
/* A field that needs the whole row — a multi-line box next to single-line
   ones would otherwise drag the row's height around it. */
.form-grid > .wide { grid-column: 1 / -1; }
/* The two skill boxes: side by side on their own row, stacked when there is
   no room for both. They are a pair and read as one thing. */
.skill-pair {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0 1rem;
}

.filters {
  display: flex; gap: 1rem; flex-wrap: wrap; align-items: end;
}
.filters > div { flex: 1 1 150px; }
.filters label { margin-top: 0; }
/* One field in a filter row that should take the slack — a reason, a search
   box — rather than every field sharing it equally. */
.filters > div.grow { flex: 4 1 320px; }

/* A form sitting in a row of buttons rather than owning its own line. */
form.inline { display: inline; }

/* A list that is a list of facts, not of bullets. */
ul.plain { list-style: none; padding: 0; margin: .3rem 0; }
ul.plain li { padding: .1rem 0; }
.small { font-size: .82rem; }

.inline-form {
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 8px; padding: .6rem .75rem; margin: .5rem 0;
  min-width: 230px;
}
.hint { color: var(--ink-soft); font-size: .82rem; margin: .6rem 0 0; }

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

.btn {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .5rem .9rem;
  border: 1px solid var(--line); border-radius: 7px;
  background: var(--surface); color: var(--ink);
  font: inherit; font-weight: 600; cursor: pointer; text-decoration: none;
}
.btn:hover { border-color: var(--ink-soft); }
.btn-primary { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); }
.btn-primary:hover { background: var(--brand-dark); border-color: var(--brand-dark); }
.btn-ok { background: var(--ok); border-color: var(--ok); color: var(--brand-ink); }
.btn-danger { background: var(--bad); border-color: var(--bad); color: var(--brand-ink); }
.btn-ghost { background: transparent; color: var(--ink-soft); }
.btn-block { display: flex; justify-content: center; width: 100%; margin-top: 1rem; }
.btn-small { padding: .3rem .6rem; font-size: .82rem; }

/* A form that has been submitted. The button says so and stops accepting
   clicks, which is what prevents a double-posted payment. */
.btn[aria-disabled="true"], .btn:disabled {
  opacity: .6; cursor: progress; pointer-events: none;
}

.link {
  color: var(--brand); cursor: pointer; font-size: .85rem;
  text-decoration: none; font-weight: 600;
}
.danger-link { color: var(--bad); font-weight: 600; }
/* A button that is really a link: it does something on this page rather than
   going anywhere, so it is a button in the markup and reads as a link. */
button.link {
  background: none; border: 0; padding: 0; font-family: inherit;
  text-decoration: underline;
}

/* ---------------- punch clock ---------------- */

.login-card h1 { text-align: center; }

.clock-card { text-align: center; }
.clock-card.running { border-color: var(--ok); box-shadow: 0 0 0 3px var(--ok-bg); }
.big-clock {
  font-size: 2.6rem; font-weight: 700; letter-spacing: -1px;
  font-variant-numeric: tabular-nums; margin: .4rem 0;
}
.status-pill {
  display: inline-block; padding: .2rem .7rem; border-radius: 999px;
  font-size: .78rem; font-weight: 700; text-transform: uppercase;
  letter-spacing: .04em;
}
.status-pill.live { background: var(--ok-bg); color: var(--ok); }
.status-pill.idle { background: var(--surface-2); color: var(--ink-soft); }

/* ---------------- stats ---------------- */

.stat-row { display: flex; gap: 1.5rem; flex-wrap: wrap; }
.stat-row.cards { gap: 1rem; }
.stat { flex: 1 1 110px; }
.stat-value {
  display: block; font-size: 1.5rem; font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.stat-value.ok { color: var(--ok); }
.stat-value.warn { color: var(--warn); }
.stat-value.bad { color: var(--bad); }
/* Neither good nor bad: a count of work that is simply still ahead. */
.stat-value.info { color: var(--info); }
.stat-label { color: var(--ink-soft); font-size: .82rem; }

/* A label-and-value list — "Contact: …", "Next payment: …". Two columns on
   a wide screen so the values line up, and one column on a narrow one,
   where a squeezed value column wraps every other line. Unlike .doc-meta
   the value is left-aligned: these are names and dates, not a column of
   figures to compare down the page. */
dl.pairs {
  display: grid; grid-template-columns: max-content 1fr;
  gap: .3rem .9rem; margin: .6rem 0 0;
}
dl.pairs dt { color: var(--ink-soft); }
dl.pairs dd { margin: 0; }
@media (max-width: 640px) {
  dl.pairs { grid-template-columns: 1fr; gap: 0 0; }
  dl.pairs dt { margin-top: .5rem; font-size: .82rem; }
}
.stat-card { text-decoration: none; color: inherit; flex: 1 1 150px; }
.stat-card:hover { border-color: var(--brand); box-shadow: var(--shadow-lift); }

/* A stat you can click: it applies the filter it is counting. A stat-card is
   a card in its own right; this is a cell inside one, so it gets the hover
   and the focus ring without the border. The second line under the label is
   what the number means — "tomorrow onwards" answers, on the page, the
   question the number would otherwise raise. */
.stat-note {
  display: block; color: var(--ink-soft); font-size: .74rem; margin-top: .1rem;
}
.stat-link {
  display: block; text-decoration: none; color: inherit;
  padding: .45rem .6rem; margin: -.45rem -.6rem;
  border-radius: var(--radius); border: 1px solid transparent;
}
.stat-link:hover { background: var(--surface-2); }
.stat-link:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* The filter this board is already showing. Marked so the page and the
   dropdown underneath it can never look like they disagree. */
.stat-link.on { border-color: var(--brand); background: var(--surface-2); }
/* Four numbers and a sentence, above the filters. Tighter than a normal card
   because it is read at a glance and then scrolled past. */
.board .hint { margin-top: .9rem; margin-bottom: 0; }

/* ---------------- tables ---------------- */

table { width: 100%; border-collapse: collapse; font-size: .9rem; }
th {
  text-align: left; font-size: .78rem; text-transform: uppercase;
  letter-spacing: .03em; color: var(--ink-soft);
  border-bottom: 1px solid var(--line); padding: .45rem .5rem;
  background: var(--surface);
  /* A long review queue scrolls past its own headings otherwise. */
  position: sticky; top: 0; z-index: 1;
}
td { padding: .55rem .5rem; border-bottom: 1px solid var(--line); vertical-align: top; }
tbody tr:hover > td { background: var(--surface-2); }
tfoot td { font-weight: 700; border-bottom: none; }
tr.row-muted { opacity: .55; }
/* The support list keeps each row's edit form folded away under it, so a long
   list still reads as a list. */
tr.edit-row td { padding-top: 0; background: var(--surface-2); }
.row-edit summary {
  cursor: pointer; color: var(--ink-soft); font-size: .82rem;
  font-weight: 600; padding: .25rem 0;
}
.row-edit summary:hover { color: var(--brand); }
.row-edit[open] { padding-bottom: .5rem; }
/* Moving an engagement to another developer. On one line and set apart from
   the Save form above it, because it is a different kind of change. */
.move-dev {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
  margin: .6rem 0 0; padding-top: .6rem;
  border-top: 1px dashed var(--line);
}
.move-dev label { margin: 0; font-size: .82rem; font-weight: 600; }
.move-dev select { width: auto; min-width: 14rem; }
.compact td, .compact th { padding: .3rem .5rem; }
/* A record whose last day has passed. Still there, still readable, but it is
   history and should not compete with what is coming. */
tr.is-past > td { color: var(--ink-soft); }
.note { max-width: 240px; color: var(--ink-soft); }
.reason { color: var(--bad); }
.actions { display: flex; flex-direction: column; gap: .35rem; align-items: flex-start; }
.day-head { display: flex; justify-content: space-between; align-items: baseline; }
.day-total { font-weight: 700; font-variant-numeric: tabular-nums; }
.reject-reason {
  background: var(--bad-bg); color: var(--bad);
  padding: .5rem .6rem; border-radius: 7px; margin: .5rem 0; font-size: .85rem;
}

/* ---------------- sortable headings ---------------- */
/* A heading that sorts has to look different from one that does not, or the
   only way to find out is to click everything. The arrow is the whole signal:
   a faint pair of chevrons means "this one sorts", a solid single arrow means
   "this one is sorting, this way". */

th.sortable { padding: 0; }
th.sortable > a {
  display: flex; align-items: center; gap: .3rem;
  padding: .45rem .5rem; color: inherit; text-decoration: none;
  font: inherit; letter-spacing: inherit;
}
th.sortable.num > a { justify-content: flex-end; }
th.sortable > a:hover { color: var(--ink); background: var(--surface-2); }
th.sorted > a { color: var(--brand); }
.ico-sort { width: .95em; height: .95em; flex: none; }
/* Faint until hovered: present enough to invite a click, quiet enough that a
   table of eight headings does not read as eight arrows. */
.ico-idle { opacity: .3; }
th.sortable > a:hover .ico-idle { opacity: .7; }

/* ---------------- the pager ---------------- */

.pager {
  display: flex; flex-wrap: wrap; align-items: center; gap: .6rem 1rem;
  margin-top: .9rem; padding-top: .8rem; border-top: 1px solid var(--line);
  font-size: .85rem;
}
.pager-count { margin: 0; color: var(--ink-soft); }
.pager-count b { color: var(--ink); font-variant-numeric: tabular-nums; }
/* The numbers sit in the middle and the page size at the far end, so the two
   never get mistaken for each other. */
.pager-pages { display: flex; flex-wrap: wrap; gap: .25rem; margin-left: auto; }
.pager-size { display: flex; align-items: center; gap: .25rem; }

.page-link {
  display: inline-flex; align-items: center; gap: .2rem;
  min-width: 2rem; justify-content: center;
  padding: .3rem .55rem; border-radius: 7px;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink-soft); text-decoration: none;
  font-weight: 600; font-variant-numeric: tabular-nums;
}
.page-link:hover { border-color: var(--ink-soft); color: var(--ink); }
.page-link.on {
  background: var(--brand); border-color: var(--brand); color: var(--brand-ink);
}
/* Previous on page one. Still drawn, so the row of controls does not shift
   sideways the moment you use it. */
.page-link.off { opacity: .4; cursor: default; }
.page-link.off:hover { border-color: var(--line); color: var(--ink-soft); }
.page-link .ico { width: .9em; height: .9em; }
.page-gap { padding: .3rem .2rem; color: var(--ink-soft); }

@media (max-width: 700px) {
  /* The words go and the arrows stay: on a phone the row of numbers is what
     matters and "Previous" costs a line of it. */
  .page-link[rel] span { display: none; }
  .pager-pages { margin-left: 0; }
}

/* ---- the week grid -------------------------------------------------------
   Seven fixed columns and a name. Not a .stack table: stacking it would turn
   the one shape that makes a gap visible back into a list, which is the
   thing the grid exists to not be. On a narrow screen it scrolls sideways
   instead, with the name column pinned so you can always see whose row you
   are reading. */
.weekgrid { width: 100%; border-collapse: collapse; }
.wk-nav {
  display: flex; align-items: center; flex-wrap: wrap; gap: .5rem .8rem;
  margin-bottom: .9rem;
}
.wk-range { margin-right: auto; font-size: 1.05rem; }
.wk-range .badge { margin-left: .4rem; vertical-align: .1em; }

.weekgrid th, .weekgrid td {
  border: 1px solid var(--line); padding: .45rem .5rem; text-align: right;
}
.weekgrid thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--surface-2); font-size: .8rem;
}
.weekgrid .wk-name {
  text-align: left; font-weight: 600; white-space: normal;
  position: sticky; left: 0; background: var(--surface); z-index: 2;
  min-width: 11rem;
}
.weekgrid thead .wk-name { background: var(--surface-2); z-index: 3; }
.wk-day { width: 9%; }
.wk-total { width: 11%; }
.wk-dow { display: block; font-weight: 700; }
.wk-date { display: block; font-size: .75rem; color: var(--ink-soft); font-weight: 400; }
.wk-day a { color: inherit; text-decoration: none; display: block; }
.wk-day a:hover { color: var(--brand); }
.wk-day a:hover .wk-date { color: var(--brand); }

/* Today is a column, not a cell: the point is to find it at a glance. */
.weekgrid .wk-today { background: var(--info-bg); }
.weekgrid .wk-weekend { background: var(--surface-2); }
/* A day that has not happened is not empty, it is not yet — and a dash in it
   should not read as an accusation. */
.weekgrid .wk-future { opacity: .45; }
.wk-hours { font-variant-numeric: tabular-nums; }
.wk-none { color: var(--ink-soft); opacity: .6; }
.wk-flag { margin-left: .2rem; vertical-align: -.1em; }
.wk-flag .ico { width: .9em; height: .9em; }
/* The worst status in the cell tints it. Approved is left plain: a grid
   where every filled cell is coloured has no colour left to warn with. */
.weekgrid .wk-pending { background: var(--warn-bg); color: var(--warn); }
.weekgrid .wk-rejected { background: var(--bad-bg); color: var(--bad); }
.weekgrid .wk-open { background: var(--info-bg); }
.weekgrid .wk-totals th, .weekgrid .wk-totals td {
  background: var(--surface-2); border-top: 2px solid var(--line-strong);
}

/* A day off. Hatched rather than tinted, so it cannot be mistaken for one of
   the status colours — this is not a state the hours are in, it is a day
   there were never going to be any. */
.weekgrid .wk-off {
  background: var(--note-bg);
  background-image: repeating-linear-gradient(
    -45deg, transparent, transparent 5px,
    color-mix(in srgb, var(--note) 12%, transparent) 5px,
    color-mix(in srgb, var(--note) 12%, transparent) 10px);
}
.wk-away {
  font-size: .72rem; font-weight: 700; letter-spacing: .02em;
  color: var(--note); text-transform: lowercase;
}

/* Sideways rather than stacked, with an explicit wrapper doing the
   scrolling — the table itself cannot scroll, and a sticky name column needs
   something to be sticky inside. */
.wk-scroll { overflow-x: auto; }
@media (max-width: 900px) {
  .weekgrid { min-width: 42rem; }
  .weekgrid .wk-name { min-width: 8rem; }
}

@media print {
  /* Paper has no sideways scroll: a clipped grid would silently lose the end
     of the week, which on this page is the whole point. */
  .wk-scroll { overflow: visible; }
  .weekgrid { min-width: 0; page-break-inside: avoid; }
  /* Nothing is sticky on paper, and a sticky heading can print twice. */
  .weekgrid thead th, .weekgrid .wk-name { position: static; }
  /* The week's dates stay — a printed grid has to say which week it is — and
     the buttons around them are already hidden by the rule above. */
  .wk-nav { margin-bottom: .5rem; }
  /* .45 of black is too pale to read once the tint behind it is gone. */
  .weekgrid .wk-future { opacity: .7; }
  /* Every tint collapses to white on paper, so the flag icon in the cell is
     what is left to carry the status — except for a rejection, which is worth
     more than an icon and gets a heavy border as well. */
  .weekgrid .wk-rejected { box-shadow: inset 0 0 0 2px #000; }
  /* The hatching survives printing and is the only thing distinguishing a
     day off from an empty one once the colour has gone. It is kept, in grey. */
  .weekgrid .wk-off {
    background-image: repeating-linear-gradient(
      -45deg, transparent, transparent 5px,
      #ddd 5px, #ddd 10px);
  }
}

/* A table marked .stack turns into one card per row on a narrow screen. Each
   cell carries a data-label, which becomes its heading — so the table can be
   read on a phone instead of scrolled sideways one column at a time. Only
   worth the markup on the tables people actually read on a phone. */
@media (max-width: 700px) {
  table.stack thead { display: none; }
  table.stack, table.stack tbody, table.stack tr, table.stack td { display: block; width: 100%; }
  table.stack tr {
    border: 1px solid var(--line); border-radius: 8px;
    margin-bottom: .6rem; padding: .25rem .1rem; background: var(--surface);
  }
  table.stack td {
    border-bottom: 0; padding: .3rem .7rem;
    display: flex; justify-content: space-between; gap: 1rem; text-align: right;
  }
  table.stack td::before {
    content: attr(data-label);
    font-size: .78rem; text-transform: uppercase; letter-spacing: .03em;
    color: var(--ink-soft); font-weight: 700; text-align: left;
  }
  /* A cell with nothing to say adds a blank line to every card. */
  table.stack td:empty { display: none; }
  table.stack tfoot td { border-top: 1px solid var(--line); }
}

.bulk-bar {
  display: flex; gap: .6rem; align-items: center; flex-wrap: wrap;
  border-top: 1px solid var(--line); margin-top: 1rem; padding-top: 1rem;
}
.bulk-bar label { margin: 0; white-space: nowrap; }
.bulk-bar input { flex: 1 1 220px; width: auto; }
/* A date or a status picker in a bulk bar is a fixed-width thing, not a
   text box to be stretched across whatever room is left. */
.bulk-bar input[type="date"] { flex: 0 0 auto; }
.bulk-bar select { width: auto; }

.bulk-count { font-variant-numeric: tabular-nums; color: var(--ink-soft); }
/* What is about to move, beside the button that moves it. */
.bulk-total { font-variant-numeric: tabular-nums; color: var(--ink); }
.bulk-bar button[disabled] { opacity: .5; cursor: not-allowed; }

/* ---------------- empty states ---------------- */
/* A table with headings and no rows looks like a failure. This says what
   would be here and how to put something here. */

.empty {
  text-align: center; padding: 2rem 1rem; color: var(--ink-soft);
}
.empty .ico { width: 2rem; height: 2rem; opacity: .45; stroke-width: 1.5; }
.empty h3 { color: var(--ink); margin: .6rem 0 .25rem; font-size: 1rem; }
.empty p { margin: 0 auto; max-width: 42ch; font-size: .9rem; }
.empty .btn { margin-top: 1rem; }
.empty-inline { padding: 1.1rem .75rem; }

/* ---------------- attachments ---------------- */
/* A list rather than a table: the name is the only column that matters and
   everything else is a caption under it, which is also what makes this
   readable on a phone without a horizontal scrollbar. */

.files { list-style: none; margin: .5rem 0 0; padding: 0; }
.file {
  display: flex; gap: .75rem; align-items: flex-start;
  padding: .7rem .25rem; border-top: 1px solid var(--line);
}
.file:first-child { border-top: 0; }
.file-icon .ico { width: 1.4rem; height: 1.4rem; opacity: .6; }
.file-main { flex: 1; min-width: 0; }
/* Long filenames wrap instead of pushing the buttons off the card. */
.file-main a { font-weight: 600; word-break: break-word; }
.file-main .badge { margin-left: .4rem; vertical-align: 2px; }
.file-note { font-size: .88rem; margin: .15rem 0; }
.file-actions { display: flex; gap: .35rem; flex-shrink: 0; }

.upload { margin-top: 1rem; border-top: 1px solid var(--line); padding-top: 1rem; }
.upload-row { display: flex; gap: .75rem; flex-wrap: wrap; }
.upload-row > div { flex: 1 1 14rem; }
.upload input[type=file] {
  width: 100%; font-size: .9rem; padding: .45rem;
  border: 1px dashed var(--line); border-radius: var(--radius);
  background: var(--surface-2); color: var(--ink);
}

/* ---------------- badges ---------------- */

.badge {
  display: inline-block; padding: .12rem .5rem; border-radius: 999px;
  font-size: .72rem; font-weight: 700; letter-spacing: .03em;
  background: var(--surface-2); color: var(--ink-soft); white-space: nowrap;
}
.b-approved { background: var(--ok-bg); color: var(--ok); }
.b-pending  { background: var(--warn-bg); color: var(--warn); }
.b-rejected { background: var(--bad-bg); color: var(--bad); }
.b-open     { background: var(--info-bg); color: var(--info); }
.b-role     { text-transform: capitalize; }
/* Days off. Sick is the one worth noticing in a list, so it gets the warning
   colour; the other three are statements of fact and stay quiet. */
.b-sick     { background: var(--warn-bg); color: var(--warn); }
.b-leave    { background: var(--info-bg); color: var(--info); }
.b-holiday  { background: var(--note-bg); color: var(--note); }
.b-other    { background: var(--surface-2); color: var(--ink-soft); }
/* The primary skill is the one you staff on, so it is the one that stands
   out; the secondary sits beside it in the plain badge. */
.b-skill    { background: var(--info-bg); color: var(--info); }

.muted { color: var(--ink-soft); }
.error-code { font-size: 3rem; color: var(--ink-soft); margin: 0; }

/* ---------------- WhatsApp ---------------- */

/* A phone number and its chat button, wherever one is shown. Inline-flex so
   the pair never breaks across a line in the middle: a number split from its
   own button reads as two numbers. */
.phone { display: inline-flex; align-items: center; gap: .4rem; }
.phone-number { white-space: nowrap; }

/* WhatsApp's green, and the one place in this stylesheet where a colour is
   not taken from the palette. It is a brand mark: recognising the button
   without reading it is the whole reason for drawing one, and a button in
   the app's own brand colour would just be another button. Both shades are
   the official ones, the darker for the light theme so the text on it keeps
   its contrast. */
.wa {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .12rem .45rem;
  border: 1px solid #1da851; border-radius: 6px;
  background: #25d366; color: #05300f;
  font-size: .78rem; font-weight: 600; line-height: 1.5;
  text-decoration: none; white-space: nowrap;
}
.wa:hover { background: #1da851; color: #fff; }

/* On a list the word is dropped and the glyph carries it, because a column
   of phone numbers is narrow and the button is beside every one of them. The
   title on the link still says where it goes, so nothing is lost for anyone
   reading it aloud or hovering. */
/* `position: relative` on the link, so the label below has a containing
   block inside the table cell. Without it the label is positioned against
   the initial containing block, and in a table 1,292px wide that puts a
   1px box 1,292px from the left of the *document* — which no ancestor can
   clip, because none of them is its containing block. The page then scrolls
   270px sideways into empty space, on every phone, because of five hidden
   labels. It took a headless browser to find: every visible element fitted.
   The same trap applies to `.sr-only` anywhere inside a wide scrolling
   container; see the note there. */
.wa { position: relative; }
td .wa-label, .phone-compact .wa-label {
  position: absolute; width: 1px; height: 1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
td .wa { padding: .12rem .3rem; }

/* Why a number has no button. The mark is for a table cell, the sentence for
   a detail page — see the phone() macro in _ui.html. Both are warn rather
   than bad: a number that cannot be messaged is worth fixing, not an
   emergency, and the record itself is fine. */
.phone-mark { color: var(--warn); cursor: help; display: inline-flex; }
.phone-problem {
  display: inline-flex; align-items: center; gap: .3rem;
  color: var(--warn); font-size: .82rem;
}

/* The two recipients of a proxy briefing, side by side. Side by side on
   purpose: the developer's message and the client's message say different
   things about the same appointment, and the whole reason for showing both
   is that somebody can see the difference before pressing either. */
.send-to {
  display: grid; gap: 1rem;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
}
.send-to h3 { margin: 0 0 .5rem; font-size: .95rem; }

/* The box a message is written in. Not monospace — this is prose somebody
   will read on a phone, not code — but a little smaller than a notes box,
   so a nine-line message is visible without scrolling it. */
textarea.wording { min-height: 9rem; line-height: 1.5; font-size: .88rem; }

/* What will be typed into WhatsApp, shown before it is. A <pre> because the
   briefing has real line breaks in it and they are the layout of the
   message; pre-wrap so a long joining link folds rather than pushing the
   card sideways. */
.message-preview {
  margin: .6rem 0 0; padding: .6rem .7rem;
  border: 1px solid var(--line); border-left: 3px solid #25d366;
  border-radius: var(--radius);
  background: var(--info-bg); color: var(--ink-soft);
  font: inherit; font-size: .84rem; line-height: 1.5;
  white-space: pre-wrap; overflow-wrap: anywhere;
}

/* Printed, a chat button is a green rectangle that does nothing. The number
   beside it is the part that survives paper. */
@media print {
  .wa, .phone-mark { display: none; }
}

/* ---------------- client register ---------------- */

.page-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;
}
.page-head p { margin: .35rem 0 0; color: var(--ink-soft); }
.page-head h1 { display: flex; align-items: center; gap: .5rem; }
.page-head .head-actions { display: flex; gap: .5rem; flex-wrap: wrap; }

/* The date boxes a support status asks for. */
.status-dates {
  border-top: 1px dashed var(--line); margin-top: 1rem; padding-top: .25rem;
  align-items: start;
}
.date-field small { display: block; margin-top: .2rem; font-size: .78rem; }
.no-dates { grid-column: 1 / -1; }

/* "Nothing in this window, and here is where the money actually is."
   Given a left rule rather than left plain, because it is an explanation of
   the zero above it and not another paragraph of page furniture. */
.empty-why {
  border-left: 3px solid var(--line); padding-left: .75rem; margin: 0;
}

.chips { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: 1rem; }
.chip {
  display: inline-flex; gap: .4rem; align-items: baseline;
  padding: .35rem .75rem; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--ink-soft); text-decoration: none;
  font-size: .85rem; font-weight: 600;
}
.chip b { color: var(--ink); }
.chip:hover { border-color: var(--ink-soft); }
.chip.on { outline: 2px solid var(--brand); border-color: var(--brand); }

/* Which half of the business a sum of money belongs to. Two colours rather
   than one, because these sit next to each other down a column of payments
   and the whole reason the column exists is that support and proxy money
   were being read as one thing. Sized down from a filter chip: this is a
   label on a row, not something to click. */
.chip-support, .chip-proxy {
  padding: .1rem .5rem; font-size: .75rem; letter-spacing: .02em;
}
.chip-support { background: var(--ok-bg);   color: var(--ok);   border-color: transparent; }
.chip-proxy   { background: var(--info-bg); color: var(--info); border-color: transparent; }

.nowrap { white-space: nowrap; }

.b-new            { background: var(--info-bg); color: var(--info); }
.b-in_progress    { background: var(--ok-bg);   color: var(--ok); }
.b-on_hold        { background: var(--warn-bg); color: var(--warn); }
/* Completed is finished work. It gets its own colour rather than sharing the
   green of In progress or the blue of New: all three sit in one table and the
   status is read at a glance, so looking alike would be the whole failure. */
.b-completed      { background: var(--note-bg); color: var(--note); }
.b-stopped        { background: var(--bad-bg);  color: var(--bad); }
.b-not_interested { background: var(--surface-2); color: var(--ink-soft); }

/* Where a proxy job is between being booked and being written up. Their own
   classes rather than borrowed support-status ones: the two appear in the
   same column on the proxy list as "settled"/"unsettled", and a booking that
   read in the same green as settled money would be the one misreading that
   matters. */
.b-upcoming       { background: var(--info-bg); color: var(--info); }
.b-today          { background: var(--warn-bg); color: var(--warn); }
.b-noresult       { background: var(--bad-bg);  color: var(--bad); }

/* How a mock interview went. Cancelled is the quiet one on purpose: it is not
   a failure, it is a practice run that did not need to happen, and colouring
   it like a stopped engagement would read as something going wrong. */
.b-planned        { background: var(--info-bg); color: var(--info); }
.b-done           { background: var(--ok-bg);   color: var(--ok); }
.b-cancelled      { background: var(--surface-2); color: var(--ink-soft); }

/* A payment date that has arrived. Red is the whole point, so it is stated
   in one place and used wherever such a date is printed. */
.due { color: var(--bad); font-weight: 700; }
.stat-value.due { color: var(--bad); }
tr.due-row > td { background: var(--bad-bg); }

/* ---------------- invoices ---------------- */
/* An invoice has three states and they are read at a glance down a column,
   so, like the client statuses above, no two may look alike. Draft is
   deliberately the quiet one: it is not a document yet. */
.b-draft { background: var(--surface-2); color: var(--ink-soft); }
.b-sent  { background: var(--info-bg);   color: var(--info); }
.b-void  { background: var(--bad-bg);    color: var(--bad); }

/* Money lines up on the right and nowhere else. A column of figures that is
   not right-aligned cannot be scanned for the odd one out, which is the only
   reason anybody reads a column of figures. */
th.num, td.num { text-align: right; }
th.w-desc { width: 45%; }
.line-editor input { width: 100%; }
.line-editor input.num { text-align: right; }
.line-amount { white-space: nowrap; }

/* The running total under the line editor. Not a card: it belongs to the
   table above it and a border between them would break the connection. */
.totals-strip {
  display: flex; flex-wrap: wrap; gap: 1.25rem; justify-content: flex-end;
  padding: .6rem .5rem; border-top: 2px solid var(--line-strong);
  font-variant-numeric: tabular-nums;
}
.totals-strip span { color: var(--ink-soft); font-size: .88rem; }
.totals-strip b { color: var(--ink); margin-left: .4rem; font-size: 1rem; }
.totals-strip .grand b { font-size: 1.15rem; }

tfoot tr.grand th, tfoot tr.grand td,
tr.grand th, tr.grand td { border-top: 2px solid var(--line-strong); }

/* A value shown rather than edited, sitting where a field would be, so the
   two line up on the same row of a form. */
.static-value { margin: .3rem 0; }

/* An address keeps the line breaks it was typed with; so do notes and terms,
   which are often a list somebody laid out on purpose. */
.address, .pre { white-space: pre-line; margin: .3rem 0; }

/* The document view: the invoice on its own, sized to print. */
.document { background: var(--surface); padding: 2rem; border-radius: 10px; }
.doc-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 2rem; border-bottom: 2px solid var(--line-strong);
  padding-bottom: 1rem; margin-bottom: 1.5rem;
}
.doc-head h1 { margin: 0; }
.doc-number { font-size: 1.1rem; font-weight: 700; margin: .2rem 0 0; }
.doc-meta dl {
  display: grid; grid-template-columns: auto auto; gap: .2rem .8rem; margin: 0;
}
.doc-meta dt { color: var(--ink-soft); }
.doc-meta dd { margin: 0; text-align: right; font-variant-numeric: tabular-nums; }
.doc-parties { margin-bottom: 1.5rem; }
.doc-lines { margin-bottom: 1.5rem; }
/* Said once, large, because a void invoice that looks like a valid one is
   the one mistake on this page that costs real money. */
.stamp {
  display: inline-block; margin-top: .6rem; padding: .2rem .8rem;
  border: 3px solid var(--bad); color: var(--bad);
  font-size: 1.4rem; font-weight: 800; letter-spacing: .15em;
  transform: rotate(-4deg);
}
@media print {
  .document { padding: 0; }
}

/* ---------------- printing ---------------- */
/* Anything can be printed, but this exists for the things meant to be sent:
   an invoice and a statement. Ink is expensive and a screenshot of a web page
   is not a document. */

@media print {
  /* Print on white with black text whatever theme the screen was in. */
  :root, [data-theme="dark"] {
    color-scheme: light;
    --bg: #fff; --surface: #fff; --surface-2: #fff; --field: #fff;
    --ink: #000; --ink-soft: #444; --line: #999; --line-strong: #666;
    --brand: #000; --brand-dark: #000; --brand-ink: #fff;
    --ok: #000; --ok-bg: #fff; --warn: #000; --warn-bg: #fff;
    --bad: #000; --bad-bg: #fff; --info: #000; --info-bg: #fff;
    --note: #000; --note-bg: #fff;
    --shadow: none; --shadow-lift: none;
  }
  .topbar, .foot, .flashes, .filters, .chips, .bulk-bar,
  .no-print, .form-actions, .btn, .row-edit, .bell { display: none !important; }
  /* The pager's *links* go; its count stays. "Showing 26–50 of 137" is the
     one thing that stops a printed page of rows from looking like the whole
     list, and on paper there is nothing else left to say otherwise. */
  .pager-pages, .pager-size { display: none !important; }
  .pager { border: 0; padding: .25rem 0 0; }
  .printed-note {
    display: flex; justify-content: space-between; gap: 1rem;
    font-size: .72rem; color: #444;
    border-bottom: 1px solid #ccc; padding-bottom: .25rem; margin-bottom: .6rem;
  }
  .printed-where { font-family: ui-monospace, monospace; word-break: break-all; }
  /* A sorted heading keeps its label and loses its arrow. */
  th.sortable > a { padding: 0; color: #000; }
  .ico-sort { display: none !important; }
  /* Kept on paper deliberately. The filter form above it is hidden, so this
     line is the only thing left saying the list is a narrowed one — and it
     matters most here, because a printout outlives the screen it came from.
     The link is dropped: nothing to click on a page. */
  .restored { border: 0; padding: 0; background: none; color: #000; }
  .restored a, .restored .ico { display: none !important; }
  .card {
    border: 0; box-shadow: none; padding: 0; margin: 0 0 1rem;
    overflow: visible !important; break-inside: avoid;
  }
  .wrap { max-width: none; padding: 0; }
  body { font-size: 12px; }
  th { position: static; border-bottom: 1px solid #000; }
  a { text-decoration: none; color: #000; }
  /* A printed table must not be cut in half across a page break. */
  tr { break-inside: avoid; }
  thead { display: table-header-group; }
}

/* Somebody who has asked their system not to animate things. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important; transition-duration: .001ms !important;
  }
}

/* ---------------- statements ---------------- */
/* The brought-forward line is not a transaction — it is where the account
   stood before the period began — so it is set apart from the rows below. */
tr.opening > td {
  background: var(--surface-2); border-bottom: 2px solid var(--line-strong);
}
.closing {
  margin: 1.25rem 0; padding-top: 1rem;
  border-top: 2px solid var(--line-strong);
}
.closing .doc-number { font-size: 1.25rem; }
.doc-foot { margin-top: 1.5rem; padding-top: .75rem; border-top: 1px solid var(--line); }

/* ---------------- client requests ---------------- */
/* Three statuses again, and In progress deliberately borrows the green it
   already has on the client register — the same words should not mean two
   different colours in one application. */
.b-open     { background: var(--info-bg); color: var(--info); }
.b-resolved { background: var(--ok-bg);   color: var(--ok); }

/* The number on the Requests tab. Only drawn when something is unanswered,
   so its presence is the message and its absence is the inbox-zero. */
.nav-count {
  display: inline-block;
  min-width: 1.25rem;
  padding: 0 .35rem;
  margin-left: .35rem;
  border-radius: 999px;
  background: var(--bad);
  color: #fff;
  font-size: .72rem;
  font-weight: 700;
  line-height: 1.25rem;
  text-align: center;
}

/* A thread reads top to bottom, oldest first. The two sides are offset
   rather than left- and right-aligned: a hard right-align reads as a chat
   app and makes long paragraphs hard to follow, which is what these are. */
.thread { display: flex; flex-direction: column; gap: .85rem; }

.msg {
  border: 1px solid var(--line);
  border-left: 3px solid var(--line);
  border-radius: var(--radius);
  padding: .7rem .85rem;
  background: var(--surface);
}
.msg > header {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  align-items: baseline;
  margin-bottom: .4rem;
}
.msg-body { margin: 0; }

/* Theirs, ours, and a note to ourselves. The note is the one that must never
   be mistaken for a reply, so it is the one that does not look like the
   other two: a different background, not just a different edge. */
.msg-them { border-left-color: var(--info); }
.msg-us   { border-left-color: var(--brand); margin-left: 1.5rem; }
.msg-note {
  border-left-color: var(--warn);
  background: var(--warn-bg);
  margin-left: 1.5rem;
}

/* The office speaking in somebody else's conversation. Deliberately not
   `.msg-note`: a note is the thing the client must never see, and this is
   the opposite — everyone in the chat reads it. It is centred rather than
   offset to either side, because it belongs to neither of them. */
.msg-office {
  border-left-color: var(--note);
  background: var(--note-bg);
  margin-left: 0;
}

/* Where you got to last time. A line rather than a badge, because its job
   is to be scrolled past and found, not counted. */
.thread-divider {
  display: flex;
  align-items: center;
  gap: .6rem;
  margin: .2rem 0;
  font-size: .78rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--brand);
}
.thread-divider::before,
.thread-divider::after {
  content: ""; flex: 1; height: 1px; background: var(--brand);
  opacity: .45;
}

/* The last thing said, in a list of conversations. Clamped to two lines:
   a list answers "who is waiting on me", and a paragraph in every row
   buries the answer. */
.chat-snippet {
  margin: .25rem 0 0;
  color: var(--ink-soft);
  font-size: .85rem; line-height: 1.45;
  overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}

@media (max-width: 640px) {
  .msg-us, .msg-note { margin-left: 0; }
}

/* Visible to a screen reader, not on the page. For a label on a control
   whose purpose the surrounding text already makes obvious. */
/* Visually hidden, still read aloud. `position: absolute` needs a
   positioned ancestor wherever this sits inside something wide and
   horizontally scrollable, or the 1px box lands at the far right of the
   *document* and makes the whole page scroll sideways — see the note on
   `.wa-label`. Inside the top bar and inside forms it has one; if you add
   it to a wide table cell, give the cell `position: relative`. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---------------- portal banner ---------------- */
/* One notice at the top of the portal home, coloured by how urgent it is.
   The colours are the four the app already uses for state, so "red means
   something is wrong" needs learning once. */
.banner {
  display: flex;
  gap: .9rem;
  align-items: flex-start;
  padding: 1rem 1.1rem;
  margin-bottom: 1rem;
  border: 1px solid var(--line);
  border-left: 4px solid var(--ink-soft);
  border-radius: var(--radius);
  background: var(--surface);
}
.banner h2 { margin: 0 0 .25rem; font-size: 1.05rem; }
.banner p  { margin: 0; color: var(--ink-soft); }
.banner .banner-icon { flex: 0 0 auto; line-height: 1; padding-top: .1rem; }

.banner-bad  { border-left-color: var(--bad);  background: var(--bad-bg); }
.banner-bad .banner-icon { color: var(--bad); }
.banner-warn { border-left-color: var(--warn); background: var(--warn-bg); }
.banner-warn .banner-icon { color: var(--warn); }
.banner-info { border-left-color: var(--info); background: var(--info-bg); }
.banner-info .banner-icon { color: var(--info); }
.banner-ok   { border-left-color: var(--ok);   background: var(--ok-bg); }
.banner-ok .banner-icon { color: var(--ok); }

/* ---------------- charts ---------------- */
/* The SVG has no fixed width: it is drawn in its own coordinate space and
   stretched to whatever the card gives it, which is how it stays sharp on a
   phone and on a wide screen without a script measuring anything. */
.chart { margin: 0; }
.chart svg { width: 100%; height: 180px; display: block; }
.chart-axis { stroke: var(--line); stroke-width: 1; }
.chart-bar  { fill: var(--brand); }
.chart-bar:hover { fill: var(--brand-dark, var(--brand)); }
.chart-label {
  /* Drawn in a stretched coordinate space, so the text is stretched with
     it. Small and evenly spaced is the only thing that survives that. */
  fill: var(--ink-soft);
  font-size: 11px;
}
.chart-figures { margin-top: .5rem; }
.chart-figures summary { cursor: pointer; }
.chart-figures table { margin-top: .5rem; }

/* Series colours for a grouped chart. Money coming in is the brand colour
   and money going out is the warning one; the two are also labelled in the
   key and written out in the figures table, so nothing here is the only
   thing telling the two series apart. */
.chart-bar-in  { fill: var(--brand); }
.chart-bar-out { fill: var(--warn); }
.chart-bar-in:hover  { fill: var(--brand-dark, var(--brand)); }
.chart-bar-out:hover { fill: var(--bad, var(--warn)); }

.chart-key {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem 1rem;
  margin: .4rem 0 0;
  color: var(--ink-soft);
}
.chart-key-item { display: inline-flex; align-items: center; gap: .35rem; }
.chart-swatch {
  width: .7rem;
  height: .7rem;
  border-radius: 2px;
  /* The swatches reuse the bar classes, which set `fill` for SVG. In HTML
     it is `background` that shows, so each one is repeated here. */
  background: var(--line-strong);
}
.chart-swatch.chart-bar-in  { background: var(--brand); }
.chart-swatch.chart-bar-out { background: var(--warn); }

@media print {
  /* The figures are the point on paper; a details element prints closed. */
  .chart-figures[open] summary { display: none; }

  /* Everything goes black on paper, which would make two series one solid
     block. The second is drawn hollow instead, so the picture still says
     which bar is which without relying on a colour that is not printing. */
  .chart-bar-out, .chart-swatch.chart-bar-out {
    fill: #fff;
    background: #fff;
    stroke: #000;
    border: 1px solid #000;
  }
}

/* ---------------- the calendar ----------------
   One date control for every module. The native <input type="date"> is
   still the field that submits; it is masked rather than removed, because
   a display:none input cannot be focused and Chrome then refuses to show
   its own "please fill this in" bubble on a required date. Opacity keeps
   it focusable and invisible, and the bubble lands over the right box. */

.cal { position: relative; display: block; }
/* In a filter row or a bulk bar these boxes size themselves, so the
   wrapper has to stop being a full-width block and let them. */
.filters .cal, .bulk-bar .cal { display: block; width: 100%; }
.bulk-bar .cal { width: auto; }

.cal-js > input[type=date],
.cal-js > input[type=datetime-local] {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  margin: 0; padding: 0; border: 0;
  opacity: 0; pointer-events: none;
}

.cal-text {
  /* Room on the right for the button that sits over it. */
  padding-right: 2.1rem !important;
  font-variant-numeric: tabular-nums;
}
.cal-btn {
  position: absolute; right: 1px; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; justify-content: center;
  width: 2rem; height: calc(100% - 2px);
  border: 0; border-radius: 0 7px 7px 0;
  background: none; color: var(--ink-soft); cursor: pointer;
}
.cal-btn:hover { color: var(--brand); }
.cal-btn:disabled { cursor: not-allowed; opacity: .5; }

.cal-pop {
  position: absolute; z-index: 60; top: calc(100% + 4px); left: 0;
  width: 17.5rem; padding: .6rem;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  box-shadow: var(--shadow-lift);
}
.cal-pop.up    { top: auto; bottom: calc(100% + 4px); }
.cal-pop.right { left: auto; right: 0; }

.cal-head { display: flex; gap: .25rem; align-items: center; }
.cal-head select {
  width: auto; flex: 1 1 auto; padding: .25rem .3rem; font-size: .85rem;
}
.cal-nav {
  flex: 0 0 auto; width: 1.75rem; height: 1.75rem;
  border: 1px solid var(--line); border-radius: 6px;
  background: var(--surface-2); color: var(--ink);
  font-size: 1.1rem; line-height: 1; cursor: pointer;
}
.cal-nav:hover { border-color: var(--brand); color: var(--brand); }

.cal-dow, .cal-grid {
  display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px;
}
.cal-dow {
  margin-top: .5rem;
  font-size: .7rem; font-weight: 600; color: var(--ink-soft);
  text-align: center; text-transform: uppercase; letter-spacing: .03em;
}
.cal-grid { margin-top: .2rem; }

.cal-day {
  padding: .35rem 0; border: 1px solid transparent; border-radius: 6px;
  background: none; color: var(--ink); cursor: pointer;
  font: inherit; font-size: .85rem; font-variant-numeric: tabular-nums;
}
.cal-day:hover:not(:disabled) { background: var(--surface-2); }
.cal-day:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* The days either side of this month are shown rather than left blank, so
   the grid keeps its shape and a date near a month boundary is one click
   away — but they are quiet, because they are not what you came for. */
.cal-day.out { color: var(--ink-soft); opacity: .55; }
.cal-day.we  { color: var(--ink-soft); }
.cal-day.now { border-color: var(--brand); font-weight: 700; }
.cal-day.sel {
  background: var(--brand); border-color: var(--brand);
  color: var(--brand-ink); font-weight: 700;
}
.cal-day:disabled {
  color: var(--ink-soft); opacity: .3; cursor: not-allowed;
  text-decoration: line-through;
}

.cal-time {
  display: flex; gap: .5rem; align-items: center;
  margin-top: .6rem; padding-top: .6rem; border-top: 1px solid var(--line);
}
.cal-time label { margin: 0; flex: 0 0 auto; }
.cal-tin { width: auto !important; flex: 1 1 auto; padding: .3rem .4rem; }

.cal-foot {
  display: flex; gap: .4rem; justify-content: space-between;
  margin-top: .6rem; padding-top: .5rem; border-top: 1px solid var(--line);
}
.cal-today, .cal-clear, .cal-done {
  padding: .3rem .6rem; border: 1px solid var(--line); border-radius: 6px;
  background: var(--surface-2); color: var(--ink);
  font: inherit; font-size: .8rem; cursor: pointer;
}
.cal-today:hover:not(:disabled), .cal-clear:hover, .cal-done:hover {
  border-color: var(--brand); color: var(--brand);
}
.cal-done {
  background: var(--brand); border-color: var(--brand); color: var(--brand-ink);
}
.cal-done:hover { background: var(--brand-dark); color: var(--brand-ink); }
.cal-today:disabled { opacity: .4; cursor: not-allowed; }
.cal-clear { margin-left: auto; color: var(--ink-soft); }

@media print {
  .cal-btn, .cal-pop { display: none !important; }
}

/* An amount and the currency it is in, side by side, because they are one
   answer. The select is sized to its content so the box next to it keeps
   the width a figure actually needs. */
.money-pair { display: flex; gap: .4rem; align-items: stretch; }
.money-pair input { flex: 1 1 auto; min-width: 0; }
.money-pair select { flex: 0 0 auto; width: auto; }

/* ==========================================================================
   On a phone
   ==========================================================================
   Measured rather than guessed. `_mobile_audit.py` drives a headless browser
   over every page at 390px and 320px and asks the only question that
   decides whether a page works on a phone: is the document wider than the
   screen. A page that fits needs no pinching and no sideways scrolling of
   the whole layout; a page that does not is unusable in a way no amount of
   font tuning fixes.

   The top bar was the floor under nearly every page. `.user-block` is a
   flex row of six things — search, bell, name, theme, password, sign out —
   and it did not wrap, so it set a minimum width of 487px that every page
   inherited whatever was on it. Most of what follows is about giving that
   row somewhere to go. */

@media (max-width: 760px) {
  .topbar { gap: .45rem .7rem; padding: .55rem .85rem; }
  .user-block { flex-wrap: wrap; gap: .5rem; margin-left: auto; }

  /* The role under the name goes first. The name alone still answers "who
     am I signed in as", and the role is on every page that acts on it. */
  .whoami small { display: none; }
  .whoami { font-weight: 600; font-size: .86rem; }

  /* The search box takes its own line rather than squeezing the five
     controls beside it down to nothing. `order` puts it last in the bar
     and full width, which on a phone is where a search box belongs. */
  .topsearch { order: 10; flex: 1 1 100%; }
  .topsearch input { width: 100%; max-width: none; }

  /* A grouped menu anchors to the whole bar instead of to its own heading.
     Six headings wrap onto two lines, and a panel hung off the last one
     would open past the right edge of the screen. */
  .topbar nav { position: relative; }
  .nav-group { position: static; }
  .nav-menu,
  .nav-group:last-child .nav-menu {
    left: 0; right: 0; width: auto; min-width: 0; max-width: none;
  }
}

@media (max-width: 480px) {
  /* The name goes too. Who you are is on your own page; a 390px bar has
     room for the things you press and not for a label. */
  .whoami { display: none; }
  .quiet { font-size: .82rem; }
  .topbar { padding: .5rem .6rem; }
  .wrap { padding-left: .7rem; padding-right: .7rem; }
  .card { padding: .9rem .8rem; }
  h1 { font-size: 1.35rem; }
  h2 { font-size: 1.1rem; }
}

/* ---- things that must not set a minimum width ---------------------------
   A grid of fixed-minimum columns is the other way a page gets too wide: at
   190px a minimum, two columns plus a gap is already 400px. Below the
   breakpoints each of these becomes a single column. */
@media (max-width: 560px) {
  .form-grid, .skill-pair { grid-template-columns: 1fr; }
  .stat-row { gap: .6rem; }
  .stat, .stat-card { flex-basis: 100%; }
  .page-head { flex-wrap: wrap; gap: .6rem; }
  .head-actions { flex-wrap: wrap; }
  /* A filter bar is several selects; one per line beats six squeezed. */
  .filters { grid-template-columns: 1fr; }
}

/* ---- a wide table scrolls itself, never the page ------------------------
   `.card { overflow-x: auto }` above is the container half of this. This is
   the other half: a table that is a flex or grid item has `min-width: auto`
   by default, which means it refuses to shrink below its content and pushes
   its container open from the inside. Nothing here changes what a table
   looks like — it only lets the scrollbar belong to the table instead of to
   the document. */
@media (max-width: 760px) {
  /* The scrollbar belongs to the card, not to the table. Putting
     `display: block; overflow-x: auto` on the <table> itself looks like it
     works — the table reports a sensible width — but the internal table box
     is still laid out at its full width and Chromium keeps counting it in
     the *document's* scroll area, so the page scrolls sideways while every
     element on it fits. The card is an ordinary block and clips properly.
     `min-width: 0` is the other half: a flex or grid item defaults to
     `min-width: auto`, which means it refuses to shrink below its content
     and pushes its own container open from the inside. */
  .card, .grid > *, .grid-2 > * { min-width: 0; }
}

/* A sticky heading inside a horizontally scrolling table keeps contributing
   to the *document's* scroll width in Chromium, even though the table itself
   is contained — so the page scrolled sideways while every element on it
   fitted. That took a headless browser to find: `body.scrollWidth` was 390
   and `documentElement.scrollWidth` was 660, which is the signature of a
   positioned element escaping the body box.
   A heading has nothing useful to stick to on a phone anyway: the table is
   a few rows in a box you scroll, not a long queue you lose your place in. */
@media (max-width: 760px) {
  .card > table th { position: static; }
}

/* A statement or an invoice is a printed document first, so it carries
   generous padding and a two-column header. On a 320px screen that padding
   alone is a fifth of the width and the header's two columns plus their gap
   cannot both fit. The print rules are untouched — this only applies to a
   phone. */
@media (max-width: 560px) {
  .document { padding: 1.1rem; }
  .doc-head { flex-wrap: wrap; gap: 1rem; }
  /* Right-aligned was for a column of figures beside a wide document. Once
     the header has wrapped, the meta block is the full width and reading
     its values off the left edge matches everything above it. */
  .doc-meta dd { text-align: left; }
  .doc-meta dl { grid-template-columns: max-content 1fr; }
}

/* ---- thumbs, not cursors ------------------------------------------------
   Only where the pointer is coarse, so nothing moves on a desktop. A mouse
   hits a 16px link without thinking; a thumb does not, and the controls
   worth enlarging are the ones that *do* something — a row's Open link, the
   WhatsApp button, the theme switch — rather than every link inside a
   sentence, where forcing a tap height would space the prose out instead. */
@media (hover: none) and (pointer: coarse) {
  .btn, .wa, .theme-toggle, .quiet,
  td > a, td > .link, .pager a, .chip {
    min-height: 2.15rem;
    display: inline-flex; align-items: center; justify-content: center;
  }
  td > a, td > .link { justify-content: flex-start; }
  /* A cell holding two of them wants them apart, not touching. */
  td > a + a, td > a + form { margin-left: .5rem; }
  .wa { padding: .25rem .5rem; }
  /* A checkbox in a bulk-select column is the smallest target of all. */
  input[type="checkbox"], input[type="radio"] {
    width: 1.15rem; height: 1.15rem;
  }
}

/* ---- looking at the application as somebody else ------------------------
   Above the top bar, full width, in a colour nothing else on the page uses.
   It is deliberately loud: an administrator who forgets a view is open will
   find every button refused and conclude the application is broken, and the
   fix for that is not a subtler banner.
   Sticky with the bar rather than scrolling away, because the moment it is
   needed most is three pages into somebody else's account. */
.viewing-bar {
  position: sticky; top: 0; z-index: 30;
  display: flex; align-items: center; justify-content: space-between;
  gap: .75rem; flex-wrap: wrap;
  padding: .5rem 1.25rem;
  background: #4c1d95; color: #fff;
  font-size: .88rem;
}
.viewing-bar span { display: inline-flex; align-items: center; gap: .45rem; }
.viewing-bar strong { color: #fff; }
.viewing-bar svg { flex: none; }
.viewing-bar .btn {
  background: #fff; color: #4c1d95; border-color: #fff; font-weight: 700;
}
.viewing-bar .btn:hover { background: #ede9fe; color: #4c1d95; }
/* The top bar is sticky too; it sits under this one rather than over it. */
.viewing-bar ~ .topbar { top: 2.4rem; z-index: 20; }
@media (max-width: 560px) {
  .viewing-bar { padding: .45rem .7rem; font-size: .8rem; }
  .viewing-bar ~ .topbar { top: 0; position: static; }
}
/* A printed page is a document, not a screen. */
@media print { .viewing-bar { display: none; } }
