/* handle.css - the Handle's own design system.
 *
 * The Handle is an internal tool that one or two people keep open all day, so
 * it is designed for density and legibility rather than for impact. It is a
 * DIFFERENT PRODUCT from the publisher sites and deliberately does not import
 * their stylesheets: static/publisher/css/* is a public reading layout with a
 * fluid type scale, a page cache and a per-site token override, none of which
 * applies here, and linking it would tie an operator screen to a theme a client
 * can edit. What the two do share is a family resemblance - the same indigo and
 * slate, the same three-step text ramp, the same data-theme mechanism - so the
 * Handle reads as part of the platform without inheriting its constraints.
 *
 * NO FRAMEWORK, NO BUILD STEP, NO SCRIPT DEPENDENCY. One file, plain CSS,
 * collected by collectstatic and served by whitenoise. The only JavaScript on a
 * Handle page is the four-line theme toggle in base.html; every rule below is
 * inert without it.
 *
 * THEME. `html[data-theme="dark"]`, stamped SERVER-SIDE in base.html from the
 * `handle_theme` cookie. The publisher sites have to stamp theirs from a
 * pre-paint script because their HTML is cached for every visitor at once; the
 * Handle's pages are rendered per session and never cached, so the attribute
 * can just be printed and there is no flash to guard against. Light is the
 * default because that is what the client is looking at, not because dark is an
 * afterthought - the ramp below is designed twice.
 *
 * CLASS CONTRACT. The screen templates are not owned by this file and consume a
 * fixed vocabulary: .grid .card .note .wide .pill .ok .warn .bad .off .muted
 * .zero .big .stat .label .bar and `td.n`. Every one of them means here what it
 * meant in the inlined stylesheet this file replaces. Four names joined them
 * when the approval queue was brought in off its own page-local stylesheet -
 * .media (with .thumb), .controls and .btn-danger - and each is a shape any
 * screen can ask for rather than a rule about that one: a record with a picture,
 * a row of controls, and the button role the token block already had colours
 * for. Two behaviours were tightened, both of them bugs rather than taste:
 *
 *   1. `.bar` IS A METER AND NOTHING ELSE. It used to collide with the page
 *      header, which was also `class="bar"` - see base.html for the whole
 *      story. The chrome is now `.topbar`, so this name belongs to the meter
 *      alone, and its fill is matched as a DIRECT child (`.bar > span`) so the
 *      rule can never reach into a nested element again.
 *   2. `.ok/.warn/.bad/.off` colour TEXT. They only grow a plate when they are
 *      also `.pill`. Previously a bare `<p class="bad">` picked up the pill's
 *      pink block, and `<div class="bar warn">` fought the meter track for its
 *      own background.
 *
 * WHAT DOES NOT BELONG HERE. Rules for one screen. A page that needs its own -
 * the dashboard's chart palette, for instance - puts them in base.html's `head`
 * block or in its own file next to this one. This file is the vocabulary every
 * Handle screen shares, and it stops being that the moment it grows a section
 * per page.
 */

/* ==========================================================================
   1. Tokens
   ========================================================================== */

:root {
  color-scheme: light;

  /* --- surfaces ---------------------------------------------------------
     Four steps of recession: the page, the card, the quiet band inside a card
     (table heads, the topbar), and the control well (inputs, meter tracks). */
  --bg: #f4f6f9;
  --surface: #ffffff;
  --surface-2: #f7f9fc;
  --surface-3: #edf1f7;

  --line: #e2e7ef;
  --line-2: #cbd3e0;

  /* --- text -------------------------------------------------------------
     Three inks that may carry words, and a fourth that may not.

     --ink-3 is the muted layer, which on this product is most of the page:
     it was darkened until it clears 4.5:1 on --bg and --surface-3, not only
     on white. It is also the FLOOR. There is no legible ink quieter than it
     in this ramp: the lightest slate that still clears 4.5:1 on every one of
     the four surfaces is #606d80, which is --ink-3 to the eye. So "quieter
     than --ink-3" is not a thing this palette can offer, and a rule that
     wants it is a rule that wants illegible text.

     --ink-4 IS NOT A TEXT COLOUR. It is the non-text absence tone: a hairline
     accent, a disabled border, the stub that marks a day whose value was
     zero. It was previously described as the "this number is zero / this
     field is empty" grey and "held to no ratio, because it never carries
     information the operator has to read" - which was wrong twice. It painted
     .zero, and .zero renders the literal "0" in "0 comments pending" and the
     word "never" under Last publish; those are the readings the dashboard
     exists to give, not decoration. At 2.27:1 on --surface-3 and 2.58:1 on
     white it was the least legible text on the page. Both text uses (.zero
     and ::placeholder) now take --ink-3; what is left of --ink-4 draws no
     glyphs. Do not paint a word with it. */
  --ink: #131722;
  --ink-2: #3c4657;
  --ink-3: #5c687b;
  --ink-4: #98a2b3;

  /* --- brand -------------------------------------------------------------
     Indigo, the same hue the sites use. Three jobs, three tokens, for the
     reason the publisher tokens spell out: --brand is graphics and borders,
     --brand-strong is brand-coloured WORDS, --brand-fill is a plate carrying
     a white label and must stay dark in both themes. */
  --brand: #4f46e5;
  --brand-strong: #4338ca;
  --brand-soft: #eef2ff;
  --brand-fill: #4f46e5;
  --brand-fill-hover: #4338ca;
  --on-brand: #ffffff;

  /* --- status ------------------------------------------------------------
     Four states, because that is what apps/control/views/dashboard.py emits:
     STATE_LABELS = ok / warn / bad / off. Each has an ink (text-safe on both
     the page and a card) and a plate (the pill fill, which carries the ink). */
  --ok: #04724d;
  --ok-bg: #e4f4ec;
  --ok-line: #a7d9c2;
  --warn: #92400e;
  --warn-bg: #fdf1e3;
  --warn-line: #ecc59a;
  --bad: #b4123f;
  --bad-bg: #fdebef;
  --bad-line: #f0b6c3;
  /* The destructive PLATE, and the third of these that is not a shade of
     --bad. --bad is an ink and goes pastel in dark so it can be read on a
     navy card; a white label on that pastel is 1.6:1. A button that deletes
     something has to stay deep in both themes for exactly the reason
     --brand-fill does, and it is separate from --bad-bg because that plate
     carries --bad and this one carries white. Used by the Handle's own
     confirm screen and by admin-theme.css for the admin's delete link, its
     "Yes, I'm sure" and every intermediate action page. */
  --bad-fill: #b4123f;
  --bad-fill-hover: #91092f;
  --on-bad: #ffffff;
  --off: #5c687b;
  --off-bg: #eef1f6;
  --off-line: #d3dae4;

  /* --- informatics --------------------------------------------------------
     Exposed as tokens so an inline SVG can write fill="var(--chart-1)" and
     stay in step with the theme. No charting library is involved and none is
     wanted: an <svg> the view generates costs nothing, needs no script, and
     survives a strict CSP. Five categoricals, one grid line, one track. */
  --chart-1: #4f46e5;
  --chart-2: #0284c7;
  --chart-3: #0f766e;
  --chart-4: #7c3aed;
  --chart-5: #b45309;
  --chart-grid: #e2e7ef;
  --chart-track: #e7ecf3;

  --link: #4338ca;
  --link-hover: #312e9e;
  --focus: #4f46e5;

  /* --- type ---------------------------------------------------------------
     System stack. A stepped scale, not a fluid one: the publisher's clamp()
     ladder exists so a reading page survives browser zoom, and this product's
     problem is the opposite - an operator comparing two numbers in two tables
     needs them the same size on every screen the Handle opens on. */
  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;
  --fs-2xs: 0.6875rem;  /* 11 - pill and legend */
  --fs-xs: 0.75rem;     /* 12 - table head, meta */
  --fs-sm: 0.8125rem;   /* 13 - secondary copy, nav */
  --fs-md: 0.875rem;    /* 14 - body */
  --fs-lg: 1rem;        /* 16 - card title, wordmark */
  --fs-xl: 1.1875rem;   /* 19 - page title */
  --fs-2xl: 1.5rem;     /* 24 - the number on a stat. It was 28, which is a
                           poster's size for a figure that is read at a glance
                           and sits four to a card; the ramp under it is
                           unchanged, so the step from the 16px card title is
                           still unmistakable and each stat row is 4px shorter. */
  --lh: 1.5;
  --lh-tight: 1.25;

  /* --- space --------------------------------------------------------------
     A 4px grid. Dense but not cramped means the small steps get used a lot
     and the large ones almost never. */
  --s-1: 4px;
  --s-2: 6px;
  --s-3: 8px;
  --s-4: 12px;
  --s-5: 16px;
  --s-6: 20px;
  --s-7: 28px;
  --s-8: 40px;

  /* --- density ------------------------------------------------------------
     THE KNOBS THE WHOLE PRODUCT IS TUNED WITH, and the reason they are tokens
     rather than values typed into forty rules: this is a tool one or two people
     keep open all day, the client's complaint was that it takes too much room
     to say what it says, and "how much room" has to be one decision rather than
     forty. Every card, control, table cell and form row below measures itself
     from these five, and handle-admin.css measures Django's twenty-six model
     screens from the same five, so the admin and the hand-built screens cannot
     drift into two densities.

     They are deliberately not a second spacing scale. --s-1..--s-8 stay the
     vocabulary for anything one-off; these name the four decisions that repeat
     on every screen, so changing the product's density is changing four lines.

     --cell-y is 5px and not a step on the 4px grid on purpose: at --fs-sm and
     line-height 1.4 that is a 28px row, which is the shortest row that still
     lets a 15px info glyph and a pill sit in it without either one setting the
     height. 4px would, 6px is the row we are coming from.

     --label-w is the admin change form's label column. It is here rather than
     in handle-admin.css because the help text under a field has to hang off the
     same measure, and that is three rules in two files agreeing about one
     number - which is what a token is for. */
  --pad-card: var(--s-4);
  --gap: var(--s-4);
  --cell-y: 5px;
  --cell-x: var(--s-3);
  --ctl-y: 4px;
  --ctl-x: var(--s-4);
  --ctl-h: 28px;
  --label-w: 170px;

  --r-sm: 4px;
  --r: 6px;
  --r-lg: 10px;
  --r-pill: 999px;

  --shadow-1: 0 1px 2px rgba(15, 23, 42, 0.05);
  --shadow-2: 0 6px 20px rgba(15, 23, 42, 0.10);

  /* --- layout -------------------------------------------------------------
     --wrap is the cap on the content column, and it is 1600 rather than 1280
     on purpose. The brief is "usable at 1280px and still working at 1440+
     without becoming a field of whitespace": at 1280 and at 1440 the content
     simply fills the window minus its gutters, and the cap only starts doing
     anything on a monitor wider than most of the tables on this product.
     --topbar-h is shared by the sticky header and the sticky nav's `top`, so
     the two cannot drift apart. */
  --wrap: 1600px;
  --topbar-h: 42px;
  --navbar-h: 34px;

  /* --- motion -------------------------------------------------------------
     Two durations, because there are only two kinds of motion here: a state
     the pointer is causing (--dur-1, felt rather than watched) and something
     arriving on the page (--dur-2). Anything slower than this on a tool
     somebody keeps open all day stops being feedback and becomes a wait.
     The easing decelerates: it starts at full speed, so the response reads as
     immediate even though it takes 90ms to finish. See section 13. */
  --dur-1: 90ms;
  --dur-2: 160ms;
  --ease: cubic-bezier(0.2, 0, 0.2, 1);
}

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

  /* Re-designed rather than tinted: a slate-navy page with three lighter
     planes above it, so the same recession that separates page from card from
     control in light still exists here. */
  --bg: #0d131f;
  --surface: #161e2c;
  --surface-2: #1b2433;
  --surface-3: #212c3e;

  --line: #28334a;
  --line-2: #3a4761;

  --ink: #e6ebf3;
  --ink-2: #c4cddc;
  --ink-3: #94a2b8;
  --ink-4: #5f6e87;

  /* Indigo lifts; --brand-fill does not, because a plate under a white label
     has to stay deep in both themes or the label falls below 4.5:1. */
  --brand: #6366f1;
  --brand-strong: #a5b4fc;
  --brand-soft: #1e1b4b;
  --brand-fill-hover: #5a51ea;

  /* Status inks invert into the pastel end of each hue and the plates become
     deep wells. A #04724d word on a navy card is unreadable, and an #e4f4ec
     plate on one is a hole punched in the page. */
  --ok: #6ee7b7;
  --ok-bg: #10291f;
  --ok-line: #1f5540;
  --warn: #fbbf24;
  --warn-bg: #2c2313;
  --warn-line: #5c4517;
  --bad: #fda4af;
  --bad-bg: #2e161e;
  --bad-line: #5f2734;
  /* Lifted just enough to clear 3:1 against the page it sits on, and no
     further: white on it is 5.65:1, and the hover is LIGHTER rather than
     darker, the same direction --brand-fill-hover moves in this theme. */
  --bad-fill: #c81e42;
  --bad-fill-hover: #d92650;
  --off: #94a2b8;
  --off-bg: #1f2836;
  --off-line: #33405a;

  --chart-1: #818cf8;
  --chart-2: #38bdf8;
  --chart-3: #2dd4bf;
  --chart-4: #c084fc;
  --chart-5: #fbbf24;
  --chart-grid: #28334a;
  --chart-track: #232f44;

  --link: #a5b4fc;
  --link-hover: #c7d2fe;
  --focus: #818cf8;

  --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45);
  --shadow-2: 0 6px 20px rgba(0, 0, 0, 0.55);
}

/* ==========================================================================
   2. Base
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: var(--fs-md) / var(--lh) var(--font);
  -webkit-font-smoothing: antialiased;
}

a {
  color: var(--link);
  text-decoration: none;
}

a:hover {
  color: var(--link-hover);
  text-decoration: underline;
}

/* THE FOCUS RING IS ONE RULE AND IT IS NEVER REMOVED. An admin behind a TOTP
   gate is operated by keyboard more than most screens, and `outline: none` on
   a control is how that stops working. :focus-visible keeps it off a mouse
   click, offset-2 keeps it clear of the control's own border. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

::selection {
  background: var(--brand-soft);
  color: var(--brand-strong);
}

/* Keyboard and screen-reader escape from the nav row. Visible only on focus,
   which is the point: it is chrome for the people who need it. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: var(--s-3) var(--s-5);
  background: var(--brand-fill);
  color: var(--on-brand);
  border-radius: 0 0 var(--r) 0;
}

.skip:focus {
  left: 0;
  text-decoration: none;
}

/* ==========================================================================
   3. Shell - topbar, nav, page
   ========================================================================== */

.topbar {
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: var(--s-4);
  min-height: var(--topbar-h);
  padding: 0 var(--s-6);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

/* The wordmark is a flex row with its own baseline, so the mark, the product
   name and the role tag cannot be pulled apart by the header's alignment. */
.wordmark {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  min-width: 0;
  color: var(--ink);
  font-size: var(--fs-lg);
  font-weight: 650;
  letter-spacing: -0.01em;
  line-height: var(--lh-tight);
}

.wordmark:hover {
  color: var(--ink);
  text-decoration: none;
}

.wordmark svg {
  display: block;
  flex: none;
}

.wordmark .name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Which server this is. The Handle only ever runs on central, and saying so in
   the chrome is cheaper than an operator wondering. */
.role-tag {
  flex: none;
  padding: 1px var(--s-3);
  border: 1px solid var(--line-2);
  border-radius: var(--r-pill);
  color: var(--ink-3);
  font-size: var(--fs-2xs);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

.topbar-end {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  margin-left: auto;
}

/* The logout form. A flex row rather than `display: contents`, which drops the
   element from the accessibility tree in more than one shipping engine and is
   not worth it to save one box. */
.topbar-end form {
  display: flex;
  align-items: center;
}

/* THE SCRAPER WARNING. A .pill from section 6 serving as a link, so all it
   needs from here is to lose the underline: the plate, the ink and the dark
   theme all come from .pill.bad, and section 13 already transitions its
   border because it is an anchor. It is the one thing in this bar that is
   ABSENT on a normal day, which is the whole reason it gets noticed on the
   day it appears. See control/_scraper_badge.html. */
.scraper-alert {
  text-decoration: none;
}

.scraper-alert:hover {
  border-color: var(--bad);
  text-decoration: none;
}

.who {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-width: 0;
  color: var(--ink-3);
  font-size: var(--fs-sm);
}

.who strong {
  overflow: hidden;
  color: var(--ink-2);
  font-weight: 600;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The operator's initial. Not an avatar: there is no photo on a staff account
   and there never will be, so this is one glyph and no request. */
.who .mark {
  display: grid;
  flex: none;
  place-items: center;
  width: 22px;
  height: 22px;
  background: var(--brand-soft);
  border-radius: var(--r-pill);
  color: var(--brand-strong);
  font-size: var(--fs-2xs);
  font-weight: 700;
  text-transform: uppercase;
}

.navbar {
  position: sticky;
  top: var(--topbar-h);
  z-index: 29;
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
}

/* Horizontal scroll rather than a wrap or a burger: the row is nine items
   wide, it fits at 1280 with room to spare, and on anything narrower an
   operator would rather swipe it than lose an item behind a menu. */
.navbar-inner {
  display: flex;
  gap: 2px;
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 var(--s-6);
  overflow-x: auto;
  scrollbar-width: thin;
}

.nav-link {
  display: flex;
  align-items: center;
  min-height: var(--navbar-h);
  padding: 0 var(--s-3);
  border-bottom: 2px solid transparent;
  color: var(--ink-2);
  font-size: var(--fs-sm);
  font-weight: 500;
  white-space: nowrap;
}

.nav-link:hover {
  background: var(--surface-3);
  color: var(--ink);
  text-decoration: none;
}

.nav-link[aria-current="page"] {
  border-bottom-color: var(--brand);
  color: var(--brand-strong);
  font-weight: 650;
}

.nav-sep {
  flex: none;
  align-self: center;
  width: 1px;
  height: 18px;
  margin: 0 var(--s-3);
  background: var(--line-2);
}

.page {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: var(--s-5) var(--s-6) var(--s-7);
}

.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--s-4);
  flex-wrap: wrap;
  margin-bottom: var(--s-4);
  padding-bottom: var(--s-3);
  border-bottom: 1px solid var(--line);
}

.page-actions {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

h1 {
  margin: 0;
  font-size: var(--fs-xl);
  font-weight: 650;
  letter-spacing: -0.01em;
  line-height: var(--lh-tight);
}

.sub {
  margin: var(--s-1) 0 0;
  color: var(--ink-3);
  font-size: var(--fs-sm);
}

/* dashboard.html renders `<p class="sub">{% block subtitle %}{% endblock %}</p>`
   unconditionally, so a screen that sets no subtitle would otherwise reserve a
   blank line under every heading. */
.sub:empty {
  display: none;
}

/* ==========================================================================
   4. Cards and layout
   ========================================================================== */

/* auto-fit against an intrinsic floor, so the column count is computed from
   the space the grid has instead of declared per breakpoint: two columns at
   1280, three or four on a wide monitor, one on a laptop in a split view. */
.grid {
  display: grid;
  gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
}

.card {
  margin-bottom: var(--gap);
  padding: var(--pad-card);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-1);
}

.grid > .card {
  margin-bottom: 0;
}

.card > h2 {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  min-width: 0;
  margin: 0 0 var(--s-3);
  font-size: var(--fs-md);
  font-weight: 650;
  line-height: var(--lh-tight);
}

.card > h2 .note {
  color: var(--ink-3);
  font-size: var(--fs-xs);
  font-weight: 400;
}

/* dashboard.html puts a `.note` in every card title whether or not the link it
   would hold exists, and an empty flex item still spends a gap. */
.card > h2 .note:empty {
  display: none;
}

.card > p:last-child {
  margin-bottom: 0;
}

.note {
  color: var(--ink-3);
  font-size: var(--fs-xs);
  font-weight: 400;
}

/* A table wider than its card scrolls INSIDE the card. The page body must
   never scroll sideways: an operator who has to pan the whole document to read
   a count has lost the header and the nav with it. */
.wide {
  margin: 0 calc(var(--pad-card) * -1);
  padding: 0 var(--pad-card);
  overflow-x: auto;
  scrollbar-width: thin;
}

.card > .wide:not(:last-child) {
  margin-bottom: var(--s-3);
}

/* A ROW WITH A THUMBNAIL AND A BODY BESIDE IT, and a LIST of them.
   The approval queue is the first screen to need one - a cover, then everything
   the operator has to read and every control they decide with - and it is the
   shape any "picture plus record" list on this product will want. It is a grid
   and not a flex row for one reason: the second track is `minmax(0, 1fr)`, so a
   long unbroken title or a wide control row inside it shrinks instead of pushing
   the card past the page and taking the sticky header sideways with it.

   The rows are separated by a HAIRLINE rather than by a gap between cards. A
   backlog is read down a column: fifty bordered boxes cost fifty borders' worth
   of vertical space and give the eye fifty edges to skip over, where one panel
   per site with ruled rows inside it reads as a list. */
.media {
  display: grid;
  grid-template-columns: 56px minmax(0, 1fr);
  gap: var(--s-3);
  align-items: start;
}

/* 56px and not 64. The well is 2:3, so its WIDTH sets the row's floor: 64px of
   cover is 96px of row before a word is read, and the approval queue is forty
   of these. 56 is 84 - still a cover an operator recognises, 12px less row. */

.media + .media {
  margin-top: var(--s-3);
  padding-top: var(--s-3);
  border-top: 1px solid var(--line);
}

/* The picture WELL, not the picture. The queue's covers are fetched from the
   publisher boxes and an image that does not load removes itself, so the box has
   to be what holds the column open - otherwise one dead cover shifts a whole
   row's text 64px left and the list stops scanning. `aspect-ratio` keeps the
   well at cover proportions without the view having to know the file's. */
.media .thumb {
  aspect-ratio: 2 / 3;
  background: var(--surface-3);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  overflow: hidden;
}

.media .thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The body's own blocks sit on one tight rhythm. A queue row carries a title, a
   line of pills, a line of metadata, a synopsis, a note, a group of fields and a
   row of buttons; at the browser's default paragraph margin that is over 100px
   of air per row, and this screen is scrolled forty rows at a time. Matched as
   "whatever follows the thumb" so the body needs no class of its own. */
.media > .thumb + * > * {
  margin: 0 0 var(--s-2);
}

.media > .thumb + * > :last-child {
  margin-bottom: 0;
}

/* ==========================================================================
   5. Tables
   ========================================================================== */

table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

th,
td {
  padding: var(--cell-y) var(--cell-x) var(--cell-y) 0;
  border-bottom: 1px solid var(--line);
  line-height: 1.4;
  text-align: left;
  white-space: nowrap;
  vertical-align: baseline;
}

/* Not sticky. `.wide` is an overflow container, so `position: sticky` on a
   header cell would resolve against that box rather than against the viewport
   and pin nothing; a sticky head here needs the scroll container to move, and
   it does not. */
th {
  background: var(--surface);
  color: var(--ink-3);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

tbody tr:hover td,
tr:hover td {
  background: var(--surface-2);
}

tr:last-child td {
  border-bottom: none;
}

td.n,
th.n {
  padding-right: var(--s-4);
  text-align: right;
}

/* ==========================================================================
   6. Status: pills, inks, meters, stats
   ========================================================================== */

/* The four state names are the CSS classes dashboard.py emits. As TEXT they
   colour a word; as a `.pill` they get the plate and the hairline too. */
.ok {
  color: var(--ok);
}

.warn {
  color: var(--warn);
}

.bad {
  color: var(--bad);
}

.off {
  color: var(--off);
}

.muted {
  color: var(--ink-3);
}

/* "This count is zero", "this never happened". The class stays, because the
   templates mark absence deliberately and that intent is worth keeping, but it
   is painted with the muted ink rather than with --ink-4: a zero is a reading.
   4.98:1 at worst in light, 5.43:1 in dark, against 2.27:1 and 2.72:1 before.
   It does not stop signalling by getting legible - the contrast that matters
   is with the numbers BESIDE it in the same column, and those are --ink at
   15.79:1, so a zero still drops a visible step out of its row. That it now
   matches .muted is correct rather than a collision: "0" and "no report yet"
   are the same statement, and the old palette said one of them twice as
   quietly as the other for no reason anyone could name. */
.zero {
  color: var(--ink-3);
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  padding: 1px var(--s-3);
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  line-height: 1.6;
  white-space: nowrap;
}

.pill.ok {
  background: var(--ok-bg);
  border-color: var(--ok-line);
  color: var(--ok);
}

.pill.warn {
  background: var(--warn-bg);
  border-color: var(--warn-line);
  color: var(--warn);
}

.pill.bad {
  background: var(--bad-bg);
  border-color: var(--bad-line);
  color: var(--bad);
}

.pill.off {
  background: var(--off-bg);
  border-color: var(--off-line);
  color: var(--off);
}

/* THE METER. `<div class="bar STATE"><span style="width: N%"></span></div>`.
   The fill is a DIRECT child so this rule can never reach into a nested
   element - which is exactly the bug that broke the old page header, where
   `header.bar` and this utility shared a name and `.bar span` repainted the
   header's own spans. */
.bar {
  height: 6px;
  margin: var(--s-2) 0 var(--s-1);
  background: var(--chart-track);
  border-radius: var(--r-pill);
  overflow: hidden;
}

.bar > span {
  display: block;
  height: 100%;
  background: var(--brand);
  border-radius: inherit;
}

.bar.ok > span {
  background: var(--ok);
}

.bar.warn > span {
  background: var(--warn);
}

.bar.bad > span {
  background: var(--bad);
}

/* --off, not --ink-4: every other .bar.STATE fills with its own status token
   and this one alone reached for the grey. The token exists and is the same
   family, and it lifts the fill from 2.17:1 against the track to 4.75:1 in
   light (2.60 to 5.20 in dark), so an "off" meter is a bar the operator can
   see rather than a track that looks empty. */
.bar.off > span {
  background: var(--off);
}

/* A headline number and its caption. Inline-block rather than a grid because
   a card carries between one and four of them and they should simply wrap. */
.stat {
  display: inline-block;
  margin: 0 var(--s-6) var(--s-2) 0;
  vertical-align: top;
}

.stat:last-child {
  margin-right: 0;
}

.big {
  display: block;
  font-size: var(--fs-2xl);
  font-weight: 650;
  letter-spacing: -0.02em;
  line-height: 1.1;
}

.big a {
  color: inherit;
}

.big a:hover {
  color: var(--link);
  text-decoration: none;
}

.stat .label,
.label {
  display: block;
  color: var(--ink-3);
  font-size: var(--fs-xs);
}

/* Sizing hooks for the inline SVG the dashboard draws its informatics with.
   The <svg> itself is generated by the view or written into the template; all
   this file supplies is the box it sits in and the palette it fills from. */
.chart {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.spark {
  display: inline-block;
  height: 1.1em;
  vertical-align: -0.15em;
}

.legend {
  display: flex;
  gap: var(--s-4);
  flex-wrap: wrap;
  margin: var(--s-2) 0 0;
  color: var(--ink-3);
  font-size: var(--fs-xs);
}

.legend > span {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}

.legend i {
  width: 9px;
  height: 9px;
  border-radius: 2px;
  background: currentColor;
}

/* ==========================================================================
   7. Buttons
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: var(--ctl-h);
  padding: var(--ctl-y) var(--ctl-x);
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  color: var(--ink-2);
  cursor: pointer;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
}

.btn:hover {
  background: var(--surface-3);
  border-color: var(--ink-4);
  color: var(--ink);
  text-decoration: none;
}

.btn:active {
  transform: translateY(1px);
}

.btn[disabled] {
  cursor: not-allowed;
  opacity: 0.55;
}

.btn-primary {
  background: var(--brand-fill);
  border-color: var(--brand-fill);
  color: var(--on-brand);
}

.btn-primary:hover {
  background: var(--brand-fill-hover);
  border-color: var(--brand-fill-hover);
  color: var(--on-brand);
}

/* THE THIRD BUTTON ROLE, and the one the file was missing. --bad-fill,
   --bad-fill-hover and --on-bad have been in the token block from the start
   "used by the Handle's own confirm screen and by admin-theme.css", so the
   admin's delete link has had a destructive button all along and a hand-built
   screen has not - which is how the approval queue ended up with a `.danger`
   of its own inside a page-local stylesheet.

   It is an INK on the ordinary button and not the red plate, deliberately. The
   plate means "this click is the deletion" and belongs to a confirmation screen,
   where it is the only filled control on the page. Reject sits in a row of five
   buttons that repeats forty times down a queue: forty red plates would drown
   out the one indigo Approve beside each of them, and a plate that shouts on
   every row stops being a warning. The word is red, its neighbours are not, and
   the hover fills in the pale plate the pills already use for the same state. */
.btn-danger {
  color: var(--bad);
}

.btn-danger:hover {
  background: var(--bad-bg);
  border-color: var(--bad-line);
  color: var(--bad);
}

.btn-quiet {
  background: transparent;
  border-color: transparent;
  color: var(--ink-3);
}

.btn-quiet:hover {
  background: var(--surface-3);
  border-color: var(--line-2);
}

/* The sign-in page's one button, and the one place on this product where a
   control is the subject of the screen rather than an instrument on it. It
   opts out of --ctl-h deliberately. */
.btn-block {
  width: 100%;
  min-height: 0;
  padding: var(--s-4);
  font-size: var(--fs-md);
}

/* The theme toggle. Square, iconic, and shipped `hidden` by base.html so that
   a session without JavaScript is never offered a control that cannot work. */
.btn-icon {
  width: var(--ctl-h);
  height: var(--ctl-h);
  min-height: 0;
  padding: 0;
}

.btn-icon svg {
  display: block;
}

/* The icon shows what the button WILL DO, not what the page currently is: a
   moon on a light page, a sun on a dark one. A control that pictures its own
   state reads as a status light and gets clicked by accident. */
html[data-theme="dark"] .icon-moon,
html:not([data-theme="dark"]) .icon-sun {
  display: none;
}

/* ==========================================================================
   8. Forms
   ========================================================================== */

.field {
  margin-bottom: var(--s-4);
}

.field > label,
.field-label {
  display: block;
  margin-bottom: var(--s-2);
  color: var(--ink-2);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.field .hint {
  display: block;
  margin-top: var(--s-2);
  color: var(--ink-3);
  font-size: var(--fs-xs);
}

/* Per-field errors. Django's own `errorlist` markup is styled too, so a
   `{{ field.errors }}` dropped into a Handle template is not naked. */
.field-errors,
.field .errorlist {
  margin: var(--s-2) 0 0;
  padding: 0;
  color: var(--bad);
  font-size: var(--fs-xs);
  list-style: none;
}

/* Attribute selectors, not a class: the login form's widgets are rendered by
   django_otp's own form and this stylesheet cannot put a class on them. */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="search"],
input[type="url"],
input[type="number"],
input[type="datetime-local"],
input[type="date"],
select,
textarea {
  display: block;
  width: 100%;
  min-height: var(--ctl-h);
  padding: var(--ctl-y) var(--s-3);
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  color: var(--ink);
  font: inherit;
  font-size: var(--fs-sm);
}

textarea {
  min-height: 64px;
  resize: vertical;
}

/* The other place --ink-4 was painting glyphs. A placeholder is text a person
   reads to find out what the field wants, so it is not the "incidental" or
   "inactive" content 1.4.3 exempts - it was 2.58:1 in light and 3.24:1 in
   dark. --ink-3 takes it to 5.64:1 and 6.46:1 while staying a long way off
   --ink (17.90:1), which is the difference that has to hold: a placeholder
   must not read as a value already typed in. */
input::placeholder,
textarea::placeholder {
  color: var(--ink-3);
}

input:focus,
select:focus,
textarea:focus {
  border-color: var(--brand);
}

input[aria-invalid="true"],
.has-error input,
.has-error select {
  border-color: var(--bad);
}

/* The one-time code. Wide tracking and a mono face so six digits are checked
   at a glance and a transposition is visible before the form is submitted. */
.otp-input {
  font-family: var(--font-mono);
  font-size: 1.25rem;
  font-weight: 600;
  letter-spacing: 0.35em;
  text-align: center;
}

/* A ROW OF CONTROLS THAT BELONG TO EACH OTHER: buttons, and the input a button
   reads. It has to live down here rather than up with the layout section for
   the same reason .checkline does - the rule above makes every text control a
   full-width BLOCK, which is right for a stacked form and wrong for a date
   sitting beside the button that schedules it, and only a rule declared after
   that one wins on equal specificity.

   It wraps, and that is not decoration: an approval row carries five buttons
   and a datetime input, and without it the row simply runs off the side of the
   card on a laptop - taking the sticky header and the nav with it, because the
   document is what would scroll. */
.controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-2);
}

.controls input,
.controls select {
  width: auto;
}

.checkline {
  display: flex;
  align-items: center;
  gap: var(--s-3);
}

.checkline input {
  width: auto;
}

/* ==========================================================================
   9. Messages and notices
   ========================================================================== */

.msgs {
  margin: 0 0 var(--s-4);
  padding: 0;
  list-style: none;
}

.notice,
.msgs li {
  display: block;
  margin-bottom: var(--s-2);
  padding: var(--s-3) var(--s-4);
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--ink-4);
  border-radius: var(--r);
  color: var(--ink-2);
  font-size: var(--fs-sm);
}

.notice p,
.msgs li p {
  margin: 0;
}

.notice p + p {
  margin-top: var(--s-2);
}

.notice strong {
  color: var(--ink);
}

.notice-ok,
.msgs li.success {
  background: var(--ok-bg);
  border-color: var(--ok-line);
  border-left-color: var(--ok);
  color: var(--ok);
}

.notice-warn,
.msgs li.warning {
  background: var(--warn-bg);
  border-color: var(--warn-line);
  border-left-color: var(--warn);
  color: var(--warn);
}

.notice-bad,
.msgs li.error {
  background: var(--bad-bg);
  border-color: var(--bad-line);
  border-left-color: var(--bad);
  color: var(--bad);
}

.notice-ok strong,
.notice-warn strong,
.notice-bad strong {
  color: inherit;
}

/* ==========================================================================
   10. The sign-in page
   ========================================================================== */

/* Its own body class, because this is the one screen with no shell: no nav, no
   identity, nothing to reach. A single centred card and a footer note. */
.signin {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  min-height: 100dvh;
  padding: var(--s-6);
  /* Two very soft indigo washes rather than a flat plane. This is the only
     decorative rule in the file and it is here because the login screen is the
     one Handle page that is looked at rather than worked in. */
  background:
    radial-gradient(60rem 30rem at 15% -10%, color-mix(in srgb, var(--brand) 9%, transparent), transparent 70%),
    radial-gradient(50rem 28rem at 105% 110%, color-mix(in srgb, var(--chart-2) 9%, transparent), transparent 70%),
    var(--bg);
}

.signin-wrap {
  width: 100%;
  max-width: 400px;
}

.signin-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
  margin-bottom: var(--s-5);
  color: var(--ink);
  font-size: 1.375rem;
  font-weight: 650;
  letter-spacing: -0.02em;
}

.signin-card {
  padding: var(--s-7) var(--s-6) var(--s-6);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-2);
}

/* THE ONE SCREEN THAT OPTS OUT OF THE DENSITY PASS. Everything above is
   tuned for an operator scanning a tool they already know; this is the page
   they meet before they are anybody, it holds two fields, and there is nothing
   below the fold to make room for. The controls keep the size they had. */
.signin-card input,
.signin-card select {
  min-height: 0;
  padding: var(--s-3) var(--s-4);
  font-size: var(--fs-md);
}

.signin-card h1 {
  font-size: var(--fs-lg);
}

.signin-card .sub {
  margin-bottom: var(--s-6);
}

.signin-card form {
  margin: 0;
}

.signin-foot {
  margin: var(--s-5) 0 0;
  color: var(--ink-3);
  font-size: var(--fs-xs);
  text-align: center;
}

.signin-foot a {
  color: var(--ink-3);
  text-decoration: underline;
}

/* The step marker. django_otp's form is one form in two passes - password
   first, then the code - and the operator has no other way of telling which
   pass they are on. */
.steps {
  display: flex;
  gap: var(--s-2);
  margin-bottom: var(--s-5);
}

.steps span {
  flex: 1;
  height: 3px;
  background: var(--line);
  border-radius: var(--r-pill);
}

.steps span.on {
  background: var(--brand);
}

/* ==========================================================================
   11. The info affordance
   ==========================================================================

   ONE COMPONENT, AND THE ONLY ONE, FOR EXPLAINING A CONTROL.

   The Handle knew a great deal that its screens said out loud: what Live
   actually predicates, what delete-and-rescrape does that delete-and-block does
   not, why nothing ever auto-unpublishes. All of it is worth money to get
   right and none of it is worth re-reading daily, which is what a paragraph
   pinned to a screen asks of the one operator who opens that screen every
   morning. This is where that prose goes: one gesture away, not deleted.

   THE MARKUP, and it is the whole contract:

     <button type="button" class="info-btn" popovertarget="info-live"
             aria-describedby="info-live" aria-label="What Live means"></button>
     <div class="info-pop" id="info-live" popover role="note">
       <p>Live is the visibility predicate, not the stored status.</p>
     </div>

   control/_info.html renders exactly that and is the way to get it; the raw
   form is for a panel that needs real markup inside it. Both attributes on the
   trigger do different jobs and both are required: popovertarget is what OPENS
   the panel - natively, with no script, with Escape and outside-click included
   - and aria-describedby is what makes the panel the trigger's description, so
   the explanation is announced when the trigger is FOCUSED and a screen reader
   user never has to open anything.

   WHY A POPOVER AND NOT A `<details>`. `.results` and `.wide` are overflow
   containers, and a `<details>` panel inside one is clipped by it however it is
   positioned - there is no z-index that escapes an ancestor's overflow. A
   popover is painted in the TOP LAYER, outside the whole box tree, so the same
   component works in a changelist row, in a fieldset, in a sticky header and in
   a card. `<details class="dash-more">` is still the right answer for prose
   that belongs IN the flow and is merely long; this one is for prose that
   should not be in the flow at all.

   HOVER IS NOT AN OPENING GESTURE HERE. It is invisible to a keyboard, absent
   on a touch screen and a trap for anyone who needs to read at their own pace.
   The panel opens on CLICK and on Enter or Space, which is what a <button> does
   for free.

   WHAT THIS FILE DOES NOT DO. Placement. handle-info.js measures the trigger
   and sets `top`/`left`, because a stylesheet cannot know that the trigger is
   40px from the bottom of the window. Everything below is inert without it: an
   unplaced popover renders at its static position, which is beside its trigger
   and merely less careful about the viewport edge. */

/* THE TRIGGER. A 24x24 hit target - WCAG 2.5.8's floor - carrying a 15px ring,
   so the control is big enough to hit and small enough not to shove a heading
   around. The negative inline margin gives the surplus back to the layout: the
   glyph sits 15px from its neighbour and the other 9px of target overhangs. */
.info-btn {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 24px;
  height: 24px;
  margin: -4px calc(var(--s-1) * -1) -4px 0;
  padding: 0;
  background: none;
  border: 0;
  color: var(--ink-3);
  cursor: pointer;
  font: inherit;
  vertical-align: middle;
}

/* The glyph is generated content, which is why the trigger MUST carry an
   aria-label: an accessible name computed from a CSS "i" is no name at all.
   Every text property is restated because this control is dropped into an
   uppercase, letter-spaced, 650-weight fieldset heading as often as into a
   paragraph, and it must look the same in both. */
.info-btn::before {
  content: "i";
  display: grid;
  place-items: center;
  width: 15px;
  height: 15px;
  border: 1px solid currentColor;
  border-radius: var(--r-pill);
  font-family: var(--font);
  font-size: 10px;
  font-style: normal;
  font-weight: 700;
  letter-spacing: 0;
  line-height: 1;
  text-transform: none;
}

.info-btn:hover {
  color: var(--ink);
}

/* WHERE A TRIGGER IS ALLOWED TO SIT, and what has to become a flex row to hold
   it. An <h1> and a field label are blocks, so a 24px inline-grid child inside
   one drops onto the text baseline and drags the line box down with it; as a
   flex row the glyph is centred against the words and the heading keeps its
   height. `min-width: 0` is the other half: without it a long title stops
   shrinking and pushes the trigger off the end of the row.

   `.card > h2` is already a flex row for its own reasons (section 4) and needs
   nothing here. Anywhere the trigger follows INLINE text - inside a <p>, a
   table cell, a pill's caption - it needs no rule at all: inline-grid and
   `vertical-align: middle` do the work. */
h1:has(> .info-btn),
.field > label:has(> .info-btn),
.field-label:has(> .info-btn) {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  min-width: 0;
}

/* Open is a STATE and it is drawn, because on a screen with eight of these the
   operator has to be able to see which one is talking. */
.info-btn[aria-expanded="true"] {
  color: var(--brand-strong);
}

.info-btn[aria-expanded="true"]::before {
  background: var(--brand-soft);
  border-color: var(--brand);
}

/* The focus ring comes from the one rule in section 2 and is not touched here.
   It draws around the 24px box, which is the target, not around the 15px ring.

   THE PANEL. Sized for one short explanation - 320px, or the window less its
   margins on anything narrower - and capped in height by the script to whatever
   room the trigger has, so a long one scrolls inside itself instead of off the
   screen.

   THE UA'S OWN `inset: 0; margin: auto` IS LEFT ALONE, on purpose and after
   trying the alternative. Overriding it to `inset: auto` was meant to drop the
   panel at its static position - beside its trigger - for a session with no
   JavaScript; measured in a changelist cell it lands in the window's top-left
   corner instead, because a top-layer box has no useful static position to
   resolve against. Centred is the better of the two honest answers: it is
   unmistakably a panel, it is never off screen, and it is dismissed the same
   way. handle-info.js sets `inset: auto` with a `top` and a `left` inline when
   it runs, so this only ever describes the degraded case. */
.info-pop {
  z-index: 60;
  width: max-content;
  max-width: min(320px, calc(100vw - 2 * var(--s-5)));
  padding: var(--s-3) var(--s-4);
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  box-shadow: var(--shadow-2);
  color: var(--ink-2);
  /* Restated for the same reason the glyph's are: the panel is opened from
     inside table cells (`white-space: nowrap`), uppercase headings and bold
     labels, and none of that may reach the prose. */
  font: var(--fs-sm) / 1.5 var(--font);
  font-weight: 400;
  letter-spacing: normal;
  text-align: left;
  text-transform: none;
  white-space: normal;
}

.info-pop p {
  margin: 0 0 var(--s-2);
}

.info-pop > :last-child {
  margin-bottom: 0;
}

.info-pop strong {
  color: var(--ink);
}

.info-pop code {
  padding: 1px 4px;
  background: var(--surface-3);
  border-radius: var(--r-sm);
  color: var(--ink-2);
  font-family: var(--font-mono);
  font-size: 0.9em;
}

/* THE FALLBACK, for an engine with no popover API. handle-info.js stamps
   `info-fallback` on <html> only in that case and drives `.is-open` by hand;
   on every current browser this pair of rules never matches anything, because
   the UA stylesheet is already hiding the closed panel and the class is never
   set. Both halves are needed: `position: fixed` because the script portals the
   panel to <body> to keep it out of a scroll container, and the display rule
   because `[popover]` is an unknown attribute there and the panel would
   otherwise be a visible block of prose sitting in the page. */
.info-fallback .info-pop {
  position: fixed;
  display: none;
}

.info-fallback .info-pop.is-open {
  display: block;
}

/* LINE CLAMPS. Not part of the affordance, but the other half of the same job:
   a cell or a summary that must show its first line or two and no more. Offered
   here rather than typed into a screen sheet because the changelist, the queue
   and the dashboard all want it and a clamp written three times is three
   clamps. -webkit-box is still the only clamp with universal support; the
   standard `line-clamp` is listed after it so the two agree wherever both are
   understood. */
.clamp-1,
.clamp-2,
.clamp-3 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
}

.clamp-1 {
  -webkit-line-clamp: 1;
  line-clamp: 1;
}

.clamp-2 {
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

.clamp-3 {
  -webkit-line-clamp: 3;
  line-clamp: 3;
}

/* ==========================================================================
   12. Narrow viewports
   ========================================================================== */

@media (max-width: 720px) {
  .topbar,
  .navbar-inner,
  .page {
    padding-left: var(--s-4);
    padding-right: var(--s-4);
  }

  .role-tag,
  .who strong {
    display: none;
  }

  .card {
    padding: var(--s-3);
  }

  /* The window is the constraint now, not the trigger. */
  .info-pop {
    max-width: calc(100vw - 2 * var(--s-4));
  }
}

/* ==========================================================================
   13. Motion
   ========================================================================== */

/* EVERY STATE BELOW WAS ALREADY HERE AND NONE OF THEM MOVED. Sections 3 to 11
   give hover, focus and pressed styling to every control on the product, and
   until this section each one jumped between two frames with nothing in
   between. That is not a missing decoration. A change with no duration does not
   read as something the pointer is causing, and on a dense table of
   same-height rows the jump is the difference between "this row is under my
   cursor" and "the page just redrew".

   SO THESE RULES ARE FAST AND THEY MOVE NO LAYOUT. Nothing here transitions
   width, height, margin, padding or top, so no animation can reflow a table
   under a cursor that is already moving towards something. The only movement is
   the 1px press on .btn, which section 7 has had all along - the transition
   merely stops it snapping - and a 4px rise on things that were not on the page
   a moment ago.

   NOTHING ON THE CONTENT ITSELF. A changelist of 200 rows that fades in is 200
   rows an operator waits to read. What animates on arrival is a message and a
   popover panel, because in both cases the movement IS the information: this is
   new, and it came from the thing you just clicked.

   The blanket prefers-reduced-motion rule below is what makes all of it
   optional. It was written before any of this existed and covers every property
   here without naming one. */

/* Controls. Colour, border and shadow - the properties the state rules in
   sections 5 to 8 actually change. */
a,
button,
.btn,
.info-btn,
summary {
  transition:
    background-color var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease),
    box-shadow var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}

/* The press from section 7, smoothed in both directions. */
.btn {
  transition:
    background-color var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease),
    box-shadow var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease),
    transform var(--dur-1) var(--ease);
}

/* Fields. The focus ring in section 2 is a box-shadow, so it fades in with the
   border rather than appearing a frame ahead of it. */
input,
select,
textarea {
  transition:
    background-color var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease),
    box-shadow var(--dur-1) var(--ease);
}

/* Rows. The whole reason for --dur-1 being as short as it is: a pointer crosses
   several of these in a second and each one has to have finished by then. */
tbody tr {
  transition: background-color var(--dur-1) var(--ease);
}

/* Things that were not on the page a moment ago.

   AN ANIMATION AND NOT A TRANSITION, deliberately. A transition needs a
   from-state, which for a popover means `opacity: 0` in the base rule - and if
   :popover-open is not supported, or the panel is reached some other way, that
   base rule is a permanently invisible explanation. An animation on the OPEN
   state cannot fail that way: no support means no animation and a panel that is
   simply there, which is what section 11 promised. */
@keyframes handle-rise-in {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }
}

.info-pop:popover-open {
  animation: handle-rise-in var(--dur-2) var(--ease);
}

.notice {
  animation: handle-rise-in var(--dur-2) var(--ease);
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  .btn:active {
    transform: none;
  }
}

@media print {
  .topbar,
  .navbar,
  .skip,
  .info-btn {
    display: none;
  }

  .card {
    box-shadow: none;
  }
}
